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:
@@ -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 A–F 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.
|
||||
|
||||
Reference in New Issue
Block a user