diff --git a/docs/roadmap/storage-locations.md b/docs/roadmap/storage-locations.md index 5eb2094..570cb93 100644 --- a/docs/roadmap/storage-locations.md +++ b/docs/roadmap/storage-locations.md @@ -18,7 +18,7 @@ Nextcloud's 4 TB of photos go on the spinning disk. Vaultwarden and the control - ❌ A general volume manager. We don't format, partition, mount, or write `/etc/fstab`. The drive must already be mounted; we validate and use it. - ❌ Striping/tiering/RAID-alikes. One app's data lives on exactly one location. No splitting an app across two. - ❌ Per-*volume* placement inside an app (`./data` here, `./db` there). Location granularity is the app directory. Revisit only if a real need shows up. -- ❌ Making backup locations and storage locations the same registry. They overlap on disk but differ in trust, lifecycle and ownership (see §9, §11.2). +- ❌ Making backup locations and storage locations the same registry. They overlap on disk but differ in trust, lifecycle and ownership (see §11, §13.2). ## 2. What already works in our favour @@ -67,8 +67,8 @@ That single file then does **three** jobs, which is the main reason to like it: | Job | How | |---|---| | Admission | "empty or marked" — §3 | -| **Mount detection** | the marker lives *on the drive*. Not mounted ⇒ bare mountpoint ⇒ no marker ⇒ location unavailable. No `findmnt`, no `fs_uuid` bookkeeping, and it works identically for USB disks, network mounts, and LUKS volumes that haven't been unlocked (§8.1) | -| Provenance on migrate | it carries the install id and location id the snapshot's manifest names, so "is this the same `bigdisk` the app came from?" is a file read (§7) | +| **Mount detection** | the marker lives *on the drive*. Not mounted ⇒ bare mountpoint ⇒ no marker ⇒ location unavailable. No `findmnt`, no `fs_uuid` bookkeeping, and it works identically for USB disks, network mounts, and LUKS volumes that haven't been unlocked (§10.1) | +| Provenance on migrate | it carries the install id and location id the snapshot's manifest names, so "is this the same `bigdisk` the app came from?" is a file read (§9) | **Residual wrinkle, not solved by elegance:** validate-then-`chown` is a TOCTOU window, and bash is a poor language for race-free path handling. The practical closure is to additionally require that the **parent** directory is not manager-writable — true for `/mnt`, `/srv`, `/media`, false for a path inside the manager's home. That's a real restriction on where locations may live, not a free lunch, and it should be stated in the docs rather than hidden. @@ -84,7 +84,7 @@ pathIsContainerData

# is this path under ANY container root? - **`appDir`** builds a `slug → dir` map once per process by scanning each enabled root for `*/.config`, memoises it in an associative array, and falls back to the primary root for a slug that doesn't exist yet (fresh install). Discovery-first, config-second: if the map and `CFG__STORAGE` disagree, **the directory on disk wins** and we warn. That's what makes the system self-healing after a hand-move or a half-finished migration. - It is also **the single place the availability check belongs**. Every one of the ~200 sites has to call it, so a location whose drive is missing fails *once*, centrally, instead of needing a guard sprinkled at each caller. `appDir` returns non-zero **and** prints an unusable sentinel path, so the many callers that won't check `$?` still fail loudly on a path that cannot exist, rather than writing into a bare mountpoint. See §8.1. + It is also **the single place the availability check belongs**. Every one of the ~200 sites has to call it, so a location whose drive is missing fails *once*, centrally, instead of needing a guard sprinkled at each caller. `appDir` returns non-zero **and** prints an unusable sentinel path, so the many callers that won't check `$?` still fail loudly on a path that cannot exist, rather than writing into a bare mountpoint. See §10.1. - **`pathIsContainerData`** replaces the `[[ "$p" == "$containers_dir"* ]]` idiom that decides manager-vs-container-user elevation. It appears in ~10 files (`create_folder.sh`, `create_touch.sh`, `copy_file.sh`, `copy_files.sh`, `copy_folder.sh`, `copy_folders.sh`, `move_file.sh`, `runCfgOp`, `tags_manager_update.sh`, `webui_atomic_write.sh`). **Every one is a silent-corruption bug if missed** — a file under an unrecognised root gets written as the manager, lands with the wrong owner, and the container fails to read it at a moment far removed from the cause. Scale of the sweep (measured, not estimated): @@ -93,7 +93,7 @@ Scale of the sweep (measured, not estimated): |---|---|---| | `$containers_dir$app`-shaped, app-scoped | ~186 across ~90 files | → `$(appDir "$app")` | | `$containers_dir/libreportal/…` (the WebUI's own tree) | 63 | → `webuiDir` helper, **pinned to the primary root** | -| `$containers_dir/{traefik,prometheus,grafana,adguard,…}` — one app reaching into another by literal name | ~29 | → `appDir `; those apps stay pinned in phase 1 (§8.3) | +| `$containers_dir/{traefik,prometheus,grafana,adguard,…}` — one app reaching into another by literal name | ~29 | → `appDir `; those apps stay pinned in phase 1 (§10.3) | | root-helper `$CONTAINERS_DIR/$app` | `libreportal-ownership`, `libreportal-appcfg` | → registry lookup inside the helper | | the `== "$containers_dir"*` elevation test | ~10 | → `pathIsContainerData` | @@ -112,7 +112,7 @@ CFG_BOOKSTACK_STORAGE=default - The value is a **location name**, not a path. Names are what survive a migrate to a host whose disks are laid out differently; paths aren't. `default` always resolves to the install-time containers root. - It renders as a dropdown on the app's config page for free — the WebUI config renderer already builds selects from the `[a:A|b:B]` comment convention; the option list is generated from the registry into the config comment on regen. -- Changing it in the config editor does **not** move data. It records intent; the move is `libreportal app move` (§6). The config page shows the current *resolved* directory next to the field, and flags a mismatch. +- Changing it in the config editor does **not** move data. It records intent; the move is `libreportal app move` (§8). The config page shows the current *resolved* directory next to the field, and flags a mismatch. - `configBackfillMissingKeys` carries the new key into already-installed apps on the next update, so nothing needs a reinstall. - **New compose tag** `#LIBREPORTAL|APP_DIR_TAG|APP_DIR_DATA`, filled in `dockerConfigSetupFileWithData` alongside the existing `CONTAINERS_DIR_TAG`, for the rare template that genuinely needs its own absolute host path. Most apps need nothing — relative volumes already do the right thing. @@ -123,11 +123,71 @@ A path reads better in isolation — `grep STORAGE *.config` would tell you wher - **Mount paths move.** External drives arrive at `/media//