feat(booth): downloadable booths — whole-booth zip + ?dl force-download (v0.1.4)

A verbatim index.html booth (e.g. edict-design-brief: a rendered brief + its
.md) had no download affordance — the page is served raw with no gallery/per-file
chrome. Adds:
  - GET /b/<name>/?download=1 -> streams the whole booth as <name>.zip (attachment)
  - GET /b/<name>/<file>?dl=1 -> forces Content-Disposition: attachment (html/md/
    text otherwise render inline with no easy save)
  - a download link on the index card (the accessible spot for verbatim booths)
    and the gallery header
Tests for both; verified live against edict-design-brief on nh3-dev :8090.
This commit is contained in:
vh
2026-07-25 17:49:11 -07:00
parent c2d6b3290e
commit a8bfe592b4
5 changed files with 73 additions and 4 deletions
+31
View File
@@ -150,6 +150,37 @@ def test_booth_serves_file(client):
assert r.content == b"\x00\x01payload"
def test_booth_zip_download(client):
# a verbatim index.html booth (no per-file chrome) is still downloadable as a zip
c, data = client
d = data / "brief"
d.mkdir()
(d / "index.html").write_text("<h1>BRIEF</h1>")
(d / "notes.md").write_text("# notes")
r = c.get("/b/brief/?download=1")
assert r.status_code == 200
assert r.headers["content-type"] == "application/zip"
assert "attachment" in r.headers["content-disposition"]
assert "brief.zip" in r.headers["content-disposition"]
import io as _io
import zipfile as _zip
assert set(_zip.ZipFile(_io.BytesIO(r.content)).namelist()) == {"index.html", "notes.md"}
def test_booth_file_force_download(client):
# ?dl=1 forces attachment so html/md/text saves instead of rendering inline
c, data = client
d = data / "brief"
d.mkdir()
(d / "index.html").write_text("<h1>BRIEF</h1>")
r = c.get("/b/brief/index.html")
assert "attachment" not in r.headers.get("content-disposition", "")
r2 = c.get("/b/brief/index.html?dl=1")
assert r2.status_code == 200
assert "attachment" in r2.headers["content-disposition"]
assert "index.html" in r2.headers["content-disposition"]
def test_missing_booth_404(client):
c, _ = client
assert c.get("/b/nope/").status_code == 404