controller v0.242.0: a removed app is listed with its kept backup; five small ones (R-487 R-491 R-490 R-489 R-476 R-456)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
R-487: the local backup lists are keyed on the drives, not on what is deployed — a removed app whose unit was kept is listed with the restore that reinstalls it, the picker answers for it, and the restore opens the unit where it sits. R-491: a removal clears the app's update hold. R-490: /api/system/info reaches the API router and reads the default storage path. R-489: volumes_removed is the real before/after difference, [] when none. R-476: a Tier-2 copy is dated by its data, not its manifest. R-456: the boot-orphan rule is pinned. Every fix red-proofed.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RemovedAppUnit is a recovery unit that sits on a registered drive while its app is NOT deployed —
|
||||
// the state „Töröld az adataimat is" leaves behind when the customer keeps the backups (R-487).
|
||||
//
|
||||
// It exists because the unit was restorable through POST /backup/restore the whole time and listed
|
||||
// on NEITHER backup page, so the customer's remove-by-mistake route existed only as an endpoint.
|
||||
// The off-site list had exactly this defect and was fixed by keying it on the STORE (R-237); the
|
||||
// local list is now keyed on the drives the same way — what is on disk decides, not what is deployed.
|
||||
type RemovedAppUnit struct {
|
||||
StackName string
|
||||
DisplayName string // from the unit's own manifest; the stack name when the manifest has none
|
||||
UnitDir string // the recovery-unit directory, backups/primary/<stack> on the drive it sits on
|
||||
DriveLabel string // registered storage label; the system-drive label for the SSD fallback
|
||||
Time string // RFC3339 UTC — newest artifact in the unit (same rule as ListRestorePoints)
|
||||
}
|
||||
|
||||
// primaryUnitRoots names every felhom-data namespace root a recovery unit can sit under: the system
|
||||
// data path and every registered storage path that is still connected. Deduplicated; a disconnected
|
||||
// drive is skipped — a unit nobody can open is not an offer (R-102's rule, one tier down).
|
||||
func (m *Manager) primaryUnitRoots() []string {
|
||||
seen := make(map[string]bool)
|
||||
var roots []string
|
||||
add := func(drive string) {
|
||||
if drive == "" || !filepath.IsAbs(drive) {
|
||||
return
|
||||
}
|
||||
root := m.namespaceRoot(drive)
|
||||
if root == "" || seen[root] {
|
||||
return
|
||||
}
|
||||
seen[root] = true
|
||||
roots = append(roots, root)
|
||||
}
|
||||
add(m.systemDataPath)
|
||||
if m.settings != nil {
|
||||
for _, sp := range m.settings.GetStoragePaths() {
|
||||
if sp.Disconnected {
|
||||
continue
|
||||
}
|
||||
add(sp.Path)
|
||||
}
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// driveLabelForRoot maps a namespace root back to the label the page shows for it.
|
||||
func (m *Manager) driveLabelForRoot(root string) string {
|
||||
if m.systemDataPath != "" && root == m.namespaceRoot(m.systemDataPath) {
|
||||
return systemDriveLabel
|
||||
}
|
||||
if m.settings != nil {
|
||||
for _, sp := range m.settings.GetStoragePaths() {
|
||||
if m.namespaceRoot(sp.Path) == root {
|
||||
return m.settings.GetStorageLabel(sp.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// unitNewestArtifact is the unit's data time: the newest of its manifest, .sql dumps and .tar
|
||||
// volume dumps. ONE rule, shared with ListRestorePoints, so the two lists cannot date a unit
|
||||
// differently.
|
||||
func unitNewestArtifact(unitDir string) (time.Time, bool) {
|
||||
fi, err := os.Stat(UnitManifestFile(unitDir))
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
newest := fi.ModTime()
|
||||
newest = newestArtifact(UnitDBDumpDir(unitDir), ".sql", newest)
|
||||
newest = newestArtifact(UnitVolumeDumpDir(unitDir), ".tar", newest)
|
||||
return newest, true
|
||||
}
|
||||
|
||||
// ListRemovedAppUnits walks backups/primary/ on every connected registered drive and returns the
|
||||
// units whose app is not deployed, sorted by stack name. A unit without a readable manifest is not
|
||||
// listed — the restore would fall back to the volume-only path, which is not the offer this row makes.
|
||||
// A nil provider lists nothing: with no provider "not deployed" cannot be told from "unknown", and an
|
||||
// offer to overwrite must fail closed (the isStackDeployed rule).
|
||||
func (m *Manager) ListRemovedAppUnits() []RemovedAppUnit {
|
||||
if m.stackProvider == nil {
|
||||
return nil
|
||||
}
|
||||
deployed := make(map[string]bool)
|
||||
for _, name := range m.knownStackNames() {
|
||||
deployed[name] = true
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var out []RemovedAppUnit
|
||||
for _, root := range m.primaryUnitRoots() {
|
||||
entries, err := os.ReadDir(PrimaryBackupPath(root))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if !e.IsDir() || deployed[name] || seen[name] {
|
||||
continue
|
||||
}
|
||||
unitDir := RecoveryUnitPath(root, name)
|
||||
man := readManifest(UnitManifestFile(unitDir))
|
||||
if man == nil {
|
||||
continue
|
||||
}
|
||||
newest, ok := unitNewestArtifact(unitDir)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
display := man.DisplayName
|
||||
if display == "" {
|
||||
display = name
|
||||
}
|
||||
seen[name] = true
|
||||
out = append(out, RemovedAppUnit{
|
||||
StackName: name,
|
||||
DisplayName: display,
|
||||
UnitDir: unitDir,
|
||||
DriveLabel: m.driveLabelForRoot(root),
|
||||
Time: newest.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].StackName < out[j].StackName })
|
||||
return out
|
||||
}
|
||||
|
||||
// RemovedAppUnitFor returns the removed app's unit, if one exists on a connected drive.
|
||||
func (m *Manager) RemovedAppUnitFor(stackName string) (RemovedAppUnit, bool) {
|
||||
for _, u := range m.ListRemovedAppUnits() {
|
||||
if u.StackName == stackName {
|
||||
return u, true
|
||||
}
|
||||
}
|
||||
return RemovedAppUnit{}, false
|
||||
}
|
||||
Reference in New Issue
Block a user