feat(booth): upload-for-pickup with human-readable ids (v0.1.1)
Add a reverse direction to the Booth: the operator (or any client via `curl -F`) can upload files through the browser and pick them up by a human-readable id. - POST /upload — streams files to a new booth named with a human-readable id (e.g. 4-wombat / star-84), 303-redirects to /b/<id>/ (id in the Location header so curl clients can read it). Uploads reuse the whole booth machinery (render, per-file download links, 24h TTL sweep, delete). - Human-readable ids: word+number in either order, collision-checked, from a curated 140-word friendly list; secrets-based selection. - Safety: filenames reduced to a safe basename (no traversal), streaming size cap (BOOTH_MAX_UPLOAD_MB, default 1024) + file-count cap (BOOTH_MAX_FILES, default 50), partial-write cleanup on any failure. - UI: Australis-themed upload/drop panel (drag-drop, progressive-enhancement JS, degrades to a native file input), a "⬆ pickup" badge on upload booths, a pickup banner, and a ⬇ download link on every gallery item. - python-multipart dependency; homepage tile description updated; 9 new tests (24 total, all green).
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
# The Booth
|
||||
|
||||
A dead-simple standing web server for surfacing **ephemeral media** to the
|
||||
operator — A/B renders, smoke-test screenshots, audio/video samples. A Claude
|
||||
Code session drops a folder of files somewhere on disk; the Booth renders it as
|
||||
a browsable "booth" and **wipes it 24h after the last activity**. No database,
|
||||
no upload API — the filesystem *is* the state.
|
||||
A dead-simple standing web server for shuttling **ephemeral files** between the
|
||||
operator and CC sessions — A/B renders, smoke-test screenshots, audio/video
|
||||
samples, or anything you want to hand off. It works both directions:
|
||||
|
||||
- **Session → operator:** a session drops a folder of files on disk; the Booth
|
||||
renders it as a browsable "booth".
|
||||
- **Operator/anyone → pickup:** upload files through the browser (or `curl -F`)
|
||||
and get a **human-readable pickup id** like `4-wombat` or `star-84`.
|
||||
|
||||
Either way it **wipes 24h after the last activity**. No database — the
|
||||
filesystem *is* the state.
|
||||
|
||||
- **Live:** http://10.100.10.50:8090/ (nh3-dev) · linked from Homepage → *Apps → The Booth*
|
||||
- **Data dir:** `~/booth-data/` on nh3-dev (one subfolder per booth)
|
||||
@@ -33,6 +39,27 @@ rsync -a ./out/ nh3-dev:booth-data/my-run/
|
||||
|
||||
Then hand the operator `http://10.100.10.50:8090/b/my-run/`.
|
||||
|
||||
## Upload for pickup
|
||||
|
||||
The reverse direction — put files in through the web, pick them up by id:
|
||||
|
||||
- **Browser:** the index page has an *Upload files for pickup* panel
|
||||
(drag-drop or click). Submit → you land on a booth with a **human-readable
|
||||
id** (`4-wombat`, `star-84`) whose files each have a ⬇ download link.
|
||||
- **curl (a remote session with no ssh to nh3-dev can use this too):**
|
||||
```bash
|
||||
curl -sS -i -F 'files=@out/a.png' -F 'files=@out/b.png' \
|
||||
http://10.100.10.50:8090/upload | grep -i location
|
||||
# Location: /b/star-84/ <- the pickup id
|
||||
```
|
||||
- **Pick up** at `http://10.100.10.50:8090/b/<id>/` (download links), or on
|
||||
nh3-dev straight off disk at `~/booth-data/<id>/`.
|
||||
|
||||
Uploads are stamped as pickup booths (a `⬆ pickup` badge in the UI) and expire
|
||||
on the same 24h TTL. Limits: `BOOTH_MAX_FILES` files (default 50) and
|
||||
`BOOTH_MAX_UPLOAD_MB` total per submission (default 1024); filenames are reduced
|
||||
to a safe basename (no path traversal).
|
||||
|
||||
## What a booth renders
|
||||
|
||||
- **Has its own `index.html`?** → served **verbatim** (its relative assets —
|
||||
@@ -57,6 +84,7 @@ Then hand the operator `http://10.100.10.50:8090/b/my-run/`.
|
||||
| `GET /` | Index — one card per booth (newest first), with expiry countdown |
|
||||
| `GET /b/<name>/` | A booth (its `index.html`, else auto-gallery) |
|
||||
| `GET /b/<name>/<file>` | Serve a file out of the booth |
|
||||
| `POST /upload` | Upload files → new pickup booth; 303-redirects to `/b/<id>/` (id in `Location`) |
|
||||
| `POST /b/<name>/delete` | Wipe a booth (the UI's "Wipe now" button) |
|
||||
| `DELETE /b/<name>` | Wipe a booth (curl/API) |
|
||||
| `GET /healthz` | `{ok, ttl_hours, booths}` — Homepage siteMonitor target |
|
||||
@@ -73,13 +101,14 @@ journalctl --user -u booth.service -f # sweeper logs "[booth] swept …
|
||||
```
|
||||
|
||||
Config is env in the unit (`booth.service`):
|
||||
`BOOTH_DATA_DIR`, `BOOTH_TTL_HOURS`, `BOOTH_HOST_LABEL`, `BOOTH_SWEEP_INTERVAL_MIN`.
|
||||
`BOOTH_DATA_DIR`, `BOOTH_TTL_HOURS`, `BOOTH_HOST_LABEL`, `BOOTH_SWEEP_INTERVAL_MIN`,
|
||||
`BOOTH_MAX_UPLOAD_MB` (default 1024), `BOOTH_MAX_FILES` (default 50).
|
||||
|
||||
### Install / update
|
||||
|
||||
```bash
|
||||
cd services/booth
|
||||
uv venv && uv pip install fastapi "uvicorn[standard]" jinja2 # runtime deps
|
||||
uv venv && uv pip install fastapi "uvicorn[standard]" jinja2 python-multipart # runtime deps
|
||||
cp booth.service ~/.config/systemd/user/booth.service
|
||||
systemctl --user daemon-reload && systemctl --user enable --now booth.service
|
||||
```
|
||||
@@ -97,8 +126,8 @@ cd services/booth && uv pip install pytest httpx && .venv/bin/python -m pytest -
|
||||
## Notes / non-goals
|
||||
|
||||
- **No auth.** LAN/WG-internal only, ephemeral content — don't drop secrets in a
|
||||
booth. (The data dir is world-readable-on-LAN via the server.)
|
||||
- **No upload API** by design. Sessions have filesystem access to nh3-dev; a
|
||||
folder drop is simpler and more debuggable than an HTTP upload path.
|
||||
- Booth names with `/`, `..`, or a leading `.` are rejected; file serving is
|
||||
guarded against path traversal and symlink escape.
|
||||
booth, and note anyone on the LAN can upload (bounded by the size/file limits).
|
||||
Uploaded files are served back with their own content-type, so an uploaded
|
||||
`index.html` renders as a page (a feature for custom reports; keep it in mind).
|
||||
- Booth names with `/`, `..`, or a leading `.` are rejected; file serving and
|
||||
uploaded filenames are guarded against path traversal and symlink escape.
|
||||
|
||||
Reference in New Issue
Block a user