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:
@@ -115,6 +115,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("/setup/scan", s.handleScan)
|
||||
mux.HandleFunc("/setup/scan/status", s.handleScanStatus)
|
||||
mux.HandleFunc("/setup/hub-restore", s.handleHubRestore)
|
||||
mux.HandleFunc("/setup/hub-restore/select", s.handleHubVersionSelect)
|
||||
mux.HandleFunc("/setup/restore", s.handleRestore)
|
||||
mux.HandleFunc("/setup/restore/status", s.handleRestoreStatus)
|
||||
mux.HandleFunc("/setup/fresh", s.handleFreshHub)
|
||||
@@ -233,7 +234,8 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
|
||||
switch source {
|
||||
case "local":
|
||||
drivePath := r.FormValue("drive_path")
|
||||
go s.executeLocalRestore(drivePath)
|
||||
historyFile := r.FormValue("history_file")
|
||||
go s.executeLocalRestore(drivePath, historyFile)
|
||||
case "hub":
|
||||
go s.executeHubRestore()
|
||||
default:
|
||||
@@ -372,10 +374,31 @@ func (s *Server) autoProcessHubRestore(w http.ResponseWriter, r *http.Request, c
|
||||
}
|
||||
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] Setup: hub recovery received — hasInfra=%v, configLen=%d", recovery.HasInfraBackup, len(recovery.ConfigYAML))
|
||||
s.logger.Printf("[DEBUG] Setup: hub recovery received — hasInfra=%v, configLen=%d, versions=%d", recovery.HasInfraBackup, len(recovery.ConfigYAML), len(recovery.BackupVersions))
|
||||
}
|
||||
|
||||
// Store recovery data in state for restore execution
|
||||
// If multiple versions available, show picker instead of auto-restoring
|
||||
if len(recovery.BackupVersions) > 1 && recovery.HasInfraBackup {
|
||||
s.logger.Printf("[INFO] Setup: %d backup versions available — showing version picker", len(recovery.BackupVersions))
|
||||
// Store config for later use after version selection
|
||||
s.state.SetFormField("hub_config_yaml", recovery.ConfigYAML)
|
||||
s.state.Save()
|
||||
|
||||
csrf := ensureCSRFToken(w, r)
|
||||
data := map[string]interface{}{
|
||||
"CSRF": csrf,
|
||||
"Versions": recovery.BackupVersions,
|
||||
}
|
||||
s.render(w, "setup_hub_versions", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Single version or no versions — proceed directly
|
||||
s.storeRecoveryAndRestore(w, r, recovery, customerID)
|
||||
}
|
||||
|
||||
// storeRecoveryAndRestore stores recovery data in state and starts the restore goroutine.
|
||||
func (s *Server) storeRecoveryAndRestore(w http.ResponseWriter, r *http.Request, recovery *report.RecoveryResponse, customerID string) {
|
||||
s.state.SelectedBackup = &SelectedBackup{
|
||||
Source: "hub",
|
||||
CustomerID: customerID,
|
||||
@@ -389,9 +412,8 @@ func (s *Server) autoProcessHubRestore(w http.ResponseWriter, r *http.Request, c
|
||||
s.state.SetStep("restore-exec")
|
||||
s.state.Save()
|
||||
|
||||
s.logger.Printf("[INFO] Setup: hub recovery received (hasInfra=%v) — starting restore", recovery.HasInfraBackup)
|
||||
s.logger.Printf("[INFO] Setup: hub recovery stored (hasInfra=%v) — starting restore", recovery.HasInfraBackup)
|
||||
|
||||
// Start the restore goroutine, then render the progress page
|
||||
go s.executeHubRestore()
|
||||
|
||||
csrf := ensureCSRFToken(w, r)
|
||||
@@ -476,34 +498,69 @@ func (s *Server) processHubRestore(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] Setup: hub recovery received — hasInfra=%v, configLen=%d", recovery.HasInfraBackup, len(recovery.ConfigYAML))
|
||||
s.logger.Printf("[DEBUG] Setup: hub recovery received — hasInfra=%v, configLen=%d, versions=%d", recovery.HasInfraBackup, len(recovery.ConfigYAML), len(recovery.BackupVersions))
|
||||
}
|
||||
|
||||
// Store recovery data in state for restore execution
|
||||
s.state.SelectedBackup = &SelectedBackup{
|
||||
Source: "hub",
|
||||
CustomerID: customerID,
|
||||
}
|
||||
s.state.SetFormField("retrieval_password", password)
|
||||
s.state.SetFormField("hub_config_yaml", recovery.ConfigYAML)
|
||||
if recovery.HasInfraBackup && recovery.InfraBackup != nil {
|
||||
ibJSON, _ := json.Marshal(recovery.InfraBackup)
|
||||
s.state.SetFormField("hub_infra_backup", string(ibJSON))
|
||||
s.state.SelectedBackup.Timestamp = recovery.InfraBackup.Timestamp
|
||||
|
||||
// If multiple versions available, show picker
|
||||
if len(recovery.BackupVersions) > 1 && recovery.HasInfraBackup {
|
||||
s.logger.Printf("[INFO] Setup: %d backup versions available — showing version picker", len(recovery.BackupVersions))
|
||||
s.state.SetFormField("hub_config_yaml", recovery.ConfigYAML)
|
||||
s.state.Save()
|
||||
|
||||
csrf := ensureCSRFToken(w, r)
|
||||
data := map[string]interface{}{
|
||||
"CSRF": csrf,
|
||||
"Versions": recovery.BackupVersions,
|
||||
}
|
||||
s.render(w, "setup_hub_versions", data)
|
||||
return
|
||||
}
|
||||
s.state.SetStep("restore-exec")
|
||||
s.state.Save()
|
||||
|
||||
s.logger.Printf("[INFO] Setup: hub recovery received (hasInfra=%v) — starting restore", recovery.HasInfraBackup)
|
||||
s.storeRecoveryAndRestore(w, r, recovery, customerID)
|
||||
}
|
||||
|
||||
// Start the restore goroutine, then render the progress page
|
||||
go s.executeHubRestore()
|
||||
|
||||
csrf := ensureCSRFToken(w, r)
|
||||
data := map[string]interface{}{
|
||||
"CSRF": csrf,
|
||||
// handleHubVersionSelect processes the user's version selection from the Hub version picker.
|
||||
func (s *Server) handleHubVersionSelect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Redirect(w, r, "/setup/hub-restore", http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.render(w, "setup_restore_exec", data)
|
||||
if !validateCSRF(r) {
|
||||
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
versionStr := r.FormValue("version_id")
|
||||
customerID := s.state.GetFormField("customer_id")
|
||||
password := s.state.GetFormField("retrieval_password")
|
||||
hubURL := DefaultHubURL
|
||||
|
||||
if customerID == "" || password == "" {
|
||||
http.Redirect(w, r, "/setup/hub-restore", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
var versionID int64
|
||||
fmt.Sscanf(versionStr, "%d", &versionID)
|
||||
|
||||
s.logger.Printf("[INFO] Setup: user selected backup version %d for %s", versionID, customerID)
|
||||
|
||||
// Fetch the specific version
|
||||
recovery, err := report.PullRecoveryVersion(hubURL, customerID, password, versionID)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Setup: failed to fetch version %d: %v", versionID, err)
|
||||
csrf := ensureCSRFToken(w, r)
|
||||
data := map[string]interface{}{
|
||||
"CSRF": csrf,
|
||||
"Error": fmt.Sprintf("Hiba a verzió letöltésekor: %v", err),
|
||||
}
|
||||
s.render(w, "setup_hub_versions", data)
|
||||
return
|
||||
}
|
||||
|
||||
s.storeRecoveryAndRestore(w, r, recovery, customerID)
|
||||
}
|
||||
|
||||
func (s *Server) processFreshHub(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -618,7 +675,7 @@ func (s *Server) processManual(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// --- Restore Execution ---
|
||||
|
||||
func (s *Server) executeLocalRestore(drivePath string) {
|
||||
func (s *Server) executeLocalRestore(drivePath, historyFile string) {
|
||||
s.restoreMu.Lock()
|
||||
s.restoreRunning = true
|
||||
s.restoreDone = false
|
||||
@@ -630,8 +687,14 @@ func (s *Server) executeLocalRestore(drivePath string) {
|
||||
}
|
||||
s.restoreMu.Unlock()
|
||||
|
||||
// Step 1: Read backup
|
||||
backupData, _, err := backup.ReadLocalInfraBackup(drivePath)
|
||||
// Step 1: Read backup (current or historical version)
|
||||
var backupData []byte
|
||||
var err error
|
||||
if historyFile != "" {
|
||||
backupData, _, err = backup.ReadLocalInfraBackupFromHistory(drivePath, historyFile)
|
||||
} else {
|
||||
backupData, _, err = backup.ReadLocalInfraBackup(drivePath)
|
||||
}
|
||||
if err != nil {
|
||||
s.setRestoreError(0, fmt.Sprintf("Mentés olvasási hiba: %v", err))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user