From 7e42ca64e5439466cbf66466a2e6af6a4d764695 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 5 Jul 2026 13:23:19 +0200 Subject: [PATCH] =?UTF-8?q?docs(controller):=20backup-architecture=20?= =?UTF-8?q?=E2=80=94=20class-C=20in-place=20file=20restore=20(C2/v0.100.0,?= =?UTF-8?q?=20additive-only=20semantics=20+=20reindex=20caveat)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6 --- documentation/controller/backup-architecture.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/documentation/controller/backup-architecture.md b/documentation/controller/backup-architecture.md index 03ba65a..940878c 100644 --- a/documentation/controller/backup-architecture.md +++ b/documentation/controller/backup-architecture.md @@ -69,6 +69,16 @@ Two keep-side restore entry points exist; neither does a whole-guest restore (th **The restore panel's data source (F1, v0.99.0).** `GET /api/backup/snapshots?stack=` is backed by `backup.Manager.ListRestorePoints` (`internal/backup/restore_points.go`). The route was a restic-era leftover the backups.html template still fetched; it was never registered, so the snapshot dropdown could never populate and the restore button never enabled (drill finding F1 — the whole keep-side restore UI was dead). It returns at most ONE entry — the current recovery unit (`time` = newest artifact mtime among manifest/db-dumps/volume-dumps, `short_id:"helyi"`, `tier:1`, `drive_label` from the storage registry). Tier-2 entries are never emitted: `POST /backup/restore` only reads the primary unit, so a tier-2 listing would silently restore tier-1 data while claiming tier-2. Guards: invalid/empty stack name → 400, unknown stack → 404, no unit yet → `ok:true, data:[]`. +**Class-C in-place file restore (C2, v0.100.0 — closes drill finding F2).** HDD bind-mount user files (`appdata/`) are outside both keep-side restore paths above (the drill's expected-negative). The customer path is now **`POST /backup/tier2/restore`** → `RestoreTier2Files` (`internal/backup/tier2_restore.go`), surfaced as the **"Fájlok visszaállítása"** button on the Tier-2 row. Semantics are deliberately **additive-only** (`rsync -a --ignore-existing` — `rsyncRestoreMissing`, the exec shape of `rsyncMirror` with the opposite-direction flags): + +- a file missing live is copied back from the recorded Tier-2 copy (attrs preserved); +- an existing live file is **never overwritten** — a customer edit after the last nightly copy always wins; +- **nothing is ever deleted** — `rsyncMirror`'s `--delete` in this direction would erase every file created since the last copy, which is why that helper must never be used tier2→live. + +The source is the RECORDED `CrossDriveBackup.DestinationPath` (never a fresh `selectTier2Target`, which could re-pick an empty drive). Refusals — no Tier-2 copy / never ran / copy dir gone / either drive disconnected / live drive decommissioned — happen before the app is stopped, with customer-readable Hungarian reasons; the flow is stop → copy → restart → health-wait, single-flight with backup/restore. Zero files copied is a success ("Nincs hiányzó fájl — minden fájl megvan a helyén."), and re-running the button is idempotent. + +**What it deliberately does NOT do:** no overwrite/point-in-time restore (corruption rollback stays with the offbox restore-to-verify + operator paths), no per-file selection, and it never touches `recovery-unit/` under the Tier-2 dir (backup artifacts are not user files). **App-reindex caveat:** the restore is filesystem-level — apps that index their data dir (e.g. Nextcloud → `occ files:scan`) may need a rescan before restored files appear in the app's own UI. + ### 4b. Security note — `.fab` import path validation (CTRL-001, v0.59.0) The portable `.fab` import (`internal/appexport/`) validates every manifest path segment before it reaches a `filepath.Join` against a trusted base (`appexport.ValidateSegment`, `validate.go:28`; `validateManifestPaths`, `validate.go:51`, called from `UnmarshalManifest`). The attacker-controllable `AppName` / `HDDSubdirs` / `VolumeNames` are rejected on any `..`, path separator, or absolute path, closing the v0.59.0 path-traversal finding. This is cross-referenced here; the detail lives in the appexport doc and the v0.59.0 audit record.