refactor: extract app-data-backup into internal/appbackup (no behaviour change)
Extract the stateless, keep-side app-data backup primitives out of internal/backup/ into a new self-contained internal/appbackup/ package: - dbdump.go: DB dump discovery/execution (DiscoverDatabases, DumpOne, ...) - appdata.go: StackDataProvider + app-data/volume discovery, HumanizeBytes - paths.go: keep-side path helpers (AppDBDumpPath, AppVolumeDumpPath, AppDataDir) backup/ keeps every name available via type/const aliases + one-line function forwarders (appbackup_bridge.go), so the still-present delete-side code (restic, cross-drive, drive-mount) and the both-side consumers (web/api/report) compile unchanged. The keep-only consumers appexport and storage are rewired to import appbackup directly and no longer import backup. This is the Part-2 prerequisite for the Proxmox port: appbackup has zero references to restic/cross-drive/drive-mount and does not import backup, so the delete-side can later be removed without breaking app-data backup or appexport. Behaviour-preserving: pure move + import/qualifier rewrites, no logic edits. The four Manager methods (RunDBDumps/DumpAppVolumes/DumpAppVolumesSafe share the delete-side mutex/status state; RestoreAppFromTier2 reads the cross-drive mirror) intentionally stay on Manager and delegate to appbackup — for the re-platform step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package appbackup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// StackDataProvider provides stack data to the backup packages without circular imports.
|
||||
type StackDataProvider interface {
|
||||
GetStackComposePath(name string) (composePath string, ok bool)
|
||||
ListDeployedStacks() []StackSummary
|
||||
GetStackHDDMounts(name string) []string
|
||||
GetStackHDDPath(name string) string // raw HDD_PATH from app.yaml (empty if no HDD)
|
||||
GetDockerVolumes(name string) []string // full Docker volume names (project-prefixed)
|
||||
StopStack(name string) error
|
||||
StartStack(name string) error
|
||||
RefreshAndIsRunning(name string) bool
|
||||
}
|
||||
|
||||
// StackSummary holds minimal stack info needed for app data discovery.
|
||||
type StackSummary struct {
|
||||
Name string
|
||||
DisplayName string
|
||||
ComposePath string
|
||||
NeedsHDD bool
|
||||
HasVolumes 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
|
||||
HasVolumeData bool
|
||||
StorageLabel string // resolved from registered storage paths
|
||||
}
|
||||
|
||||
// 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.
|
||||
// All apps with HDD data are backed up automatically (mandatory — no opt-in).
|
||||
func DiscoverAppData(provider StackDataProvider, 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, path.SizeHuman = appDirSize(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)
|
||||
info.HasVolumeData = len(info.DockerVolumes) > 0
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// All apps with HDD data are backed up automatically (mandatory)
|
||||
info.BackupEnabled = info.HasHDDData
|
||||
|
||||
result = append(result, info)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] [backup] Discovered app data: %d apps", len(result))
|
||||
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
|
||||
}
|
||||
|
||||
// ResolveDockerVolumeNames returns full Docker volume names with the compose project prefix.
|
||||
// Docker Compose V2 creates volumes as <project>_<volumeName> where project = directory name.
|
||||
func ResolveDockerVolumeNames(composePath string) []string {
|
||||
vols := ParseComposeNamedVolumes(composePath)
|
||||
if len(vols) == 0 {
|
||||
return nil
|
||||
}
|
||||
project := filepath.Base(filepath.Dir(composePath))
|
||||
names := make([]string, 0, len(vols))
|
||||
for _, v := range vols {
|
||||
names = append(names, project+"_"+v.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// appDirSize returns the total byte count and a human-readable string for a directory.
|
||||
// H2/H3: Single du invocation with 30s timeout replaces two separate calls.
|
||||
func appDirSize(path string) (int64, string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "du", "-sb", path)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, "?"
|
||||
}
|
||||
fields := strings.Fields(string(output))
|
||||
if len(fields) == 0 {
|
||||
return 0, "?"
|
||||
}
|
||||
var size int64
|
||||
if n, _ := fmt.Sscanf(fields[0], "%d", &size); n != 1 {
|
||||
return 0, "?"
|
||||
}
|
||||
return size, humanizeBytes(size)
|
||||
}
|
||||
|
||||
// HumanizeBytes converts bytes to a human-readable string.
|
||||
// Exported so the backup package can forward to it (shared helper).
|
||||
func HumanizeBytes(b int64) string {
|
||||
return humanizeBytes(b)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user