diff --git a/docs/roadmap/storage-locations.md b/docs/roadmap/storage-locations.md index e2eed17..2a1980f 100644 --- a/docs/roadmap/storage-locations.md +++ b/docs/roadmap/storage-locations.md @@ -1,6 +1,6 @@ # LibrePortal — Storage Locations (per-app data placement) -**Status:** Proposal — **not built.** · **Audience:** us, future-self · **Scope:** register more than one filesystem root for live app data, choose one per app, move an app between them, and resolve the right one on restore/migrate · **Origin:** "add different locations to set up LibrePortal on, with control per app" (2026-08-24) +**Status:** Phases 0-2, 4 and 5 **built**; the two WebUI surfaces (setup-wizard step, Disks page) are not. · **Audience:** us, future-self · **Scope:** register more than one filesystem root for live app data, choose one per app, move an app between them, and resolve the right one on restore/migrate · **Origin:** "add different locations to set up LibrePortal on, with control per app" (2026-08-24) --- @@ -345,6 +345,44 @@ The WebUI's own dir stays pinned permanently and *structurally* (`webuiDir`), no **10.7 — Footprint version.** New root helper + changed baked helpers ⇒ **bump `footprint_version`** (currently 5), or existing installs update the manager-owned code and keep root helpers that don't know about the registry. +## 10.8 — Postmortem: the index that became a fork bomb + +Recorded because the mistake was subtle and the blast radius was the whole +machine. + +`storageIndexSet` cached the app→root map at `configs/storage/app_locations`. The +file's stated requirements are "manager-owned" and "not on a removable disk", and +`configs/` satisfies both — which is exactly why it looked like the right home. +It carries a third property the file violated: **`sourceScanFiles` sources what it +finds under `configs/`, and sourcing means executing.** + +The index is a TSV of ``. bash reads that as a command and its +argument. It stayed harmless while no slug matched a real executable — and armed +the instant a row existed for the app named `libreportal`, because that IS the CLI +on `PATH`. Sourcing ran `libreportal /libreportal-containers`, which re-entered the +scan, which sourced the file again: one process pair per level, from every CLI +invocation on the box including the task processor's own poll. 514 `start.sh` +processes within 100 seconds of boot; the kernel killed plasmashell, kwin and sddm +as collateral. + +Two fixes, deliberately at both ends: + +- `scan_files.sh` now requires a `.category` marker before sourcing anything in a + `configs/` subdirectory — the contract `commandReloadConfigs` already enforced, + and one all five real categories already satisfied. +- the index moved to `$system_dir/storage/app_locations`, with a one-shot + migration. A machine-written data file has no business in the one tree whose + contract is "everything here is executed", guard or no guard. + +`scripts/dev/lp-configs-guard-test` pins both: a file of the exact detonating +shape placed in an unmarked `configs/` subdirectory must not execute, while a +marked category must still load. + +**The generalisable rule:** before putting a file anywhere in this repo, ask what +the directory's *contract* is, not just who owns it. `configs/**` is executed. +`containers//**.config` is executed (see the note in `scan_files.sh`). Both +are load-bearing conventions that a plain data file silently violates. + ## 11. What doesn't get a clean answer Most of §10 collapses into a small number of good primitives — the marker file, `appDir`, the emptiness rule, a comment marker. Four things don't, and pretending otherwise would set us up to be surprised later. @@ -360,13 +398,13 @@ Each phase is independently shippable and independently verifiable. Phase 0 carr | Phase | Deliverable | Visible? | |---|---|---| -| **0** | `appDir` / `storageRoots` / `pathIsContainerData` / `webuiDir`; sweep the ~200 call sites; helpers read a registry that contains exactly one root. Full install + backup + restore verified. | No | -| **1** | Root-owned registry + `libreportal-storage` helper + the §6 fitness checks + `libreportal storage {list,add,remove,check,scan}`. Locations can exist; nothing uses them yet. | CLI only | -| **2** | `CFG__STORAGE` + the `**READONLY**` comment marker, resolved path in the comment (§5.2), install-time placement, per-location config panels and disk gauges. | Yes | -| **3** | Setup-wizard **Storage** step (§7) + `--storage-dir=` for unattended installs. Depends on 1 and 2; deliberately after them, so first-run drives a path that already works headlessly. | Yes | -| **4** | `libreportal app move`. | Yes | -| **5** | Manifest `storage` block, staged restore + path rewrite (fixes §9's latent bug), the "unknown location" prompt in migrate. | Yes | -| **6** | **Disks view** (§7.1) — extend `/admin/system/storage` into one row per filesystem with roles, badges and actions; `libreportal storage disks`. Periodic `storage check` + dashboard degradation warnings. Replaces the separate *Storage* page this phase used to propose: with disks as the frame, a second locations page would reintroduce exactly the split §13.1 resolved. | Yes | +| **0** ✅ | `appDir` / `storageRoots` / `pathIsContainerData` / `webuiDir`; ~260 call sites swept. `scripts/dev/lp-storage-test`. | No | +| **1** ✅ | Root-owned registry + `libreportal-storage` + fitness checks + `libreportal storage {list,add,remove,check,scan,apps,disks}`. | CLI only | +| **2** ✅ | `CFG__STORAGE` in 37 templates + the `**READONLY**` marker + resolved path in the comment (§5.2) + per-location config panel emit. | Yes | +| **3** ⬜ | Setup-wizard **Storage** step (§7) + `--storage-dir=`. **Not built.** The headless path it would drive already works, so this is UI on top of a working feature, not a prerequisite. | Yes | +| **4** ✅ | `libreportal app move` — stop, snapshot, copy as root, verify, then delete the source. | Yes | +| **5** ◐ | Manifest `storage` block and the staged restore + path rewrite are done (§9's latent bug is fixed). The "unknown location" **prompt** is not — an unresolvable location currently falls back to this host's default rather than asking. | Yes | +| **6** ◐ | The Disks **data layer** is built and `libreportal storage disks` renders it (including not-attached rows). The WebUI page at `/admin/system/storage` and the periodic `storage check` cron are **not**. | Yes | ## 13. Open questions