Land the package filesystem shell: streaming atomic package_io, journaled rollback carve-out, asymmetric platform pickers

This commit is contained in:
2026-08-02 07:36:25 -04:00
parent 09a9ef838f
commit 41a3016e63
11 changed files with 914 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# src/shell/package — package filesystem + dialog seam
## Scope
The filesystem and dialog acts behind bank-package export/import: streaming package
file I/O (`package_io`), the landed-file journal and its rollback delete
(`package_rollback`), and the platform file pickers (`package_pickers`). This seam
is bytes-only — the package format (magic, manifest, entry layout) is
`core/package`'s business, and the export/import verbs that orchestrate both do not
live here yet. No REAPER project state is touched in this directory: no ext-state
read or write, no undo block, no generation bump — those belong to the verbs.
## Invariants
- **Atomic write.** A package accumulates in a `.rsbanktmp` sibling in the
destination directory and reaches the destination only through `commit()`'s
rename (the mono-collapse temp+rename precedent). A failed, aborted, or abandoned
write leaves the destination absent or holding its prior contents — never a
partial `.rsbank`.
- **Streaming, both ways — at most ONE entry's payload in memory.** Writes append
one payload at a time; reads seek and materialize one range at a time. The claim
is structural, not aspirational: every payload crosses this seam as a move-only
`PayloadBuffer`, and `PayloadBuffer::alive()` is the seam counter the tests
assert against. There is no read-whole-package or write-whole-package entry
point; do not add one.
- **The rollback delete is prune's ONE carve-out, cited not restated.** The
citation and the discriminator live at `package_rollback.cpp`'s header. The
journal makes the discriminator structural: only paths its own `writeLandedFile`
successfully created are recorded, and `rollback()` consumes only the record — a
path this import did not write cannot be handed to it.
- **No overwrite of a bank-folder file, ever.** `writeLandedFile` refuses an
existing destination outright; collision handling (auto-rename) is the import
plan's job upstream. The package writer itself DOES replace an existing
destination — the export save dialog's own overwrite confirm is the consent —
and that asymmetry is deliberate.
- **The two pickers are asymmetric, and the asymmetry is real.** Import rides
REAPER's own `GetUserFileNameForRead` (both platforms); export goes native —
Win32 `GetSaveFileNameW` / SWELL `BrowseForSaveFile` — because the always-present
REAPER surface offers no save picker. Do not symmetrize; the newer
`GetUserFileName(mode=0)` alternative and why it is not used are recorded in
`package_pickers.cpp`'s header.
## Modules
- `package_io` — the streaming filesystem seam: `PayloadBuffer` (move-only payload + the `alive()` seam counter), `PackageFileWriter` (append-only temp+atomic-rename writer), `PackageFileReader` (seek-and-read one range per call, range-checked against the real file size), `readFilePayload` (one source file as one entry's payload), and `listFolderFileNames` (bare names, sorted, non-recursive, non-throwing). REAPER-free; tested without a DAW.
- `package_rollback``LandedFileJournal`: `writeLandedFile` (temp+rename land, recorded on success only, refuses an existing destination and an empty payload) and `rollback` (deletes exactly the recorded set, hard unlink — nothing ever referenced these bytes — tolerating a vanished file). REAPER-free; tested without a DAW.
- `package_pickers` — the two pickers in one platform TU (`#ifdef _WIN32` / `#else swell/swell.h`, the `draw_kit`/`prune_fs` split): `pickPackageForImport` (REAPER read picker) and `pickPackageSavePath` (native save dialog, UTF-8 in/out on Windows). Compile-only until the verbs land; nothing here can be exercised in a unit test.
## Gotchas
- A crash mid-write strands the `.rsbanktmp` sibling. It is not a `.rsbank` (no
picker filter matches it), and a later export to the same destination truncates
it — but one stranded in the BANK folder by a mid-import crash is a foreign file
to prune (not owned, so never an orphan) until removed by hand. `[verify — DAW]`
whether the import verb should pre-clean stale `.rsbanktmp` names when it lands.
- The picker `defext`/filter strings are spelled to the Win32 `lpstrDefExt`
convention (no dot) but are `[verify — DAW]` on all three platforms — neither
picker is exercised outside a live REAPER session.
- `readRange(_, 0)` returns an empty buffer — indistinguishable from failure, by
design (the one "nothing to work with" branch). A genuinely zero-length entry
cannot round-trip through this seam; the format layer must not emit one.