package backup import ( "fmt" "path/filepath" "regexp" ) // snapshotIDRe validates restic snapshot IDs: 8-64 lowercase hex characters. var snapshotIDRe = regexp.MustCompile(`^[0-9a-f]{8,64}$`) // RestoreApp restores an app from a restic snapshot. // All apps get config + DB dump restored. Apps with HDD data also get user data restored. func (m *Manager) RestoreApp(stackName, snapshotID string) error { if m.stackProvider == nil { return fmt.Errorf("stack provider not configured") } // Validate snapshot ID format if !snapshotIDRe.MatchString(snapshotID) { return fmt.Errorf("invalid snapshot ID: must be 8-64 lowercase hex characters") } // Prevent concurrent operations 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() }() // Determine what to restore hddMounts := m.stackProvider.GetStackHDDMounts(stackName) hasHDD := len(hddMounts) > 0 // Build list of paths to restore from the snapshot var restorePaths []string // Always restore the stack's config dir (compose + app.yaml + .felhom.yml) composePath, ok := m.stackProvider.GetStackComposePath(stackName) if ok { stackDir := filepath.Dir(composePath) restorePaths = append(restorePaths, stackDir) } // Restore DB dump files for this stack if m.cfg.Paths.DBDumpDir != "" { restorePaths = append(restorePaths, m.cfg.Paths.DBDumpDir) } // Restore HDD data (always included for apps that have it — backup is mandatory) if hasHDD { restorePaths = append(restorePaths, hddMounts...) } if len(restorePaths) == 0 { return fmt.Errorf("no restorable paths found for %s", stackName) } m.logger.Printf("[WARN] RESTORE starting: stack=%s, snapshot=%s, paths=%v, hasHDD=%v", stackName, snapshotID, restorePaths, hasHDD) // Stop the app before restore if err := m.stackProvider.StopStack(stackName); err != nil { m.logger.Printf("[WARN] RESTORE could not stop %s: %v (proceeding anyway)", stackName, err) } // Execute restore via restic if err := m.restic.RestoreAppData(snapshotID, restorePaths); err != nil { m.logger.Printf("[ERROR] RESTORE failed for %s: %v", stackName, err) if startErr := m.stackProvider.StartStack(stackName); startErr != nil { m.logger.Printf("[WARN] RESTORE could not restart %s after failure: %v", stackName, startErr) } return err } // Restart the app if err := m.stackProvider.StartStack(stackName); err != nil { m.logger.Printf("[WARN] RESTORE could not restart %s after restore: %v", stackName, err) } restoreType := "config+DB" if hasHDD { restoreType = "full (config+DB+userdata)" } m.logger.Printf("[INFO] RESTORE completed: stack=%s, snapshot=%s, type=%s", stackName, snapshotID, restoreType) return nil }