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. // // EXECUTION (R-7b, v0.145.0 — the gap this note used to describe is CLOSED). Share data now reaches // BOTH live tiers, but not through this seam: the engines remain structured around a per-app recovery // unit (backup.RunTier2 still short-circuits on `os.Stat(unitDir)`; the offsite runner still // enumerates settings.GetOffboxApps()), and Model B′ deliberately left those paths byte-identical. // Instead the backup package runs a SIBLING shares source off the SAME registry this file reads — // backup.RunSharesTier2 and backup.runOffboxSharesLeg — applying the identical rule below. // // So this function and the shares sources agree by construction on WHAT each share's class is, while // the sources own WHERE it goes. Keep the rule here in sync with internal/backup/shares_payload.go's // classifiedShares if it ever changes. 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, "/") }