package backup import ( "strings" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" ) // DeclaredDriveFileLegs answers ONE question, for the label and for the restore guard alike: which // of this app's own files live on the customer's DATA DRIVE rather than inside a Docker volume? // // R-537 / R-538 (measured 2026-09-16 on a fresh box). A Tier-1 recovery unit captures compose + // app.yaml + the DB dumps and volume tars that already exist beside it — `CaptureRecoveryUnit` has // no file-copy step at all, and `RecoveryManifest` has no field to record one. The whole-guest tiers // do not carry them either (`mp8 /mnt/felhom-drives` is a bind mount and vzdump logs // "excluding bind mount point mp8 … (not a volume)"). So for the four class-A apps the drive-side // paths are carried by Tier 2 and Tier 3 ONLY — which is the design (07-backup-architecture §6.2), // and is exactly why a page that says „Adatok" over a Tier-1 unit, or a restore that replays a // database over files it does not have, is a lie rather than a design choice. // // It returns the DECLARED mandatory paths, resolved but deliberately NOT stat-filtered. The filter // belongs to a capture (a declared path that is missing on disk is a capture gap, and // `offboxCaptureSet` warns about it there). Here the question is what the app CLAIMS to keep on the // drive, and an empty folder the customer has not filled yet must still count — otherwise the label // tells the truth today and starts lying the moment they use the app. // // Empty for: no stack provider, a legacy app with no `backup:` block (nothing declares a namespace // path — all 45 class-B apps), or an app with no resolvable HDD_PATH (undeployed). func (m *Manager) DeclaredDriveFileLegs(stack string) []string { if m.stackProvider == nil { return nil } binds, has := m.stackProvider.GetStackClassifiedBinds(stack) if !has { return nil } hdd := strings.TrimSpace(m.stackProvider.GetStackHDDPath(stack)) if hdd == "" { return nil } cs := appbackup.ComputeCaptureSet(binds, has, appbackup.TierOffsite, m.namespaceRoot(hdd), m.stackProvider.GetImportRoot()) out := make([]string, 0, len(cs.Paths)) for _, p := range cs.Paths { out = append(out, p.Abs) } if len(out) == 0 { return nil } return out } // HasDriveFileLegs is DeclaredDriveFileLegs as a predicate, for the surfaces that only need the // yes/no. Kept beside it so the two can never disagree. func (m *Manager) HasDriveFileLegs(stack string) bool { return len(m.DeclaredDriveFileLegs(stack)) > 0 }