v0.128.0: chunked browser .fab upload on /import (tunnel-proof)

Cloudflare edge caps request bodies (~100 MB, probed live: 120 MiB -> edge 413,
80 MiB -> origin), so the client slices the file into 64 MiB strictly-sequential
chunks; the server streams each to a .part file in the default drive's exports
dir and finalize renames atomically. Scan/validate/import pipeline untouched.
upload/{init,chunk,finalize,abort} inside ServeExportAPI (inherits auth+CSRF);
single-flight; offset==received or 409+echo; free-space gate; collision ->
lowest-free "name (N).fab"; startup GC of *.part-*; 15-min idle abort.
appexport.DiskFree exported (seam web.uploadDiskFree). Scenarios A-F tested,
red-proofs run (traversal / out-of-order / overwrite).
This commit is contained in:
2026-07-13 21:09:20 +02:00
parent 59e4ca70de
commit db16371f47
11 changed files with 1055 additions and 8 deletions
+35
View File
@@ -1,5 +1,40 @@
## Changelog
### v0.128.0 — browser .fab upload on the import page: chunked, tunnel-proof (2026-07-13)
The Restore/import flow no longer requires copying `.fab` files to `{tároló}/exports/` by hand
(the FileBrowser step alpha testers stumble on): `/import` now has a drag-and-drop/file-picker
upload zone. The binding constraint is the Cloudflare tunnel's request-body cap — **step-0 probe
on the REAL tunnel (2026-07-13): 120 MiB POST → edge HTTP 413 from `Server: cloudflare` before
the origin saw it; 80 MiB → passed to the origin (302 /login)** — so the client slices the file
(`File.slice`, 64 MiB chunks, strictly sequential) and the server appends each chunk to a
`.part-<random>` file in the DEFAULT drive's exports dir via `io.Copy` (no RAM proportional to
file size), then finalize fsyncs + atomically renames. The existing bundle scan + validation +
import pipeline take over untouched — the landing dir is exactly what `isValidExportPath` and
`ScanForBundles` already cover.
- **Endpoints** (inside `ServeExportAPI` — inherits the main.go `RequireAuth(CsrfProtect(...))`
mount, nothing added at the mux): `POST /api/export/upload/{init,chunk,finalize,abort}`.
Single-flight (second init → 409). Init sanitizes the filename to a `[A-Za-z0-9._ -]` base
name with a mandatory `.fab` suffix and gates on free space (declared size + 1 GiB margin,
Hungarian error with both numbers). Chunk offset MUST equal bytes received (mismatch → 409 +
`received_bytes` so the client re-syncs one step); per-request body cap 96 MiB. Finalize
requires the exact declared size (mismatch → 422, `.part` deleted) and lands collisions on the
lowest-free `"name (N).fab"` (a re-run finds its own prior `(N)` — never `(1)(1)`).
- **No client-side sha256 — deliberate:** WebCrypto can't stream-hash multi-GB files; the `.fab`
format self-validates at import. Transport integrity = sequential offsets + exact final size +
the format's own validation.
- **Crash-safety:** upload state is in-memory (a restart loses the `.part`; the browser
re-uploads). Startup GC removes `*.part-*` in every registered drive's exports dir; an upload
idle ≥15 min is aborted server-side.
- **UI** (`app_import.html`): upload zone above the bundle list ("Fájl kiválasztása" / húzza ide),
progress "Feltöltés: {pct}% ({done} / {total} GB)" + "Megszakítás"; on success the page reloads
(the existing scan renders the new row). One retry per chunk on network error, re-synced from
the 409 echo.
- **Reuse:** `appexport.DiskFree` exported (was `diskFree`) for the space gate via the
`web.uploadDiskFree` test seam. Scenarios §7 AF tested; red-proofs run for the traversal
sanitize, the out-of-order append and the collision overwrite (all FAILED pre-fix as required).
### v0.127.3 — reveal copy states the shown code is ALREADY the live one (2026-07-13, Viktor)
The supersede happens at upload, inside the ceremony job — BEFORE the code is ever displayed.