fix
This commit is contained in:
@@ -2,19 +2,20 @@
|
||||
|
||||
A minimal, self-hosted private file hosting service built with **Bun** and
|
||||
**TypeScript**. Upload a file, get a secret, unguessable download link. Files are
|
||||
stored permanently on the local filesystem and never expire.
|
||||
stored on the local filesystem and expire after one week.
|
||||
|
||||
Everything lives on disk — the filesystem is the source of truth. There is no
|
||||
database, no object storage, no queue, and no automatic cleanup.
|
||||
database, no object storage, and no queue. Expired files are swept automatically.
|
||||
|
||||
## Features
|
||||
|
||||
- Drag & drop / file-picker uploads with progress, straight from the browser.
|
||||
- **Required per-file password**: every download is protected by a password.
|
||||
- Permanent storage on the local filesystem; files survive restarts.
|
||||
- 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.
|
||||
- Configurable maximum upload size (`413` when exceeded).
|
||||
- Configurable maximum upload size (`413` when exceeded, default 10GB).
|
||||
- Files **auto-expire after one week** and are removed automatically.
|
||||
- Authenticated deletion (possession of the file's secret key is required).
|
||||
- Rate-limited download/delete endpoints to blunt brute force.
|
||||
- Zero runtime dependencies (Bun is the only runtime).
|
||||
@@ -47,8 +48,9 @@ cp .env.example .env
|
||||
| `HOST` | `0.0.0.0` | Bind address. |
|
||||
| `PORT` | `3000` | Bind port. |
|
||||
| `STORAGE_DIR` | `./storage` | Directory where files + metadata are stored (relative to CWD). |
|
||||
| `MAX_FILE_SIZE` | `5GB` | Max upload size. Units: `B`, `KB`, `MB`, `GB`, `TB` (or plain int). |
|
||||
| `RATE_LIMIT_ENABLED` | `1` | Enable per-IP rate limiting on `/d/` and `/api/files/`. |
|
||||
| `MAX_FILE_SIZE` | `10GB` | Max upload size. Units: `B`, `KB`, `MB`, `GB`, `TB` (or plain int). |
|
||||
| `FILE_TTL_MS` | `604800000` | Milliseconds before an upload expires and is deleted (1 week). |
|
||||
| `RATE_LIMIT_ENABLED` | `1` | Enable per-IP rate limiting on `/(key)` and `/api/files/`. |
|
||||
| `RATE_LIMIT_MAX` | `300` | Max requests per IP per window. |
|
||||
| `RATE_LIMIT_WINDOW_MS` | `60000` | Rate-limit window length in ms. |
|
||||
| `TRUST_PROXY` | `0` | Set `1` only behind a proxy that sets `X-Forwarded-For`. |
|
||||
@@ -66,12 +68,12 @@ Then open http://localhost:3000.
|
||||
|
||||
1. Open the site.
|
||||
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...`.
|
||||
3. The server stores it and returns a URL like
|
||||
`https://files.example.com/8fJ...random-secret...`.
|
||||
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
|
||||
5. The file is removed automatically after one week, or earlier by explicit
|
||||
deletion.
|
||||
|
||||
## Filesystem layout
|
||||
@@ -128,7 +130,7 @@ curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/uploa
|
||||
{
|
||||
"id": "xmK5lLMRErlN",
|
||||
"key": "xmK5lLMRErlN",
|
||||
"url": "/d/xmK5lLMRErlN",
|
||||
"url": "/xmK5lLMRErlN",
|
||||
"originalName": "example.zip",
|
||||
"size": 123456,
|
||||
"protected": true
|
||||
@@ -139,27 +141,29 @@ curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/uploa
|
||||
the short length keeps share URLs tidy, and the required password adds a second
|
||||
factor of protection.
|
||||
|
||||
### `GET /d/:key` (also `HEAD`)
|
||||
### `GET /:key` (also `HEAD`)
|
||||
|
||||
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`.
|
||||
The download URL is just the key at the site root (`https://host/xmK5lLMRErlN`).
|
||||
The legacy `/d/:key` form still works as an alias. 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/xmK5lLMRErlN # prompts for password first
|
||||
curl -O http://localhost:3000/xmK5lLMRErlN # prompts for password first
|
||||
```
|
||||
|
||||
### Password protection
|
||||
|
||||
Every file requires its password:
|
||||
|
||||
1. `GET /d/:key` returns a small HTML password form.
|
||||
1. `GET /:key` returns a small HTML password form.
|
||||
2. The browser submits it to `POST /api/auth/:key` with `password=...`.
|
||||
3. On success the server sets a short-lived (30 min), HttpOnly, SameSite=Lax,
|
||||
signed cookie and redirects back to `/d/:key`, which then streams the file.
|
||||
signed cookie and redirects back to `/:key`, which then streams the file.
|
||||
|
||||
```bash
|
||||
curl -c cookies.txt -X POST -d "password=hunter2" http://localhost:3000/api/auth/:key
|
||||
curl -b cookies.txt -O http://localhost:3000/d/:key
|
||||
curl -b cookies.txt -O http://localhost:3000/:key
|
||||
```
|
||||
|
||||
The cookie is signed with a server secret (`storage/.secret`) so it cannot be
|
||||
@@ -237,7 +241,7 @@ server {
|
||||
listen 80;
|
||||
server_name files.example.com;
|
||||
|
||||
client_max_body_size 6g; # >= MAX_FILE_SIZE
|
||||
client_max_body_size 11g; # >= MAX_FILE_SIZE
|
||||
proxy_request_buffering off; # stream uploads, don't buffer
|
||||
|
||||
location / {
|
||||
@@ -343,7 +347,7 @@ src/
|
||||
├── log.ts # timestamped logging
|
||||
├── routes/
|
||||
│ ├── upload.ts # POST /api/upload
|
||||
│ ├── download.ts # GET /d/:key
|
||||
│ ├── download.ts # GET /:key
|
||||
│ └── delete.ts # DELETE /api/files/:id
|
||||
└── public/ # plain HTML/CSS/JS frontend (no framework)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user