+1
.env.example
+1
.env.example
+64
Cargo.lock
+64
Cargo.lock
···
1858
1858
]
1859
1859
1860
1860
[[package]]
1861
+
name = "rust-embed"
1862
+
version = "6.8.1"
1863
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1864
+
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
1865
+
dependencies = [
1866
+
"rust-embed-impl",
1867
+
"rust-embed-utils",
1868
+
"walkdir",
1869
+
]
1870
+
1871
+
[[package]]
1872
+
name = "rust-embed-impl"
1873
+
version = "6.8.1"
1874
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1875
+
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
1876
+
dependencies = [
1877
+
"proc-macro2",
1878
+
"quote",
1879
+
"rust-embed-utils",
1880
+
"syn",
1881
+
"walkdir",
1882
+
]
1883
+
1884
+
[[package]]
1885
+
name = "rust-embed-utils"
1886
+
version = "7.8.1"
1887
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1888
+
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
1889
+
dependencies = [
1890
+
"sha2",
1891
+
"walkdir",
1892
+
]
1893
+
1894
+
[[package]]
1861
1895
name = "rustc-demangle"
1862
1896
version = "0.1.24"
1863
1897
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1896
1930
version = "1.0.18"
1897
1931
source = "registry+https://github.com/rust-lang/crates.io-index"
1898
1932
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
1933
+
1934
+
[[package]]
1935
+
name = "same-file"
1936
+
version = "1.0.6"
1937
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1938
+
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1939
+
dependencies = [
1940
+
"winapi-util",
1941
+
]
1899
1942
1900
1943
[[package]]
1901
1944
name = "schannel"
···
2068
2111
"dotenv",
2069
2112
"jsonwebtoken",
2070
2113
"lazy_static",
2114
+
"mime_guess",
2071
2115
"rand",
2072
2116
"regex",
2117
+
"rust-embed",
2073
2118
"serde",
2074
2119
"serde_json",
2075
2120
"sqlx",
···
2737
2782
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2738
2783
2739
2784
[[package]]
2785
+
name = "walkdir"
2786
+
version = "2.5.0"
2787
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2788
+
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
2789
+
dependencies = [
2790
+
"same-file",
2791
+
"winapi-util",
2792
+
]
2793
+
2794
+
[[package]]
2740
2795
name = "wasi"
2741
2796
version = "0.11.0+wasi-snapshot-preview1"
2742
2797
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2831
2886
version = "0.4.0"
2832
2887
source = "registry+https://github.com/rust-lang/crates.io-index"
2833
2888
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
2889
+
2890
+
[[package]]
2891
+
name = "winapi-util"
2892
+
version = "0.1.9"
2893
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2894
+
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
2895
+
dependencies = [
2896
+
"windows-sys 0.59.0",
2897
+
]
2834
2898
2835
2899
[[package]]
2836
2900
name = "winapi-x86_64-pc-windows-gnu"
+3
-1
Cargo.toml
+3
-1
Cargo.toml
···
8
8
path = "src/lib.rs"
9
9
10
10
[dependencies]
11
+
rust-embed = "6.8"
11
12
jsonwebtoken = "9"
12
13
actix-web = "4.4"
13
14
actix-files = "0.6"
···
28
29
regex = "1.10"
29
30
lazy_static = "1.4"
30
31
argon2 = "0.5.3"
31
-
rand = { version = "0.8", features = ["std"] }
32
+
rand = { version = "0.8", features = ["std"] }
33
+
mime_guess = "2.0.5"
+38
README.md
+38
README.md
···
1
+
# SimpleLink
2
+
A very performant and light (6mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres.
3
+
4
+

5
+
6
+

7
+
8
+
## Build
9
+
10
+
### From Source
11
+
First configure .env.example and save it to .env
12
+
13
+
The project will not run withot DATABASE_URL set. (TODO add sqlite support)
14
+
15
+
```bash
16
+
#set api-domain to where you will be deploying the link shortener, eg: link.example.com, default is localhost:8080
17
+
git clone https://github.com/waveringana/simplelink && cd simplelink
18
+
./build.sh api-domain=localhost:8080
19
+
cargo run
20
+
```
21
+
22
+
Alternatively if you want a binary form
23
+
```bash
24
+
./build.sh --binary
25
+
```
26
+
then check /target/release for the binary named `SimpleGit`
27
+
28
+
### From Docker
29
+
```bash
30
+
docker build --build-arg API_URL=http://localhost:8080 -t simplelink .
31
+
docker run simplelink -p 8080:8080 \
32
+
-e JWT_SECRET=change-me-in-production \
33
+
-e DATABASE_URL=postgres://user:password@host:port/database \
34
+
simplelink
35
+
```
36
+
37
+
### From Docker Compose
38
+
Adjust the included docker-compose.yml to your liking, it includes a postgres config as well.
+9
-7
build.sh
+9
-7
build.sh
···
3
3
# Default values
4
4
API_URL="http://localhost:8080"
5
5
RELEASE_MODE=false
6
+
BINARY_MODE=false
6
7
7
8
# Parse command line arguments
8
9
for arg in "$@"
···
14
15
;;
15
16
--release)
16
17
RELEASE_MODE=true
18
+
shift
19
+
;;
20
+
--binary)
21
+
BINARY_MODE=true
17
22
shift
18
23
;;
19
24
esac
···
45
50
npm run build
46
51
cd ..
47
52
48
-
# Create static directory if it doesn't exist
53
+
# Create static directory and copy frontend build
49
54
mkdir -p static
50
-
51
-
# Clean existing static files
52
55
rm -rf static/*
53
-
54
-
# Copy built files to static directory
55
56
cp -r frontend/dist/* static/
56
57
57
58
# Build Rust project
···
62
63
# Create release directory
63
64
mkdir -p release
64
65
65
-
# Copy binary and static files to release directory
66
+
# Copy only the binary to release directory
66
67
cp target/release/simplelink release/
67
-
cp -r static release/
68
68
cp .env.example release/.env
69
69
70
70
# Create a tar archive
71
71
tar -czf release.tar.gz release/
72
72
73
73
echo "Release archive created: release.tar.gz"
74
+
elif [ "$BINARY_MODE" = true ]; then
75
+
cargo build --release
74
76
else
75
77
cargo build
76
78
fi
+3
-2
docker-compose.yml
+3
-2
docker-compose.yml
···
26
26
- API_URL=${API_URL:-http://localhost:3000}
27
27
container_name: shortener-app
28
28
ports:
29
-
- "3000:3000"
29
+
- "8080:8080"
30
30
environment:
31
31
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
32
32
- SERVER_HOST=0.0.0.0
33
-
- SERVER_PORT=3000
33
+
- SERVER_PORT=8080
34
+
- JWT_SECRET=change-me-in-production
34
35
depends_on:
35
36
db:
36
37
condition: service_healthy
-166
frontend/package-lock.json
-166
frontend/package-lock.json
···
9
9
"version": "0.0.0",
10
10
"dependencies": {
11
11
"@emotion/react": "^11.14.0",
12
-
"@headlessui/react": "^2.2.0",
13
12
"@hookform/resolvers": "^3.10.0",
14
13
"@icons-pack/react-simple-icons": "^11.2.0",
15
14
"@mantine/core": "^7.16.1",
···
29
28
"react": "^18.3.1",
30
29
"react-dom": "^18.3.1",
31
30
"react-hook-form": "^7.54.2",
32
-
"react-simple-icons": "^1.0.0-beta.5",
33
31
"recharts": "^2.15.0",
34
-
"simple-icons": "^14.4.0",
35
32
"tailwind-merge": "^2.6.0",
36
33
"tailwindcss-animate": "^1.0.7",
37
34
"zod": "^3.24.1"
···
595
592
"version": "0.2.9",
596
593
"license": "MIT"
597
594
},
598
-
"node_modules/@headlessui/react": {
599
-
"version": "2.2.0",
600
-
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
601
-
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
602
-
"license": "MIT",
603
-
"dependencies": {
604
-
"@floating-ui/react": "^0.26.16",
605
-
"@react-aria/focus": "^3.17.1",
606
-
"@react-aria/interactions": "^3.21.3",
607
-
"@tanstack/react-virtual": "^3.8.1"
608
-
},
609
-
"engines": {
610
-
"node": ">=10"
611
-
},
612
-
"peerDependencies": {
613
-
"react": "^18 || ^19 || ^19.0.0-rc",
614
-
"react-dom": "^18 || ^19 || ^19.0.0-rc"
615
-
}
616
-
},
617
595
"node_modules/@hookform/resolvers": {
618
596
"version": "3.10.0",
619
597
"license": "MIT",
···
721
699
"@jridgewell/resolve-uri": "^3.1.0",
722
700
"@jridgewell/sourcemap-codec": "^1.4.14"
723
701
}
724
-
},
725
-
"node_modules/@jxnblk/simple-icons": {
726
-
"version": "1.0.0",
727
-
"license": "MIT"
728
702
},
729
703
"node_modules/@mantine/core": {
730
704
"version": "7.16.1",
···
1395
1369
"version": "1.1.0",
1396
1370
"license": "MIT"
1397
1371
},
1398
-
"node_modules/@react-aria/focus": {
1399
-
"version": "3.19.1",
1400
-
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz",
1401
-
"integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==",
1402
-
"license": "Apache-2.0",
1403
-
"dependencies": {
1404
-
"@react-aria/interactions": "^3.23.0",
1405
-
"@react-aria/utils": "^3.27.0",
1406
-
"@react-types/shared": "^3.27.0",
1407
-
"@swc/helpers": "^0.5.0",
1408
-
"clsx": "^2.0.0"
1409
-
},
1410
-
"peerDependencies": {
1411
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
1412
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1413
-
}
1414
-
},
1415
-
"node_modules/@react-aria/interactions": {
1416
-
"version": "3.23.0",
1417
-
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz",
1418
-
"integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==",
1419
-
"license": "Apache-2.0",
1420
-
"dependencies": {
1421
-
"@react-aria/ssr": "^3.9.7",
1422
-
"@react-aria/utils": "^3.27.0",
1423
-
"@react-types/shared": "^3.27.0",
1424
-
"@swc/helpers": "^0.5.0"
1425
-
},
1426
-
"peerDependencies": {
1427
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
1428
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1429
-
}
1430
-
},
1431
-
"node_modules/@react-aria/ssr": {
1432
-
"version": "3.9.7",
1433
-
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
1434
-
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
1435
-
"license": "Apache-2.0",
1436
-
"dependencies": {
1437
-
"@swc/helpers": "^0.5.0"
1438
-
},
1439
-
"engines": {
1440
-
"node": ">= 12"
1441
-
},
1442
-
"peerDependencies": {
1443
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1444
-
}
1445
-
},
1446
-
"node_modules/@react-aria/utils": {
1447
-
"version": "3.27.0",
1448
-
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz",
1449
-
"integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==",
1450
-
"license": "Apache-2.0",
1451
-
"dependencies": {
1452
-
"@react-aria/ssr": "^3.9.7",
1453
-
"@react-stately/utils": "^3.10.5",
1454
-
"@react-types/shared": "^3.27.0",
1455
-
"@swc/helpers": "^0.5.0",
1456
-
"clsx": "^2.0.0"
1457
-
},
1458
-
"peerDependencies": {
1459
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
1460
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1461
-
}
1462
-
},
1463
-
"node_modules/@react-stately/utils": {
1464
-
"version": "3.10.5",
1465
-
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
1466
-
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
1467
-
"license": "Apache-2.0",
1468
-
"dependencies": {
1469
-
"@swc/helpers": "^0.5.0"
1470
-
},
1471
-
"peerDependencies": {
1472
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1473
-
}
1474
-
},
1475
-
"node_modules/@react-types/shared": {
1476
-
"version": "3.27.0",
1477
-
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz",
1478
-
"integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==",
1479
-
"license": "Apache-2.0",
1480
-
"peerDependencies": {
1481
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1482
-
}
1483
-
},
1484
1372
"node_modules/@rollup/rollup-darwin-arm64": {
1485
1373
"version": "4.32.0",
1486
1374
"cpu": [
···
1491
1379
"os": [
1492
1380
"darwin"
1493
1381
]
1494
-
},
1495
-
"node_modules/@swc/helpers": {
1496
-
"version": "0.5.15",
1497
-
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
1498
-
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
1499
-
"license": "Apache-2.0",
1500
-
"dependencies": {
1501
-
"tslib": "^2.8.0"
1502
-
}
1503
1382
},
1504
1383
"node_modules/@tailwindcss/node": {
1505
1384
"version": "4.0.0",
···
1568
1447
},
1569
1448
"peerDependencies": {
1570
1449
"vite": "^5.2.0 || ^6"
1571
-
}
1572
-
},
1573
-
"node_modules/@tanstack/react-virtual": {
1574
-
"version": "3.11.3",
1575
-
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
1576
-
"integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==",
1577
-
"license": "MIT",
1578
-
"dependencies": {
1579
-
"@tanstack/virtual-core": "3.11.3"
1580
-
},
1581
-
"funding": {
1582
-
"type": "github",
1583
-
"url": "https://github.com/sponsors/tannerlinsley"
1584
-
},
1585
-
"peerDependencies": {
1586
-
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1587
-
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1588
-
}
1589
-
},
1590
-
"node_modules/@tanstack/virtual-core": {
1591
-
"version": "3.11.3",
1592
-
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
1593
-
"integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==",
1594
-
"license": "MIT",
1595
-
"funding": {
1596
-
"type": "github",
1597
-
"url": "https://github.com/sponsors/tannerlinsley"
1598
1450
}
1599
1451
},
1600
1452
"node_modules/@types/babel__core": {
···
3488
3340
}
3489
3341
}
3490
3342
},
3491
-
"node_modules/react-simple-icons": {
3492
-
"version": "1.0.0-beta.5",
3493
-
"license": "MIT",
3494
-
"dependencies": {
3495
-
"@jxnblk/simple-icons": "^1.0.0"
3496
-
}
3497
-
},
3498
3343
"node_modules/react-smooth": {
3499
3344
"version": "4.0.4",
3500
3345
"license": "MIT",
···
3713
3558
"license": "MIT",
3714
3559
"engines": {
3715
3560
"node": ">=8"
3716
-
}
3717
-
},
3718
-
"node_modules/simple-icons": {
3719
-
"version": "14.4.0",
3720
-
"license": "CC0-1.0",
3721
-
"engines": {
3722
-
"node": ">=0.12.18"
3723
-
},
3724
-
"funding": {
3725
-
"type": "opencollective",
3726
-
"url": "https://opencollective.com/simple-icons"
3727
3561
}
3728
3562
},
3729
3563
"node_modules/source-map": {
readme_img/mainview.jpg
readme_img/mainview.jpg
This is a binary file and will not be displayed.
readme_img/statview.jpg
readme_img/statview.jpg
This is a binary file and will not be displayed.
+23
-3
src/main.rs
+23
-3
src/main.rs
···
1
1
use actix_cors::Cors;
2
-
use actix_files as fs;
3
-
use actix_web::{web, App, HttpServer};
2
+
use actix_web::{web, App, HttpResponse, HttpServer};
4
3
use anyhow::Result;
4
+
use rust_embed::RustEmbed;
5
5
use simplelink::check_and_generate_admin_token;
6
6
use simplelink::{handlers, AppState};
7
7
use sqlx::postgres::PgPoolOptions;
8
8
use tracing::info;
9
+
10
+
#[derive(RustEmbed)]
11
+
#[folder = "static/"]
12
+
struct Asset;
13
+
14
+
async fn serve_static_file(path: &str) -> HttpResponse {
15
+
match Asset::get(path) {
16
+
Some(content) => {
17
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
18
+
HttpResponse::Ok()
19
+
.content_type(mime.as_ref())
20
+
.body(content.data.into_owned())
21
+
}
22
+
None => HttpResponse::NotFound().body("404 Not Found"),
23
+
}
24
+
}
9
25
10
26
#[actix_web::main]
11
27
async fn main() -> Result<()> {
···
68
84
.route("/health", web::get().to(handlers::health_check)),
69
85
)
70
86
.service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url)))
71
-
.service(fs::Files::new("/", "./static").index_file("index.html"))
87
+
.default_service(web::route().to(|req: actix_web::HttpRequest| async move {
88
+
let path = req.path().trim_start_matches('/');
89
+
let path = if path.is_empty() { "index.html" } else { path };
90
+
serve_static_file(path).await
91
+
}))
72
92
})
73
93
.workers(2)
74
94
.backlog(10_000)