docs(controller): backup-architecture — class-C in-place file restore (C2/v0.100.0, additive-only semantics + reindex caveat)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -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=<app>` 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/<stack>`) 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.
|
||||
|
||||
Reference in New Issue
Block a user