This commit is contained in:
Anaïs Saraiva
2026-08-15 00:35:22 +02:00
parent 954384d1e4
commit d8b9fbe993
8 changed files with 578 additions and 388 deletions
+16 -16
View File
@@ -10,7 +10,7 @@ database, no object storage, no queue, and no automatic cleanup.
## Features
- Drag & drop / file-picker uploads with progress, straight from the browser.
- **Optional per-file password**: protect a download link with a password.
- **Required per-file password**: every download is protected by a password.
- Permanent storage on the local filesystem; files survive restarts.
- A cryptographically secure, unguessable download key for every file.
- Streaming uploads and downloads — no full buffering in RAM, large files work.
@@ -68,8 +68,9 @@ Then open http://localhost:3000.
2. Drop `example.zip` (or click to pick a file).
3. The server stores it permanently and returns a URL like
`https://files.example.com/d/8fJ...random-secret...`.
4. Copy the URL. Anyone possessing it can download the file; nobody without the
secret can. If a password was set, the downloader must also enter it.
4. Copy the URL. Anyone possessing it can download the file — **after entering
the password you set at upload**. Nobody without the secret URL and the
password can download it.
5. The file stays available indefinitely. It is removed only by explicit
deletion.
@@ -115,11 +116,11 @@ touched.
### `POST /api/upload`
Multipart/form-data with a `file` field (this is what the web UI uses). An
optional `password` field protects the download with a password.
Multipart/form-data with a `file` field and a **required** `password` field
(this is what the web UI uses). Uploads without a password are rejected with
`400`.
```bash
curl -F "[email protected]" http://localhost:3000/api/upload
curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/upload
```
@@ -130,23 +131,22 @@ curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/uploa
"url": "/d/YOWJyhDXxhe3-L8MpEFBbAN_e93AqTrEMYutkmStScq5VfbQlrqUt9yHkqmeRp7OQ",
"originalName": "example.zip",
"size": 123456,
"protected": false
"protected": true
}
```
### `GET /d/:key` (also `HEAD`)
Streams the file. Requires the full secret key; any wrong or non-existent key
returns a generic `404`. If the file has a password, the server first shows a
password prompt (see below).
Every file is password-protected. Without a valid auth cookie the server shows a
password prompt; a wrong or non-existent key returns a generic `404`.
```bash
curl -O http://localhost:3000/d/YOWJyh...7OQ
curl -O http://localhost:3000/d/YOWJyh...7OQ # prompts for password first
```
### Password protection
For files uploaded with a `password` field:
Every file requires its password:
1. `GET /d/:key` returns a small HTML password form.
2. The browser submits it to `POST /api/auth/:key` with `password=...`.
@@ -159,7 +159,7 @@ curl -b cookies.txt -O http://localhost:3000/d/:key
```
The cookie is signed with a server secret (`storage/.secret`) so it cannot be
forged, and it survives server restarts. Without it, a protected file is never
forged, and it survives server restarts. Without it, a file is never
downloadable — not even by the uploader.
### `DELETE /api/files/:id`
@@ -191,9 +191,9 @@ paths or stack traces.
or filenames are ever used as secrets.
- **At-rest.** Only a SHA-256 hash of the key is persisted. Comparisons use
constant-time `timingSafeEqual`.
- **Passwords.** An optional per-file password is stored only as an **scrypt**
hash (salted), never in plaintext. Successful entry mints a short-lived
HMAC-signed cookie, so access is granted by the server and cannot be forged.
- **Passwords.** Every file has a password stored only as an **scrypt** hash
(salted), never in plaintext. Successful entry mints a short-lived HMAC-signed
cookie, so access is granted by the server and cannot be forged.
- **Path safety.** Filesystem paths are built only from server-generated,
regex-validated ids (`^[A-Za-z0-9_-]{22}$`). The original filename is metadata
only and is sanitized (basename, control/null bytes stripped) before being