F-S2 + F-S3: compose-derived appdata dir resolution (v0.131.0)
The controller assumed an app's HDD appdata dir is always appdata/<stackName>.
paperless-ngx writes appdata/paperless (stack paperless-ngx), so every consumer
keying by stack name silently missed it via a stat-and-skip. One canonical
resolver appbackup.AppDataDirNames derives the real dir name(s) from the app's
compose ${HDD_PATH} binds; all consumers use it.
- F-S2 (tier-2): RunTier2 mirrors the resolved appdata/<name> (paperless docs
got NO tier-2 copy before). Tier2Info size + RestoreTier2Files live dir use it.
WARN when a declared appdata dir is absent. New tier2Mirror seam.
- F-S3 (migrate, NEW): all six per-app appdata legs (collision/size/copy/verify/
cleanup/skip-set) now loop resolved names. scope="app" migration of paperless
previously copied nothing and left an empty media dir (scope="all" was saved by
the merge walk). WARN on missing declared dir in the copy leg.
- Multi-dir (N>1) refusal: tier-2 backup/info/restore refuse loudly (Hungarian);
migrate supports N. No catalog app hits it today; lifted by Task 3.
- Display: storage page sums resolved dirs.
- Truth repair: the v0.130.0 "tier-2 copies the namespace wholesale" claim is
false; corrected in CHANGELOG + main.go export-adapter comment.
+9 tests; red-proofs RP-1..RP-5 all confirmed. Controller-only, no agent/hub
coupling. Task 1 of the backup-classification-redesign arc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
This commit is contained in:
+11
-5
@@ -476,7 +476,7 @@ Path computation is centralized in `backup/paths.go` via the `FelhomDataDir = "f
|
||||
- `PrimaryResticRepoPath(drivePath)` → `<drive>/felhom-data/backups/primary/restic/`
|
||||
- `AppDBDumpPath(drivePath, stackName)` → `<drive>/felhom-data/backups/primary/<stack>/db-dumps/`
|
||||
- `AppVolumeDumpPath(drivePath, stackName)` → `<drive>/felhom-data/backups/primary/<stack>/volume-dumps/`
|
||||
- `AppDataDir(drivePath, stackName)` → `<drive>/felhom-data/appdata/<stack>/`
|
||||
- `AppDataDir(drivePath, name)` → `<drive>/felhom-data/appdata/<name>/` (final segment is the app's real appdata dir NAME, resolved via `AppDataDirNames` from compose binds — NOT always the stack name; F-S2)
|
||||
- `SecondaryResticRepoPath(drivePath)` → `<drive>/felhom-data/backups/secondary/restic/`
|
||||
- `AppSecondaryRsyncPath(drivePath, stackName)` → `<drive>/felhom-data/backups/secondary/<stack>/rsync/`
|
||||
- `SecondaryInfraPath(drivePath)` → `<drive>/felhom-data/backups/secondary/_infra/`
|
||||
@@ -529,9 +529,14 @@ backups/primary/<app>/
|
||||
#### Tier 2 — off-drive copy (Phase 3, v0.55.x)
|
||||
|
||||
For every HDD app, Tier 2 (`internal/backup/tier2.go`) rsync-mirrors the recovery unit
|
||||
(`backups/primary/<app>/`) + the app's `appdata/<app>/` to `<target>/backups/secondary/<app>/` on a
|
||||
**different physical disk** — the only off-drive protection bind-mounted HDD userdata can get (PBS can't
|
||||
reach bind mounts). Auto-targeted: **prefer another registered user-data drive** (off-disk via
|
||||
(`backups/primary/<app>/`) + the app's resolved `appdata/<name>/` to `<target>/backups/secondary/<app>/`
|
||||
on a **different physical disk** — the only off-drive protection bind-mounted HDD app data can get (PBS
|
||||
can't reach bind mounts). **The appdata dir NAME is derived from the app's compose `${HDD_PATH}` binds,
|
||||
not assumed to be the stack name** (F-S2, v0.131.0: paperless-ngx writes `appdata/paperless`;
|
||||
`tier2AppDataName` → `appbackup.AppDataDirNames`); an app resolving to >1 distinct appdata dir is
|
||||
refused loudly. This copies the recovery unit + `appdata/<name>` ONLY — **not** the browsable
|
||||
`userdata/` tree (F-S1, owned by the backup-classification redesign) and **not** the namespace
|
||||
wholesale. Auto-targeted: **prefer another registered user-data drive** (off-disk via
|
||||
`system.SamePhysicalDevice`); else the **internal SSD for small units only**, behind a size-aware
|
||||
**rootfs-headroom guard** (`tier2FitsHeadroom`) that **refuses rather than fills** the ~8 GB guest rootfs
|
||||
(reserve = `max(2 GB, 20%)`), recording an honest "needs a 2nd HDD" status. Status persists via
|
||||
@@ -541,7 +546,8 @@ reach bind mounts). Auto-targeted: **prefer another registered user-data drive**
|
||||
**In-place file restore from the Tier-2 copy (C2, v0.100.0 — closes drill finding F2)** —
|
||||
`POST /backup/tier2/restore` (`backup.RestoreTier2Files`, `internal/backup/tier2_restore.go`) + the
|
||||
**"Fájlok visszaállítása"** button on the healthy Tier-2 layer row. **Additive-only** semantics
|
||||
(`rsyncRestoreMissing`: `rsync -a --ignore-existing`): files missing from the live `appdata/<stack>`
|
||||
(`rsyncRestoreMissing`: `rsync -a --ignore-existing`): files missing from the live resolved
|
||||
`appdata/<name>` dir (F-S2 — compose-derived, not the stack name)
|
||||
are copied back from the RECORDED Tier-2 copy; existing live files are **never overwritten** (a
|
||||
customer edit after the last copy wins) and **nothing is ever deleted** — this exactly serves the
|
||||
"I deleted my files" scenario with zero risk to newer data. Source = the recorded
|
||||
|
||||
Reference in New Issue
Block a user