package backup import ( "fmt" "os" "os/exec" "strings" "gopkg.in/yaml.v3" ) // StackDataProvider provides stack data to the backup package without circular imports. type StackDataProvider interface { GetStackComposePath(name string) (composePath string, ok bool) ListDeployedStacks() []StackSummary GetStackHDDMounts(name string) []string } // StackSummary holds minimal stack info needed for app data discovery. type StackSummary struct { Name string DisplayName string ComposePath string NeedsHDD bool } // AppBackupInfo holds backup-relevant data paths for a deployed app. type AppBackupInfo struct { StackName string DisplayName string NeedsHDD bool HDDPaths []AppDataPath HDDTotalSize int64 HDDSizeHuman string DockerVolumes []AppDockerVolume BackupEnabled bool HasHDDData bool HasDBDump bool } // AppDataPath represents a single HDD bind mount path. type AppDataPath struct { HostPath string Exists bool SizeHuman string SizeBytes int64 } // AppDockerVolume represents a named Docker volume. type AppDockerVolume struct { Name string Contains string } // DiscoverAppData discovers backup-relevant data for all deployed apps. func DiscoverAppData(provider StackDataProvider, hddPath string, backupPrefs map[string]bool, discoveredDBs []DiscoveredDB) []AppBackupInfo { if provider == nil { return nil } var result []AppBackupInfo for _, stack := range provider.ListDeployedStacks() { info := AppBackupInfo{ StackName: stack.Name, DisplayName: stack.DisplayName, NeedsHDD: stack.NeedsHDD, } // Discover HDD bind mounts via adapter hddMounts := provider.GetStackHDDMounts(stack.Name) for _, mount := range hddMounts { path := AppDataPath{HostPath: mount} if fi, err := os.Stat(mount); err == nil && fi.IsDir() { path.Exists = true path.SizeBytes = appDirSizeBytes(mount) path.SizeHuman = appDirSizeHuman(mount) } info.HDDPaths = append(info.HDDPaths, path) info.HDDTotalSize += path.SizeBytes } info.HDDSizeHuman = humanizeBytes(info.HDDTotalSize) info.HasHDDData = len(info.HDDPaths) > 0 // Discover Docker named volumes from compose info.DockerVolumes = parseComposeNamedVolumes(stack.ComposePath) // Check if app has a DB container (already backed up via DB dump) for _, db := range discoveredDBs { if db.StackName == stack.Name { info.HasDBDump = true break } } info.BackupEnabled = backupPrefs[stack.Name] // Only include apps that have some data to show if info.HasHDDData || len(info.DockerVolumes) > 0 { result = append(result, info) } } return result } // parseComposeNamedVolumes extracts named Docker volumes from a docker-compose.yml. func parseComposeNamedVolumes(composePath string) []AppDockerVolume { data, err := os.ReadFile(composePath) if err != nil { return nil } var compose struct { Volumes map[string]interface{} `yaml:"volumes"` } if err := yaml.Unmarshal(data, &compose); err != nil { return nil } var volumes []AppDockerVolume for name, cfg := range compose.Volumes { // Skip external volumes if cfgMap, ok := cfg.(map[string]interface{}); ok { if ext, ok := cfgMap["external"]; ok && ext == true { continue } } volumes = append(volumes, AppDockerVolume{Name: name}) } return volumes } // appDirSizeHuman returns a human-readable size string for a directory using du. func appDirSizeHuman(path string) string { cmd := exec.Command("du", "-sh", path) output, err := cmd.Output() if err != nil { return "?" } fields := strings.Fields(string(output)) if len(fields) > 0 { return fields[0] } return "?" } // appDirSizeBytes returns the total size in bytes for a directory. func appDirSizeBytes(path string) int64 { cmd := exec.Command("du", "-sb", path) output, err := cmd.Output() if err != nil { return 0 } fields := strings.Fields(string(output)) if len(fields) > 0 { var size int64 fmt.Sscanf(fields[0], "%d", &size) return size } return 0 } // humanizeBytes converts bytes to a human-readable string. func humanizeBytes(b int64) string { const ( KB = 1024 MB = KB * 1024 GB = MB * 1024 ) switch { case b >= GB: return fmt.Sprintf("%.1f GB", float64(b)/float64(GB)) case b >= MB: return fmt.Sprintf("%.1f MB", float64(b)/float64(MB)) case b >= KB: return fmt.Sprintf("%.1f KB", float64(b)/float64(KB)) default: return fmt.Sprintf("%d B", b) } }