89d64f64b3
New documentation/controller/import-and-data-paths.md: the canonical import root (and why it is NOT a registered StoragePath), the three data_paths roles, the Fork-3 validation asymmetry, the class-driven copy rule, and the seven invariants a future change must not break. Capability map "File access via browser" — status DELIBERATELY UNCHANGED. The drop-zone now has its own FileBrowser source and the app page carries a deep link, both verified live, but nothing drove the FileBrowser HTTP UI (no browser on DooPlex), so the row's standing "browse is exercised in no doc" caveat still holds and PROVEN-LIVE remains unearned. R-75 collapsed to its shipped one-liner. R-76 left open — this task does not fix it, and nothing built here assumes an import/* directory stays 2775.
118 lines
6.1 KiB
Markdown
118 lines
6.1 KiB
Markdown
# Import root & `data_paths` (R-75)
|
|
|
|
Shipped in controller **v0.172.0** (2026-07-26). Evidence:
|
|
`audits/SPIKE-catalog-data-paths-2026-07-26.md`.
|
|
|
|
---
|
|
|
|
## 1. The canonical import root
|
|
|
|
```
|
|
${IMPORT_PATH} = <system namespace root>/userdata/import
|
|
= /mnt/sys_drive/felhom-data/userdata/import (both demo boxes)
|
|
```
|
|
|
|
**One drop-zone per box, on the system drive — never per data drive.** It is resolved from
|
|
`cfg.Paths.SystemDataPath` via `appbackup.NamespaceRoot(sys, false)` (the system drive holds a
|
|
`felhom-data` SUBDIR, so it is the `inGuestDrive=false` case) and exposed as
|
|
`stacks.Manager.GetImportRoot()`.
|
|
|
|
**Why canonical, not per-drive** — this overrides the spike's Fork-1 recommendation, on a stronger
|
|
argument the spike did not weigh. Each drop-zone app has exactly **one** ingest bind. On a two-drive
|
|
box a per-drive `import/` would therefore put a folder that *looks* like a drop-zone on every drive
|
|
while only one of them does anything; and because `import/*` is `class: excluded`, files stranded in
|
|
a dead one are never backed up either. A canonical root is the only shape with no dead drop-zone.
|
|
|
|
**No fallback, deliberately.** If the system root is unresolvable, `IMPORT_PATH` is left **unset** and
|
|
compose fails loudly on `${IMPORT_PATH}`. It must never fall back to `<HDD_PATH>/userdata/import` —
|
|
that silently recreates the shape this design removes.
|
|
|
|
**The system drive is NOT a registered `StoragePath`** (verified on both demo boxes, 2026-07-26).
|
|
Registering it would make a 50 GB volume holding the recovery units a customer-visible drive, a deploy
|
|
target and a wipe candidate. Everything that must reach the import root therefore does so explicitly:
|
|
|
|
| Surface | How it reaches the import root |
|
|
|---|---|
|
|
| Deploy belt | `ensureUserdataMounts` — an import loop that is **not** drive-absent-gated |
|
|
| Skeleton | `Manager.EnsureImportRoot()`, not the per-storage-path skeleton loop |
|
|
| FileBrowser | an explicit extra bind `<root>:/srv/beolvasas`, outside `buildFileBrowserPaths` |
|
|
| SMB share | written directly to the store by `ensureImportShare` |
|
|
|
|
That last one matters: `sharingResolvePath` **refuses** the import root, correctly — it validates
|
|
paths a *customer* supplied through the picker and requires containment in a registered live root. A
|
|
controller-generated constant is a different trust class. (`TestImportRoot_NotReachableViaTheCustomerPicker`
|
|
pins both halves.)
|
|
|
|
---
|
|
|
|
## 2. The three roles
|
|
|
|
`data_paths:` is a **pure annotation** over paths that already exist as compose binds. It can never
|
|
declare a path — `ValidateDataPaths` rejects any entry matching no bind. That is the property which
|
|
keeps the design cheap: **no new filesystem-write primitive is introduced from catalog data.**
|
|
|
|
```yaml
|
|
data_paths:
|
|
- path: paperless # relative to the bind's root
|
|
root: import # import | userdata | hdd
|
|
role: import # import | library | export
|
|
label: "Beolvasandó dokumentumok"
|
|
```
|
|
|
|
| role | meaning | rendered? |
|
|
|---|---|---|
|
|
| `import` | drop-zone; the app consumes and usually DELETES from it | yes, + free space |
|
|
| `library` | where the customer's files live | yes |
|
|
| `export` | the app writes results here for the customer | yes |
|
|
|
|
`root: hdd` entries are **not** rendered: `appdata/` is app-internal, FileBrowser does not mount it,
|
|
and a link would 404.
|
|
|
|
### Validation is asymmetric, and that is a decision
|
|
|
|
| defect | outcome | precedent |
|
|
|---|---|---|
|
|
| malformed **path** (absolute, `..`, backslash, non-`Clean`'d, matches no bind, duplicate, unknown root) | **whole-block reject**, one ERROR, no folder card | `backup:` — paths govern DATA HANDLING |
|
|
| unknown **role** | **fails OPEN** — that entry is not surfaced, one WARN, the rest renders | `Lifecycle` — roles govern PRESENTATION; a typo must never brick a template |
|
|
|
|
The path rules reuse `appbackup.ValidateRelPath`, the single refusal set shared with `backup:`.
|
|
**Do not write a second path validator.**
|
|
|
|
---
|
|
|
|
## 3. The class-driven copy rule
|
|
|
|
The consequence sentence on the app page is driven by the **derived backup class**, never by the role
|
|
and never by a per-app string, so the promise the UI makes cannot drift from what the backup engines
|
|
actually do:
|
|
|
|
| class | Hungarian copy |
|
|
|---|---|
|
|
| `excluded` + `role: import` | „Ide másold a feldolgozandó fájlokat. Az alkalmazás beolvassa, majd törli innen — ez a mappa átmeneti, és nem készül róla biztonsági mentés." |
|
|
| `excluded` (other) | „Ez a mappa átmeneti, és nem készül róla biztonsági mentés." |
|
|
| `mandatory` / `optional` | „Itt tárolódnak a fájljaid. Biztonsági mentés készül róla." |
|
|
| unclassified | **nothing** — an unverified backup promise is worse than no sentence |
|
|
|
|
Both catalog import paths are `class: excluded`, so both must carry the temporary-and-unbacked
|
|
warning. `TestConsequenceIsClassDriven` pins it.
|
|
|
|
---
|
|
|
|
## 4. Invariants a future change must not break
|
|
|
|
1. **Zero removals.** `UserdataSkeletonCarry()` is the v0.171.0 hardcoded list, retained forever, so
|
|
derivation can only ADD. `documents` is implied by no catalog app and exists on both demo boxes.
|
|
2. **The skeleton set is SORTED.** `fbNeedsRecreate` force-recreates FileBrowser on any byte diff
|
|
across ~14 `SyncFileBrowserMounts` call sites; the naive map-order derivation measured **20
|
|
distinct outputs from 20 identical runs**.
|
|
3. **The catalog sync is NOT wired to `SyncFileBrowserMounts`.** Connecting them turns a 15-minute
|
|
catalog cycle into a 15-minute container-restart cycle.
|
|
4. **`url.PathEscape` per path segment for deep links — never `url.QueryEscape`** (it encodes a space
|
|
as `+`, which in a path segment is a literal plus). Let `html/template` do the attribute escaping.
|
|
5. **Sharing stays opt-in.** The `beolvasas` share is created when sharing is ENABLED, never as a side
|
|
effect of deploying an app.
|
|
6. **`${IMPORT_PATH}` binds do not resolve against `hddPath`.** Any new consumer of `BindRoot` needs
|
|
an explicit `RootImport` case with the import root passed in.
|
|
7. **Do not assume an `import/*` directory stays `2775`** — see R-76: FileBrowser creates `0755`
|
|
without propagating setgid, and a live `import/calibre` was found at `755`.
|