- TypeScript 42.8%
- Rust 34.7%
- CSS 19.1%
- Dockerfile 1.3%
- HTML 1.2%
- Other 0.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo | ||
| deploy | ||
| frontend | ||
| migrations | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| compose.yml | ||
| Dockerfile | ||
| README.md | ||
CDN
Rust file-hosting API (Axum + SQLite) with a React SPA. Anyone can browse and download; uploading and deleting require a logged-in approved account. Admins approve registrations and manage users.
🎯 Purpose
A small self-hosted CDN / file locker: store blobs on disk, metadata in SQLite, and serve them over short shareable IDs. One process can run the API and the built frontend.
🚀 Quick start
From the repo root:
cp .env.example .env
# set ADMIN_PASSWORD (required, min 8 characters)
cargo run
In a second terminal, for hot-reload UI:
cd frontend
npm install
npm run dev
| Surface | URL |
|---|---|
| API (+ production SPA if built) | http://127.0.0.1:8080 |
| Vite UI (dev) | http://127.0.0.1:5173 |
Vite proxies /api, /health, and short file IDs to the API. On first boot, if no approved admin exists, the server seeds one from ADMIN_USERNAME / ADMIN_PASSWORD.
Production-style (single process)
cd frontend && npm install && npm run build
cd .. && cargo run
When STATIC_DIR (default ./frontend/dist) contains index.html, Axum serves the SPA alongside the API.
📁 Layout
cdn/
├── src/ # Rust API binary
├── frontend/ # React + Vite SPA
├── migrations/ # SQLite schema (sqlx migrate)
├── deploy/ # systemd, Caddy, install helper
├── .forgejo/workflows/ # CI
├── compose.yml # Docker Compose
├── Dockerfile
├── .env.example
└── data/ # runtime (gitignored): cdn.db + files/
🏗️ Architecture
flowchart LR
Client[Browser / curl]
Axum[Axum process]
SPA[frontend/dist]
DB[(SQLite cdn.db)]
Disk[data/files]
Client --> Axum
Axum --> SPA
Axum --> DB
Axum --> Disk
- API — Axum routes under
/api/*, public downloads at/{id}, liveness at/health. - Auth — HTTP-only
cdn_sessioncookie; Argon2 password hashes; sessions in SQLite. - Storage — file bytes named by ID under
{DATA_DIR}/files/; metadata infilestable. - SPA — optional static overlay; without a build, only the API starts (with a warning log).
💡 Auth model
| Who | Can do |
|---|---|
| Public | List, view, download, copy share links |
| Logged-in approved user | Upload, delete any file |
| Admin | Manage accounts (create / approve / reject / delete) |
Self-registration creates a pending user until an admin approves. Pending and rejected accounts cannot log in.
🛠️ API
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
no | Liveness ({"status":"ok"}) |
GET |
/api/files |
no | List files (q, type) |
GET |
/{id} |
no | Download / inline view |
POST |
/api/auth/register |
no | Request account (pending) |
POST |
/api/auth/login |
no | Login (sets cdn_session) |
POST |
/api/auth/logout |
session | Logout |
GET |
/api/auth/me |
session | Current user |
POST |
/api/upload |
session | Multipart field file |
DELETE |
/api/files/{id} |
session | Delete file |
GET |
/api/admin/users |
admin | List users |
POST |
/api/admin/users |
admin | Create approved user |
POST |
/api/admin/users/{id}/approve |
admin | Approve pending |
POST |
/api/admin/users/{id}/reject |
admin | Reject pending |
DELETE |
/api/admin/users/{id} |
admin | Delete user |
GET /api/files query params:
q— case-insensitive substring match on original name or author usernametype—all(default),image,video,audio,document,other
Upload IDs are 10-character nanoids. Download path IDs must be 8–21 chars of [A-Za-z0-9_-].
Examples
# public list
curl -sS "http://127.0.0.1:8080/api/files"
# login (stores cdn_session cookie)
curl -sS -c cookies.txt -X POST http://127.0.0.1:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"your-password"}'
# upload
curl -sS -b cookies.txt -X POST http://127.0.0.1:8080/api/upload \
-F "file=@./photo.jpg"
⚙️ Configuration
Copy .env.example. Important variables:
| Variable | Default | Notes |
|---|---|---|
BIND |
0.0.0.0:8080 |
Listen address |
ADMIN_USERNAME |
admin |
Seeded only if no approved admin exists |
ADMIN_PASSWORD |
(required) | Min 8 chars |
DATA_DIR |
./data |
SQLite + files/ blobs |
DATABASE_URL |
sqlite:{DATA_DIR}/cdn.db?mode=rwc |
Optional override |
MAX_UPLOAD_BYTES |
52428800 (50 MiB) |
Enforced on upload |
PUBLIC_BASE_URL |
unset | Absolute URLs in list/upload responses |
STATIC_DIR |
./frontend/dist |
SPA root when index.html exists |
COOKIE_SECURE |
auto from HTTPS PUBLIC_BASE_URL |
Session cookie Secure flag |
SESSION_DAYS |
30 |
Session lifetime |
CORS_ORIGIN |
* |
Exact origin + credentials when not * |
CDN_PORT |
8080 |
Host port for Compose only |
RUST_LOG |
info |
Tracing filter |
📦 Dependencies
- Rust toolchain (edition 2024) —
cargo run/cargo build --release - Node.js 22+ recommended — frontend build and Vite
- SQLite via sqlx (embedded; no separate DB server)
- Optional: Docker / Compose, Caddy, systemd (see
deploy/)
🚀 Deployment
Docker Compose
cp .env.example .env
# set ADMIN_PASSWORD; set PUBLIC_BASE_URL=https://your.domain in production
docker compose up -d --build
Compose mounts volume cdn-data at /data and sets DATA_DIR / STATIC_DIR for the image.
Bare metal / systemd
See deploy/README.md (install-binary.sh, cdn.service, Caddyfile.example).
⚠️ Pitfalls
ADMIN_PASSWORDis required — the process exits on startup without it.- Admin seed is one-shot — changing
ADMIN_PASSWORDlater does not update an existing admin; use the DB or create another admin via the API. - Any approved user can delete any file — delete is not owner-scoped.
- CORS
*and cookies — wildcard CORS does not send credentials; use Vite’s proxy (same-origin) or set an exactCORS_ORIGINwith credentials. - Share links — without
PUBLIC_BASE_URL, API returns relative URLs (/{id}); set the public origin behind a reverse proxy. - Reverse proxy body size — raise proxy limits to match
MAX_UPLOAD_BYTES(see Caddy example).
🔗 Related
src/README.md— backend modulessrc/routes/README.md— HTTP handlersfrontend/README.md— SPAmigrations/README.md— schemadeploy/README.md— production install.forgejo/README.md— CI