A temporary file hosting service
- Svelte 48%
- JavaScript 28.8%
- TypeScript 16.7%
- CSS 5.6%
- Dockerfile 0.4%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| client | ||
| server | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
Fuse
A small, self-hosted temporary file host. Drop files, get a link and a QR code, pick how long it lives. When the fuse runs out, the files are gone.
- No accounts. Every transfer gets a private uploader key instead. The uploader link is
the share link followed by
#KEY(mega.nz style): open it and you get a Delete button. The key never leaves the browser as part of the URL — it is sent as a header. - Retention presets: 1 hour, 6 hours, 1 day, 3 days, 1 week, 2 weeks, 1 month, or one-time download (each file burns after its first complete download, and the whole transfer still expires after 90 days at most).
- Robust transfers. Uploads are chunked (8 MiB) with a tus-style offset handshake: a failed chunk is rolled back server-side and retried with backoff, and the client re-syncs its offset from the server, so a flaky connection or a proxy hiccup never corrupts a file. Downloads support HTTP ranges (except one-time files, which are only marked used once the full response has been sent). "Download all" streams an uncompressed zip.
- Text mode. Paste text instead of files; recipients see it rendered inline with a copy button. Same lifetimes, and "one-time" burns it after the first view.
- End-to-end encryption (optional). Turn it on and pick a password (8+ characters, or hit Generate): files, their names and text are encrypted in the browser with AES-256-GCM (key via PBKDF2, 600k iterations, per-transfer salt). The server stores only ciphertext and an encrypted manifest; the share link reveals nothing without the password. Recipients decrypt in their browser, which is why encrypted transfers are capped at 200 MB and download file by file (no zip).
- Single SQLite file + one directory per transfer on disk. Files are stored under opaque ids,
always served as
application/octet-streamattachments.
Run
npm install
npm run dev # server on :3000 + Vite on :5180 (proxied)
npm run build && npm start
npm test # server API tests (node:test)
Docker
cp .env.example .env # optional
docker compose up -d --build
Data (SQLite + files) lives in the /data volume (./data by default).
Behind a reverse proxy
Set TRUST_PROXY=true (the compose file does) and make sure the proxy allows request bodies of
at least MAX_CHUNK_BYTES (64 MiB by default; the browser sends 8 MiB chunks). For nginx:
client_max_body_size 64m;
proxy_request_buffering off;
proxy_read_timeout 3600s;
Configuration
Everything is via environment variables — see .env.example. Notable ones:
| Variable | Default | Meaning |
|---|---|---|
MAX_TRANSFER_BYTES |
2 GiB | total size of one transfer |
MAX_FILES |
20 | files per transfer |
E2EE_MAX_BYTES |
200 MiB | total size of an end-to-end encrypted transfer |
TEXT_MAX_BYTES |
1 MiB | size of a text paste |
ONE_TIME_MAX_DAYS |
90 | hard expiry for one-time transfers |
STALE_UPLOAD_HOURS |
24 | abandoned (never finalized) uploads are dropped after this |
TRUST_PROXY |
false | trust X-Forwarded-* from a reverse proxy |
API
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/api/config |
— | limits + retention presets |
POST |
/api/transfers |
— | {duration, kind?, e2ee?, salt?} → {id, key, expiresAt, oneTime, kind, e2ee} |
POST |
/api/transfers/:id/files |
key | register a file {name,size,mime} |
PATCH |
/api/transfers/:id/files/:fid |
key | append a chunk (Upload-Offset header, raw body) |
GET |
/api/transfers/:id/files/:fid |
key | current offset (for resume) |
POST |
/api/transfers/:id/finalize |
key | publish the transfer ({manifest?} for encrypted ones) |
GET |
/api/transfers/:id |
optional key | transfer + files (owner: true with a valid key) |
DELETE |
/api/transfers/:id |
key | delete everything |
GET |
/d/:id/:fid/:name |
— | download one file (?k=KEY = owner, doesn't consume one-time files) |
GET |
/z/:id/:name.zip |
— | download all as zip (not for encrypted transfers) |
The key goes in the X-Fuse-Key header (or ?k= for download links).
Layout
server/ Express + better-sqlite3 (app.js routes, store.js data access, db.js schema)
client/ Svelte 5 + Vite; bespoke design tokens in src/app.css (light/dark)