This commit is contained in:
Wavering Ana 2025-01-29 01:29:27 -05:00
parent 5e83c1218b
commit a5753a044e
10 changed files with 141 additions and 179 deletions

View file

@ -1,3 +1,4 @@
DATABASE_URL=postgresql://user:password@localhost/dbname
SERVER_HOST=127.0.0.1
SERVER_PORT=8080
JWT_SECRET=change-me-in-production

64
Cargo.lock generated
View file

@ -1857,6 +1857,40 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-embed"
version = "6.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "6.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "7.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -1897,6 +1931,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.27"
@ -2068,8 +2111,10 @@ dependencies = [
"dotenv",
"jsonwebtoken",
"lazy_static",
"mime_guess",
"rand",
"regex",
"rust-embed",
"serde",
"serde_json",
"sqlx",
@ -2736,6 +2781,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -2832,6 +2887,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View file

@ -8,6 +8,7 @@ name = "simplelink"
path = "src/lib.rs"
[dependencies]
rust-embed = "6.8"
jsonwebtoken = "9"
actix-web = "4.4"
actix-files = "0.6"
@ -28,4 +29,5 @@ chrono = { version = "0.4", features = ["serde"] }
regex = "1.10"
lazy_static = "1.4"
argon2 = "0.5.3"
rand = { version = "0.8", features = ["std"] }
rand = { version = "0.8", features = ["std"] }
mime_guess = "2.0.5"

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# SimpleLink
A very performant and light (6mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres.
![MainView](readme_img/mainview.jpg)
![StatsView](readme_img/statview.jpg)
## Build
### From Source
First configure .env.example and save it to .env
The project will not run withot DATABASE_URL set. (TODO add sqlite support)
```bash
#set api-domain to where you will be deploying the link shortener, eg: link.example.com, default is localhost:8080
git clone https://github.com/waveringana/simplelink && cd simplelink
./build.sh api-domain=localhost:8080
cargo run
```
Alternatively if you want a binary form
```bash
./build.sh --binary
```
then check /target/release for the binary named `SimpleGit`
### From Docker
```bash
docker build --build-arg API_URL=http://localhost:8080 -t simplelink .
docker run simplelink -p 8080:8080 \
-e JWT_SECRET=change-me-in-production \
-e DATABASE_URL=postgres://user:password@host:port/database \
simplelink
```
### From Docker Compose
Adjust the included docker-compose.yml to your liking, it includes a postgres config as well.

View file

@ -3,6 +3,7 @@
# Default values
API_URL="http://localhost:8080"
RELEASE_MODE=false
BINARY_MODE=false
# Parse command line arguments
for arg in "$@"
@ -16,6 +17,10 @@ do
RELEASE_MODE=true
shift
;;
--binary)
BINARY_MODE=true
shift
;;
esac
done
@ -45,13 +50,9 @@ npm install
npm run build
cd ..
# Create static directory if it doesn't exist
# Create static directory and copy frontend build
mkdir -p static
# Clean existing static files
rm -rf static/*
# Copy built files to static directory
cp -r frontend/dist/* static/
# Build Rust project
@ -62,15 +63,16 @@ if [ "$RELEASE_MODE" = true ]; then
# Create release directory
mkdir -p release
# Copy binary and static files to release directory
# Copy only the binary to release directory
cp target/release/simplelink release/
cp -r static release/
cp .env.example release/.env
# Create a tar archive
tar -czf release.tar.gz release/
echo "Release archive created: release.tar.gz"
elif [ "$BINARY_MODE" = true ]; then
cargo build --release
else
cargo build
fi

View file

@ -26,11 +26,12 @@ services:
- API_URL=${API_URL:-http://localhost:3000}
container_name: shortener-app
ports:
- "3000:3000"
- "8080:8080"
environment:
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
- SERVER_HOST=0.0.0.0
- SERVER_PORT=3000
- SERVER_PORT=8080
- JWT_SECRET=change-me-in-production
depends_on:
db:
condition: service_healthy

View file

@ -9,7 +9,6 @@
"version": "0.0.0",
"dependencies": {
"@emotion/react": "^11.14.0",
"@headlessui/react": "^2.2.0",
"@hookform/resolvers": "^3.10.0",
"@icons-pack/react-simple-icons": "^11.2.0",
"@mantine/core": "^7.16.1",
@ -29,9 +28,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"react-simple-icons": "^1.0.0-beta.5",
"recharts": "^2.15.0",
"simple-icons": "^14.4.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
@ -595,25 +592,6 @@
"version": "0.2.9",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.10.0",
"license": "MIT",
@ -722,10 +700,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jxnblk/simple-icons": {
"version": "1.0.0",
"license": "MIT"
},
"node_modules/@mantine/core": {
"version": "7.16.1",
"license": "MIT",
@ -1395,92 +1369,6 @@
"version": "1.1.0",
"license": "MIT"
},
"node_modules/@react-aria/focus": {
"version": "3.19.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz",
"integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.23.0",
"@react-aria/utils": "^3.27.0",
"@react-types/shared": "^3.27.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz",
"integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.7",
"@react-aria/utils": "^3.27.0",
"@react-types/shared": "^3.27.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz",
"integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.7",
"@react-stately/utils": "^3.10.5",
"@react-types/shared": "^3.27.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.5",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz",
"integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.32.0",
"cpu": [
@ -1492,15 +1380,6 @@
"darwin"
]
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.0.0",
"license": "MIT",
@ -1570,33 +1449,6 @@
"vite": "^5.2.0 || ^6"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
"integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.11.3"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
"integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"dev": true,
@ -3488,13 +3340,6 @@
}
}
},
"node_modules/react-simple-icons": {
"version": "1.0.0-beta.5",
"license": "MIT",
"dependencies": {
"@jxnblk/simple-icons": "^1.0.0"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"license": "MIT",
@ -3715,17 +3560,6 @@
"node": ">=8"
}
},
"node_modules/simple-icons": {
"version": "14.4.0",
"license": "CC0-1.0",
"engines": {
"node": ">=0.12.18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/simple-icons"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"license": "BSD-3-Clause",

BIN
readme_img/mainview.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
readme_img/statview.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -1,12 +1,28 @@
use actix_cors::Cors;
use actix_files as fs;
use actix_web::{web, App, HttpServer};
use actix_web::{web, App, HttpResponse, HttpServer};
use anyhow::Result;
use rust_embed::RustEmbed;
use simplelink::check_and_generate_admin_token;
use simplelink::{handlers, AppState};
use sqlx::postgres::PgPoolOptions;
use tracing::info;
#[derive(RustEmbed)]
#[folder = "static/"]
struct Asset;
async fn serve_static_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
HttpResponse::Ok()
.content_type(mime.as_ref())
.body(content.data.into_owned())
}
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
#[actix_web::main]
async fn main() -> Result<()> {
// Load environment variables from .env file
@ -68,7 +84,11 @@ async fn main() -> Result<()> {
.route("/health", web::get().to(handlers::health_check)),
)
.service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url)))
.service(fs::Files::new("/", "./static").index_file("index.html"))
.default_service(web::route().to(|req: actix_web::HttpRequest| async move {
let path = req.path().trim_start_matches('/');
let path = if path.is_empty() { "index.html" } else { path };
serve_static_file(path).await
}))
})
.workers(2)
.backlog(10_000)