- Rust 64.9%
- JavaScript 28.4%
- Dockerfile 5.6%
- HTML 1.1%
| single-use-file-share-api | ||
| single-use-file-share-webapp | ||
| .dockerignore | ||
| .gitignore | ||
| DOCKER_TODO.md | ||
| Dockerfile | ||
| Jenkinsfile | ||
| LICENSE | ||
| README.md | ||
single-use-file-share
Share a file with a link that stops working after a while. Upload a file, get a link, send it to someone — the file is deleted automatically when it expires, so nothing hangs around to be collected.
- Backend: Rust (axum + tokio). Streams uploads/downloads in chunks (100 GiB cap), guards disk space, and runs a background janitor that deletes expired files. In production it also serves the built frontend, so the whole app is one process on one port.
- Frontend: React (Vite + Tailwind). Drop a file, copy the link; the download page shows the file's real name, size, and time left.
- Storage: file bytes on disk (
UPLOAD_DIR), metadata in memory. A restart wipes the metadata (links die; orphaned blobs remain until you clear the volume) — moving metadata to SQLite is the natural next step.
Run with Docker (single image)
docker build -t single-use-file-share .
docker run --rm -p 3000:3000 -v sufs_uploads:/data single-use-file-share
Open http://localhost:3000. Uploaded bytes persist in the sufs_uploads
volume.
Mapping the data path to an external volume
Inside the container the app always writes uploads to /data (the image sets
UPLOAD_DIR=/data), so that one mount point is all you need to map. Either
hand it a Docker-managed named volume, or bind-mount any host folder — e.g. a
big external drive:
# Docker-managed named volume (shown above)
docker run --rm -p 3000:3000 -v sufs_uploads:/data single-use-file-share
# Bind mount a host folder (Linux/macOS)
docker run --rm -p 3000:3000 -v /mnt/bigdisk/file-share:/data single-use-file-share
# Bind mount a host folder (Windows, PowerShell)
docker run --rm -p 3000:3000 -v D:\file-share-data:/data single-use-file-share
With the 100 GiB upload cap, make sure whatever you map has room: the server rejects uploads (HTTP 507) rather than let free space on the upload volume drop below 1 GiB. Note that named volumes live inside Docker's own storage (on Docker Desktop, the Linux VM's virtual disk), so for uploads this large a bind mount to a roomy drive is usually the better fit.
Local development
Two terminals (requires Rust and Node):
# 1 — API on :3000
cd single-use-file-share-api && cargo run
# 2 — frontend on :5173 with hot reload; /api/* is proxied to :3000
cd single-use-file-share-webapp && npm install && npm run dev
Open http://localhost:5173.
To test the production setup (Rust serving the built frontend on one port):
cd single-use-file-share-webapp && npm run build
cd ../single-use-file-share-api
STATIC_DIR=../single-use-file-share-webapp/dist cargo run # → :3000
Configuration (env vars)
| Variable | Default | Meaning |
|---|---|---|
BIND_ADDR |
0.0.0.0:3000 |
Address/port the server listens on |
UPLOAD_DIR |
./uploads |
Where uploaded file bytes are written |
STATIC_DIR |
./static |
Built frontend the server hands out |
FILE_TTL_SECONDS |
86400 (24 h) |
How long a file lives before expiring |
CLEANUP_INTERVAL_SECONDS |
60 |
How often the janitor sweeps for expired files |
API
All endpoints live under /api; every other path serves the React app.
| Endpoint | Method | Purpose |
|---|---|---|
/api/upload |
POST | Multipart upload (file field) → { "id": "<uuid>" } |
/api/files/{id} |
GET | Metadata JSON (name, size, expiry); 404 if unknown/expired |
/api/download/{id} |
GET | The file itself, as an attachment; 404 if unknown/expired |
/api/health |
GET | Liveness check → { "status": "ok" } |