v0.51.0: offsite-backup UI (felhom-pbs DR) + Model-A double-nest fix

- Backups page: whole-guest backup shown as real DR — target label "Biztonsági szerver –
  külön hardver (PBS)"; app-data "Távoli mentés" card now reflects the PBS offsite tier
  (guestBackupView.Offsite) instead of "nincs beállítva".
- Model-A double-nest fix: appbackup path helpers take a felhom-data NAMESPACE ROOT (no
  internal felhom-data join); backup.Manager.namespaceRoot/AppNamespaceRoot resolve
  HDD-vs-systemDataPath provenance so a drive-resident app's backups land single-nested
  (<drive>/backups/... on the guest = <drive>/felhom-data/backups/... on the host) instead
  of .../felhom-data/felhom-data/.... Writes, deletion (GetStackBackupData/RemoveStack/
  ProtectedHDDPaths), wipe-warning scan, and export updated coherently; legacy double-nest
  dirs kept protected. New appbackup test asserts no doubled segment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 20:26:52 +02:00
parent 1e82eebc5e
commit 63484a0bd4
12 changed files with 221 additions and 58 deletions
+30 -12
View File
@@ -10,22 +10,40 @@ import "path/filepath"
// FelhomDataDir is the namespace directory on storage drives for all felhom-managed data.
const FelhomDataDir = "felhom-data"
// PrimaryBackupPath returns the root primary backup directory for a drive.
func PrimaryBackupPath(drivePath string) string {
return filepath.Join(drivePath, FelhomDataDir, "backups", "primary")
// NamespaceRoot resolves the felhom-data namespace ROOT for a drive path. All the path helpers
// below take this namespace root (the directory that directly contains backups/ and appdata/),
// NOT a bare drive path — they do not append felhom-data themselves.
//
// Model A (slice 10): the host agent binds <drive>/felhom-data onto the guest mountpoint, so an
// enrolled user-data drive's IN-GUEST mount already IS the namespace root (and its basename need
// NOT be "felhom-data" — e.g. /mnt/felhom-usb). For such mounts pass inGuestDrive=true → the path
// is returned as-is, so callers no longer double-nest into .../felhom-data/felhom-data/... .
//
// For a bare drive root that still holds a felhom-data SUBDIR — the SSD-only system-data fallback,
// or any legacy host-side layout — pass inGuestDrive=false → the felhom-data segment is appended.
func NamespaceRoot(drivePath string, inGuestDrive bool) string {
if inGuestDrive {
return filepath.Clean(drivePath)
}
return filepath.Join(drivePath, FelhomDataDir)
}
// AppDBDumpPath returns the DB dump directory for an app on its home drive.
func AppDBDumpPath(drivePath, stackName string) string {
return filepath.Join(drivePath, FelhomDataDir, "backups", "primary", stackName, "db-dumps")
// PrimaryBackupPath returns the root primary backup directory under a felhom-data namespace root.
func PrimaryBackupPath(nsRoot string) string {
return filepath.Join(nsRoot, "backups", "primary")
}
// AppVolumeDumpPath returns the directory for Docker volume dump tars on an app's home drive.
func AppVolumeDumpPath(drivePath, stackName string) string {
return filepath.Join(drivePath, FelhomDataDir, "backups", "primary", stackName, "volume-dumps")
// AppDBDumpPath returns the DB dump directory for an app under a felhom-data namespace root.
func AppDBDumpPath(nsRoot, stackName string) string {
return filepath.Join(nsRoot, "backups", "primary", stackName, "db-dumps")
}
// AppDataDir returns the app data directory path on a drive.
func AppDataDir(drivePath, stackName string) string {
return filepath.Join(drivePath, FelhomDataDir, "appdata", stackName)
// AppVolumeDumpPath returns the Docker-volume dump-tar directory for an app under a namespace root.
func AppVolumeDumpPath(nsRoot, stackName string) string {
return filepath.Join(nsRoot, "backups", "primary", stackName, "volume-dumps")
}
// AppDataDir returns the app data directory under a felhom-data namespace root.
func AppDataDir(nsRoot, stackName string) string {
return filepath.Join(nsRoot, "appdata", stackName)
}
@@ -0,0 +1,64 @@
package appbackup
import (
"path/filepath"
"strings"
"testing"
)
// slash normalizes OS path separators so the assertions hold on both Linux (production) and Windows
// (dev). The double-nest invariant is about path segments, not the separator byte.
func slash(p string) string { return filepath.ToSlash(p) }
// TestNoDoubleFelhomDataForInGuestDrive asserts that for an enrolled in-guest user-data drive
// (Model A, slice 10 — the in-guest mount IS the felhom-data namespace root), none of the backup
// path helpers re-inject a felhom-data segment, so the on-host path is single-nested rather than
// the .../felhom-data/felhom-data/... double-nest that this fix removes.
func TestNoDoubleFelhomDataForInGuestDrive(t *testing.T) {
// An enrolled drive's in-guest mount — note the basename is NOT "felhom-data".
drive := filepath.FromSlash("/mnt/felhom-usb")
ns := NamespaceRoot(drive, true)
if ns != drive {
t.Fatalf("in-guest namespace root should be the drive mount as-is: got %q want %q", ns, drive)
}
paths := map[string]string{
"PrimaryBackupPath": PrimaryBackupPath(ns),
"AppDBDumpPath": AppDBDumpPath(ns, "nextcloud"),
"AppVolumeDumpPath": AppVolumeDumpPath(ns, "nextcloud"),
"AppDataDir": AppDataDir(ns, "nextcloud"),
}
for name, p := range paths {
sp := slash(p)
if strings.Contains(sp, FelhomDataDir+"/"+FelhomDataDir) {
t.Errorf("%s double-nests felhom-data: %q", name, sp)
}
if n := strings.Count(sp, FelhomDataDir); n > 1 {
t.Errorf("%s has %d felhom-data segments (want <=1): %q", name, n, sp)
}
if !strings.HasPrefix(sp, "/mnt/felhom-usb/") {
t.Errorf("%s not rooted at the drive mount: %q", name, sp)
}
}
// Concrete expected single-nested DB-dump path for a drive-resident app.
if got, want := slash(AppDBDumpPath(ns, "nextcloud")), "/mnt/felhom-usb/backups/primary/nextcloud/db-dumps"; got != want {
t.Errorf("drive-resident DB-dump path: got %q want %q", got, want)
}
}
// TestSystemFallbackNestsOnceUnderFelhomData asserts the SSD-only system-data fallback still nests
// under exactly one felhom-data segment (the bare drive root does NOT already contain the namespace).
func TestSystemFallbackNestsOnceUnderFelhomData(t *testing.T) {
ns := NamespaceRoot(filepath.FromSlash("/mnt/sys_drive"), false)
if want := filepath.Join(filepath.FromSlash("/mnt/sys_drive"), FelhomDataDir); ns != want {
t.Fatalf("system fallback namespace root: got %q want %q", ns, want)
}
got := slash(AppDBDumpPath(ns, "nextcloud"))
if want := "/mnt/sys_drive/felhom-data/backups/primary/nextcloud/db-dumps"; got != want {
t.Errorf("system fallback DB-dump path: got %q want %q", got, want)
}
if n := strings.Count(got, FelhomDataDir); n != 1 {
t.Errorf("system fallback should have exactly one felhom-data segment, got %d: %q", n, got)
}
}