Files
felhom-controller/controller/internal/backup/r474_remove_mirrors.go
T
admin bdcbd50b42
gates / gates (push) Successful in 13s
controller v0.240.0: seven defects from the any-tier proof and the first nightly rotation
R-486 (P1): removing an app with its backups KEPT keeps its Tier-2 record,
so the second-drive restore is no longer refused over an intact mirror.
R-484: postgis/pgvector/timescaledb images are Postgres (logical dumps).
R-485: the backup card sizes the recovery unit and the mirror(s).
R-480: a held update's sentence leaves the card once the hold is lifted.
R-477: the update's off-site lookup is one snapshots call, no stats.
R-478: a copy older than this install's deploy does not count.
R-474: "delete backups" deletes the unit, the mirror(s) and the prefs.

Tests and red-proofs per row; evidence in felhom.eu
documentation/audits/v0240-2026-09-13/ and nightly-2026-09-13-adventurelog/.
2026-09-13 19:26:50 +02:00

96 lines
3.3 KiB
Go

package backup
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// R-474 (v0.240.0) — "delete backups" on removal deletes the app's Tier-2 mirror too.
//
// Measured three times on 2026-09-13: removing an app with `remove_backups:true` deleted only its
// db-dumps directory; the recovery unit, the volume tars and the Tier-2 mirror all survived, and a
// later reinstall of the same app then leaned on the old install's unit as its "fresh" restore point
// (R-478). The unit is inside the app's own backups base and RemoveStack deletes it; the MIRROR lives
// on another drive, outside that base, so it is removed here, with its own path check.
func validMirrorStackName(n string) bool {
return n != "" && n != "." && n != ".." && n != SharesPseudoStack && !strings.ContainsAny(n, `/\`)
}
// tier2MirrorRoots are the namespace roots a Tier-2 mirror of this app can live under: the recorded
// destination, every registered drive, and the system data path (a mirror outlives a changed target).
func (m *Manager) tier2MirrorRoots(stackName string) []string {
seen := map[string]bool{}
var roots []string
add := func(r string) {
if r == "" {
return
}
r = filepath.Clean(r)
if filepath.IsAbs(r) && !seen[r] {
seen[r] = true
roots = append(roots, r)
}
}
if m.settings != nil {
if cfg := m.settings.GetCrossDriveConfig(stackName); cfg != nil {
add(cfg.DestinationPath)
}
for _, sp := range m.settings.GetStoragePaths() {
if sp.Path != "" {
add(NamespaceRootFor(sp.Path, m.systemDataPath))
}
}
}
if m.systemDataPath != "" {
add(NamespaceRootFor(m.systemDataPath, m.systemDataPath))
}
return roots
}
// Tier2MirrorDirsForApp lists the app's Tier-2 mirror directories that exist now. Call it BEFORE the
// removal clears the app's cross-drive record, which is one of the places it looks.
func (m *Manager) Tier2MirrorDirsForApp(stackName string) []string {
if m == nil || !validMirrorStackName(stackName) {
return nil
}
var out []string
for _, root := range m.tier2MirrorRoots(stackName) {
d := filepath.Join(root, "backups", "secondary", stackName)
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
out = append(out, d)
}
}
return out
}
// RemoveTier2Mirrors deletes the given mirror directories, each only if it is exactly
// <root>/backups/secondary/<stackName> for one of the app's mirror roots — never another app's mirror,
// never the shares mirror, never a path that merely cleans to one. Returns "path (size)" per removal.
func (m *Manager) RemoveTier2Mirrors(stackName string, dirs []string) []string {
if m == nil || !validMirrorStackName(stackName) {
return nil
}
allowed := map[string]bool{}
for _, root := range m.tier2MirrorRoots(stackName) {
allowed[filepath.Join(root, "backups", "secondary", stackName)] = true
}
var removed []string
for _, d := range dirs {
if !allowed[d] {
m.logger.Printf("[WARN] [backup] remove %s: refusing to delete %q — not this app's Tier-2 mirror", stackName, d)
continue
}
size := humanizeBytes(dirSizeBytes(d))
if err := os.RemoveAll(d); err != nil {
m.logger.Printf("[ERROR] [backup] remove %s: deleting the Tier-2 mirror %s failed: %v", stackName, d, err)
continue
}
m.logger.Printf("[INFO] [backup] remove %s: Tier-2 mirror deleted: %s (%s)", stackName, d, size)
removed = append(removed, fmt.Sprintf("%s (%s)", d, size))
}
return removed
}