package backup import ( "fmt" "regexp" ) // snapshotIDRe validates restic snapshot IDs: 8-64 lowercase hex characters. var snapshotIDRe = regexp.MustCompile(`^[0-9a-f]{8,64}$`) // RestoreApp restores an app's HDD data from a restic snapshot. func (m *Manager) RestoreApp(stackName, snapshotID string) error { // Validate app has backup enabled if !m.settings.IsAppBackupEnabled(stackName) { return fmt.Errorf("backup not enabled for %s", stackName) } // Resolve HDD paths for this app if m.stackProvider == nil { return fmt.Errorf("stack provider not configured") } hddMounts := m.stackProvider.GetStackHDDMounts(stackName) if len(hddMounts) == 0 { return fmt.Errorf("no HDD data paths found for %s", stackName) } // H4: Validate snapshot ID format by regex instead of listing all snapshots (list caps at 100). // restic restore will return a clear error if the snapshot ID doesn't exist. if !snapshotIDRe.MatchString(snapshotID) { return fmt.Errorf("invalid snapshot ID: must be 8-64 lowercase hex characters") } // Use the running flag to prevent concurrent backup/restore m.mu.Lock() if m.running { m.mu.Unlock() return fmt.Errorf("backup or restore already in progress") } m.running = true m.mu.Unlock() defer func() { m.mu.Lock() m.running = false m.mu.Unlock() }() m.logger.Printf("[WARN] RESTORE starting: stack=%s, snapshot=%s, paths=%v", stackName, snapshotID, hddMounts) // Stop the app before restore to avoid data corruption if err := m.stackProvider.StopStack(stackName); err != nil { m.logger.Printf("[WARN] RESTORE could not stop %s before restore: %v (proceeding anyway)", stackName, err) } // Execute restore if err := m.restic.RestoreAppData(snapshotID, hddMounts); err != nil { m.logger.Printf("[ERROR] RESTORE failed for %s: %v", stackName, err) // Try to restart the app even on failure if startErr := m.stackProvider.StartStack(stackName); startErr != nil { m.logger.Printf("[WARN] RESTORE could not restart %s after failed restore: %v", stackName, startErr) } return err } // Restart the app after successful restore if err := m.stackProvider.StartStack(stackName); err != nil { m.logger.Printf("[WARN] RESTORE could not restart %s after restore: %v", stackName, err) } m.logger.Printf("[INFO] RESTORE completed: stack=%s, snapshot=%s", stackName, snapshotID) return nil }