feat: infra backup retention + version picker
Hub: GFS retention (7d/4w/3m, ~14 versions) in new infra_backup_versions table. Recovery endpoint supports ?version=ID. New /versions API endpoint. Dashboard shows backup history. Controller: local drive backups rotated into history/ (last 5 versions). Setup wizard shows version picker for Hub restores when multiple versions exist. Scan results enriched with app names, disk count, history badge. Local restore supports historical versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,15 +17,20 @@ import (
|
||||
|
||||
// DriveBackup represents a found infra backup on a drive.
|
||||
type DriveBackup struct {
|
||||
Device string `json:"device"`
|
||||
Label string `json:"label"`
|
||||
MountPoint string `json:"mount_point"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
CtrlVersion string `json:"controller_version"`
|
||||
IntegrityOK bool `json:"integrity_ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
WasTempMounted bool `json:"-"`
|
||||
Device string `json:"device"`
|
||||
Label string `json:"label"`
|
||||
MountPoint string `json:"mount_point"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
CtrlVersion string `json:"controller_version"`
|
||||
IntegrityOK bool `json:"integrity_ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
StackCount int `json:"stack_count"`
|
||||
StackNames []string `json:"stack_names,omitempty"`
|
||||
DiskCount int `json:"disk_count"`
|
||||
IsHistory bool `json:"is_history"`
|
||||
HistoryFile string `json:"history_file,omitempty"`
|
||||
WasTempMounted bool `json:"-"`
|
||||
}
|
||||
|
||||
// lsblkOutput represents the JSON output of lsblk.
|
||||
@@ -114,10 +119,8 @@ func ScanDrivesForInfraBackups(logger *log.Logger, debug bool) ([]DriveBackup, e
|
||||
continue
|
||||
}
|
||||
|
||||
result := scanPartition(part, mountedFS, logger)
|
||||
if result != nil {
|
||||
results = append(results, *result)
|
||||
}
|
||||
partResults := scanPartition(part, mountedFS, logger)
|
||||
results = append(results, partResults...)
|
||||
}
|
||||
|
||||
logger.Printf("[INFO] Setup: drive scan complete — found %d backup(s)", countValid(results))
|
||||
@@ -137,7 +140,7 @@ func CleanupTempMounts(results []DriveBackup, logger *log.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Logger) *DriveBackup {
|
||||
func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Logger) []DriveBackup {
|
||||
label := ""
|
||||
if part.Label != nil {
|
||||
label = *part.Label
|
||||
@@ -187,10 +190,12 @@ func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Lo
|
||||
return nil
|
||||
}
|
||||
|
||||
// Found backup — read and validate
|
||||
_, meta, err := backup.ReadLocalInfraBackup(mountPoint)
|
||||
var results []DriveBackup
|
||||
|
||||
result := &DriveBackup{
|
||||
// Read current backup
|
||||
backupData, meta, err := backup.ReadLocalInfraBackup(mountPoint)
|
||||
|
||||
current := DriveBackup{
|
||||
Device: part.Path,
|
||||
Label: label,
|
||||
MountPoint: mountPoint,
|
||||
@@ -198,24 +203,52 @@ func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Lo
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.IntegrityOK = false
|
||||
result.Error = err.Error()
|
||||
current.IntegrityOK = false
|
||||
current.Error = err.Error()
|
||||
if meta != nil {
|
||||
result.CustomerID = meta.CustomerID
|
||||
result.Timestamp = meta.Timestamp
|
||||
result.CtrlVersion = meta.ControllerVersion
|
||||
current.CustomerID = meta.CustomerID
|
||||
current.Timestamp = meta.Timestamp
|
||||
current.CtrlVersion = meta.ControllerVersion
|
||||
}
|
||||
} else {
|
||||
result.IntegrityOK = true
|
||||
result.CustomerID = meta.CustomerID
|
||||
result.Timestamp = meta.Timestamp
|
||||
result.CtrlVersion = meta.ControllerVersion
|
||||
current.IntegrityOK = true
|
||||
current.CustomerID = meta.CustomerID
|
||||
current.Timestamp = meta.Timestamp
|
||||
current.CtrlVersion = meta.ControllerVersion
|
||||
backup.ParseBackupCounts(backupData, ¤t.StackCount, ¤t.StackNames, ¤t.DiskCount)
|
||||
}
|
||||
|
||||
logger.Printf("[INFO] Setup: found infra backup on %s (%s) — customer=%s, integrity=%v",
|
||||
part.Path, label, result.CustomerID, result.IntegrityOK)
|
||||
results = append(results, current)
|
||||
|
||||
return result
|
||||
logger.Printf("[INFO] Setup: found infra backup on %s (%s) — customer=%s, integrity=%v",
|
||||
part.Path, label, current.CustomerID, current.IntegrityOK)
|
||||
|
||||
// Also scan history directory for older versions
|
||||
history := backup.ReadLocalInfraHistory(mountPoint)
|
||||
for _, hv := range history {
|
||||
hResult := DriveBackup{
|
||||
Device: part.Path,
|
||||
Label: label,
|
||||
MountPoint: mountPoint,
|
||||
CustomerID: hv.CustomerID,
|
||||
Timestamp: hv.Timestamp,
|
||||
CtrlVersion: hv.ControllerVersion,
|
||||
IntegrityOK: hv.IntegrityOK,
|
||||
Error: hv.Error,
|
||||
StackCount: hv.StackCount,
|
||||
StackNames: hv.StackNames,
|
||||
DiskCount: hv.DiskCount,
|
||||
IsHistory: true,
|
||||
HistoryFile: hv.HistoryFile,
|
||||
}
|
||||
results = append(results, hResult)
|
||||
}
|
||||
|
||||
if len(history) > 0 {
|
||||
logger.Printf("[INFO] Setup: found %d historical backup version(s) on %s", len(history), part.Path)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func readMountedFilesystems() map[string]string {
|
||||
|
||||
Reference in New Issue
Block a user