Tier-2 engine rework: class-driven legs, v2 layout, NAS-target exclusion (Task 3b, v0.135.0)
tier2_capture.go: classified apps get TierSecondary per-bind legs (paperless copy shrinks — export
drops); legacy apps keep the byte-identical resolver set. v2 relpath-mirroring layout
(backups/secondary/<stack>/{marker LAST, recovery-unit/, hdd/<rel>/, userdata/<rel>/}); N>1 native
(errTier2MultiDir/tier2AppDataName deleted). Migration=delete-and-rebuild + reconcile; all RemoveAll
via tier2SafeRemove (refuses outside backups/secondary/). SSD=state-only tier. selectTier2Target
never picks network storage (pinned+auto, F-6C-1). Restore reads v2 behind a marker gate.
Part 0: offbox_enlarge_blocked is a persisted one-time Load seed (opt-out sticks), not a getter
append. Part 0.5: offsite restore scratch prefers a local (non-network) path.
Full v2 test suite + all 10 §10 red-proofs verified. Destructive writes bounded to backups/secondary/.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// Part 0.5 (F-3afix-1): the offsite restore scratch must prefer a LOCAL path — a network (squashed)
|
||||
// scratch would feed PlaceOffsiteRestore wrong-owner files. A network app-drive is skipped when a
|
||||
// local candidate exists; a network-only environment is a last resort WITH a loud WARN.
|
||||
func TestOffboxScratchDir_PrefersLocal(t *testing.T) {
|
||||
m, sett := newOffboxManager(t)
|
||||
prov := &offbox3aProvider{hdd: map[string]string{}, binds: map[string][]ClassifiedBind{}, has: map[string]bool{}}
|
||||
m.SetStackProvider(prov)
|
||||
|
||||
nas := "/mnt/nas"
|
||||
local := t.TempDir()
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: nas, Label: "nas", Schedulable: true, Kind: settings.StorageKindNetwork, Protocol: "nfs"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: local, Label: "local", Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prov.hdd["immich"] = nas // the app lives ON the NAS
|
||||
|
||||
_, nsRoot, err := m.offboxRestoreScratchDir("immich")
|
||||
if err != nil {
|
||||
t.Fatalf("scratchDir: %v", err)
|
||||
}
|
||||
if strings.Contains(nsRoot, "nas") {
|
||||
t.Errorf("network app-drive must not be the scratch — got %q, want the local drive", nsRoot)
|
||||
}
|
||||
if !strings.HasPrefix(nsRoot, filepath.Clean(local)) {
|
||||
t.Errorf("scratch nsRoot = %q, want under the local drive %q", nsRoot, local)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOffboxScratchDir_NetworkOnlyLastResortWarns(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
m, sett := newOffboxManager(t)
|
||||
m.logger = log.New(&buf, "", 0)
|
||||
prov := &offbox3aProvider{hdd: map[string]string{}, binds: map[string][]ClassifiedBind{}, has: map[string]bool{}}
|
||||
m.SetStackProvider(prov)
|
||||
|
||||
nas := "/mnt/nas"
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: nas, Label: "nas", Schedulable: true, Kind: settings.StorageKindNetwork, Protocol: "nfs"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prov.hdd["immich"] = nas
|
||||
|
||||
_, nsRoot, err := m.offboxRestoreScratchDir("immich")
|
||||
if err != nil {
|
||||
t.Fatalf("scratchDir: %v", err)
|
||||
}
|
||||
if !strings.Contains(nsRoot, "nas") {
|
||||
t.Errorf("network-only → the NAS is the last resort, got %q", nsRoot)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "ownership fidelity not guaranteed") {
|
||||
t.Errorf("last-resort network scratch must log a loud WARN, got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user