4.7 KiB
4.7 KiB
One-Time File Share
A small self-hosted file-sharing app. You upload a file, get a one-time-use download link, hand it to a friend — after one successful download (or once it expires) the link is dead. Files persist in your account until you delete them, so you can mint a fresh link to re-share.
Built to run on a headless Fedora box behind nginx at the sub-path /fileShare, alongside other apps.
Features
- One-time download links, reserved atomically so exactly one download succeeds (race-safe).
- Time-based expiry on top of one-time use (
LINK_TTL_HOURS). - Accounts with email verification (SMTP); unverified users can't upload.
- Per-user dashboard: see each file's scan status, link status, expiry, copy the URL, mint a new link, or delete.
- Multi-file uploads are bundled into a single
.zipbehind one link. - Antivirus scan on every upload via ClamAV (
clamd); infected files are rejected and never get a link. - Lazy background compression (gzip) to save disk; transparently decompressed on download.
- Disk-space monitor that refuses uploads when free space is low.
- Admin dashboard to manage all users, files and links.
- Whole site is
noindex/ unlisted.
Requirements
- Node.js ≥ 20
- MySQL (existing server is fine)
- ClamAV
clamdlistening on TCP (optional in dev — setDISABLE_AV=1) - An SMTP account for verification emails (optional in dev — set
DISABLE_EMAIL_VERIFICATION=1)
Setup
- Database — create the schema yourself (no auto-migration):
mysql -u <user> -p <dbname> < schema.sql - Install deps:
npm install - Configure:
cp .env.example .env # set SESSION_SECRET, DB_*, ADMIN_EMAIL, PUBLIC_BASE_URL, SMTP_*, CLAMD_* node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # for SESSION_SECRET - Run:
The app listens onnpm start127.0.0.1:$PORTand mounts at$BASE_PATH(e.g.http://127.0.0.1:8077/fileShare).
Local development (DB over SSH tunnel)
Tunnel the server's MySQL to your laptop, then point .env at the tunnel:
ssh -L 3307:127.0.0.1:3306 user@server
DB_HOST=127.0.0.1
DB_PORT=3307
DISABLE_AV=1 # if you don't run clamd locally
DISABLE_EMAIL_VERIFICATION=1 # auto-verifies new accounts; logs verify links
PUBLIC_BASE_URL=http://127.0.0.1:8077/fileShare
TRUST_PROXY=0
Production (nginx + systemd)
- Reverse proxy: see
deploy/nginx-fileShare.conf. The app keeps the/fileShareprefix, soproxy_passhas no trailing slash. Raiseclient_max_body_sizeto matchMAX_UPLOAD_BYTES. - Service: see
deploy/fileshare.service. SetPUBLIC_BASE_URL=https://your.domain/fileShareandTRUST_PROXY=1. - The first account whose email equals
ADMIN_EMAILis auto-promoted to admin on startup.
How it behaves (worth knowing)
- Aborted downloads still burn the link. The token is reserved the moment a valid download starts, which is the safe reading of "one-time use." If a transfer fails, mint a new link from the dashboard.
- Infected uploads are deleted immediately and never stored or linked. If
clamdis unreachable,AV_FAIL_MODE=block(default) rejects the upload;skipaccepts it and marks the scanskipped. - Compression is lazy: the upload returns instantly and a background worker gzips the file shortly after (skipping already-compressed types in
COMPRESS_SKIP_EXTS). Downloads work whether or not compression has finished.
Key env vars
| Var | Purpose |
|---|---|
BASE_PATH |
Sub-path mount (e.g. /fileShare) |
PUBLIC_BASE_URL |
Absolute URL used to build copyable links |
LINK_TTL_HOURS |
Lifetime of a freshly minted link |
MIN_FREE_BYTES |
Uploads refused below this free space |
MAX_UPLOAD_BYTES |
Per-file cap |
ADMIN_EMAIL |
Account promoted to admin on startup |
AV_FAIL_MODE |
block or skip when clamd is down |
DISABLE_AV / DISABLE_EMAIL_VERIFICATION |
Dev toggles |
Project layout
src/
server.js app wiring, session, mounting, workers
config.js env parsing
db.js mysql2 pool
auth.js sessions, register/login/logout, guards, CSRF check
mailer.js SMTP verification emails
links.js link minting + per-file link state
storage.js on-disk path helpers
log.js file + stdout logger / audit trail
routes/ pages, upload, download, files, admin, verify
services/ disk, antivirus, bundle (zip), compression (gzip)
views.js server-rendered HTML
public/style.css
schema.sql run this against MySQL
deploy/ nginx + systemd samples