package stacks import ( "path/filepath" "strings" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" ) // Backup classification for the samba stack (R-7 slice 1, [R4]). // // samba is CONTROLLER-GENERATED infra: it has no .felhom.yml and its compose binds are absolute // share paths, so the catalog path (LoadMetadata → ParseComposeClassifiableBinds → ClassifyBinds) // cannot classify it. Its classification comes from the SHARES REGISTRY instead: // // share.Offsite == true → ClassMandatory → TierOffsite (offsite) AND TierSecondary (tier-2) // share.Offsite == false → ClassOptional → TierSecondary only (never offsite) // // The smb.conf / passdb mounts are deliberately NOT emitted — they are config, not customer data. // // SCOPE NOTE (the reported design fork, see REPORT.md): making this seam correct does NOT by itself // put share data into a live tier-2/offsite run. Both engines are structured around a per-app // RECOVERY UNIT — backup.RunTier2 short-circuits on `os.Stat(unitDir)` before it ever calls // GetStackClassifiedBinds, and the offsite runner enumerates settings.GetOffboxApps(). A share-only // infra stack has neither, and teaching them about one is more than an enumeration tweak, so per the // task's STOP clause it was reported rather than improvised inside the engines. func (m *Manager) sambaClassifiedBinds() ([]appbackup.ClassifiedBind, bool) { if m.settings == nil { return nil, false } shares := m.settings.GetSMBShares() out := make([]appbackup.ClassifiedBind, 0, len(shares)) for _, sh := range shares { cls := appbackup.ClassOptional if sh.Offsite { cls = appbackup.ClassMandatory } out = append(out, appbackup.ClassifiedBind{ ComposeBind: appbackup.ComposeBind{ Root: appbackup.RootHDD, RelPath: m.shareRelPath(sh.Path), ReadOnly: sh.ReadOnly, }, Class: cls, Origin: appbackup.OriginExplicit, // the customer's per-share Felhőmentés toggle IS explicit }) } return out, true } // shareRelPath expresses a share's absolute path relative to its owning registered storage root, so // the emitted bind matches the (Root, RelPath) + root-path resolution shape the capture-set helpers // use. A share under no registered root falls back to the path minus its leading slash (never // absolute — the structural guards reject absolute RelPaths). func (m *Manager) shareRelPath(path string) string { p := filepath.ToSlash(path) if m.settings != nil { for _, sp := range m.settings.GetStoragePaths() { root := filepath.ToSlash(sp.Path) if strings.HasPrefix(p, root+"/") { return strings.TrimPrefix(p, root+"/") } } } return strings.TrimPrefix(p, "/") }