Files
felhom.eu/documentation/controller/import-and-data-paths.md
T
admin 89d64f64b3 R-75 SHIPPED (controller v0.172.0): docs, capability-map note, ROADMAP collapse
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.
2026-07-26 08:30:45 +02:00

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`.