v0.14.1: Auto Tier 2 for small apps + infra config in cross-drive backup
- Auto-enable daily rsync Tier 2 for apps without HDD mounts when ≥2 storage paths exist (AutoEnableSmallApps) - Sync infrastructure config (stacks dir + controller.yaml) to all secondary destinations via _infra/ directory (syncInfraConfig) - Include infra paths in cross-drive restic snapshots - Add SecondaryInfraPath() helper to paths.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -132,7 +132,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Initialize cross-drive backup runner ---
|
// --- Initialize cross-drive backup runner ---
|
||||||
crossDriveRunner := backup.NewCrossDriveRunner(sett, stackProv, cfg.Paths.SystemDataPath, logger)
|
crossDriveRunner := backup.NewCrossDriveRunner(sett, stackProv, cfg.Paths.SystemDataPath, cfg.Paths.StacksDir, logger)
|
||||||
|
|
||||||
// Wire cross-drive → backup manager for pre-backup DB dumps
|
// Wire cross-drive → backup manager for pre-backup DB dumps
|
||||||
if backupMgr != nil {
|
if backupMgr != nil {
|
||||||
|
|||||||
@@ -26,17 +26,21 @@ type CrossDriveRunner struct {
|
|||||||
stackProvider StackDataProvider
|
stackProvider StackDataProvider
|
||||||
dbDumper DBDumper
|
dbDumper DBDumper
|
||||||
systemDataPath string // fallback drive for SSD-only apps
|
systemDataPath string // fallback drive for SSD-only apps
|
||||||
|
stacksDir string // path to stacks dir (for infra backup)
|
||||||
|
controllerYAMLPath string // path to controller.yaml (for infra backup)
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
running map[string]bool // per-app running state
|
running map[string]bool // per-app running state
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCrossDriveRunner creates a new CrossDriveRunner.
|
// NewCrossDriveRunner creates a new CrossDriveRunner.
|
||||||
func NewCrossDriveRunner(sett *settings.Settings, provider StackDataProvider, systemDataPath string, logger *log.Logger) *CrossDriveRunner {
|
func NewCrossDriveRunner(sett *settings.Settings, provider StackDataProvider, systemDataPath, stacksDir string, logger *log.Logger) *CrossDriveRunner {
|
||||||
return &CrossDriveRunner{
|
return &CrossDriveRunner{
|
||||||
sett: sett,
|
sett: sett,
|
||||||
stackProvider: provider,
|
stackProvider: provider,
|
||||||
systemDataPath: systemDataPath,
|
systemDataPath: systemDataPath,
|
||||||
|
stacksDir: stacksDir,
|
||||||
|
controllerYAMLPath: "/opt/docker/felhom-controller/controller.yaml",
|
||||||
logger: logger,
|
logger: logger,
|
||||||
running: make(map[string]bool),
|
running: make(map[string]bool),
|
||||||
}
|
}
|
||||||
@@ -146,6 +150,12 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e
|
|||||||
// RunAllScheduled runs cross-drive backups for all apps matching the schedule.
|
// RunAllScheduled runs cross-drive backups for all apps matching the schedule.
|
||||||
// Runs sequentially (disk I/O bound).
|
// Runs sequentially (disk I/O bound).
|
||||||
func (r *CrossDriveRunner) RunAllScheduled(ctx context.Context, schedule string) error {
|
func (r *CrossDriveRunner) RunAllScheduled(ctx context.Context, schedule string) error {
|
||||||
|
// Auto-enable Tier 2 for small apps (no HDD mounts) before running backups
|
||||||
|
r.AutoEnableSmallApps()
|
||||||
|
|
||||||
|
// Sync infrastructure config to all secondary destinations
|
||||||
|
r.syncInfraConfig(ctx)
|
||||||
|
|
||||||
configs := r.sett.GetAllCrossDriveConfigs()
|
configs := r.sett.GetAllCrossDriveConfigs()
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -345,6 +355,12 @@ func (r *CrossDriveRunner) runResticBackup(ctx context.Context, stackName, destB
|
|||||||
args = append(args, dumpDir)
|
args = append(args, dumpDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include infrastructure paths (same as primary restic)
|
||||||
|
args = append(args, r.stacksDir)
|
||||||
|
if _, err := os.Stat(r.controllerYAMLPath); err == nil {
|
||||||
|
args = append(args, r.controllerYAMLPath)
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "restic", args...)
|
cmd := exec.CommandContext(ctx, "restic", args...)
|
||||||
r.logger.Printf("[DEBUG] restic backup: %v", args)
|
r.logger.Printf("[DEBUG] restic backup: %v", args)
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
@@ -427,6 +443,107 @@ func (r *CrossDriveRunner) copyStackDBDumps(stackName, destDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- infra backup ---
|
||||||
|
|
||||||
|
// syncInfraConfig rsyncs infrastructure config (stacks dir + controller.yaml) to all
|
||||||
|
// secondary backup destinations. Runs once per RunAllScheduled cycle, before per-app backups.
|
||||||
|
func (r *CrossDriveRunner) syncInfraConfig(ctx context.Context) {
|
||||||
|
// Collect unique destination drives from enabled cross-drive configs
|
||||||
|
destDrives := make(map[string]bool)
|
||||||
|
for _, cfg := range r.sett.GetAllCrossDriveConfigs() {
|
||||||
|
if cfg.Enabled && cfg.DestinationPath != "" {
|
||||||
|
destDrives[cfg.DestinationPath] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(destDrives) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for dest := range destDrives {
|
||||||
|
infraDir := SecondaryInfraPath(dest)
|
||||||
|
if err := os.MkdirAll(infraDir, 0755); err != nil {
|
||||||
|
r.logger.Printf("[WARN] Cannot create infra backup dir %s: %v", infraDir, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rsync stacks dir → _infra/stacks/
|
||||||
|
stacksDest := filepath.Join(infraDir, "stacks") + "/"
|
||||||
|
if err := os.MkdirAll(stacksDest, 0755); err == nil {
|
||||||
|
stacksSrc := strings.TrimRight(r.stacksDir, "/") + "/"
|
||||||
|
cmd := exec.CommandContext(ctx, "rsync", "-a", "--delete", stacksSrc, stacksDest)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
r.logger.Printf("[WARN] Infra rsync (stacks) failed for %s: %v (%s)", dest, err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy controller.yaml → _infra/controller.yaml
|
||||||
|
if _, err := os.Stat(r.controllerYAMLPath); err == nil {
|
||||||
|
yamlDest := filepath.Join(infraDir, "controller.yaml")
|
||||||
|
data, err := os.ReadFile(r.controllerYAMLPath)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Printf("[WARN] Cannot read controller.yaml for infra backup: %v", err)
|
||||||
|
} else if err := os.WriteFile(yamlDest, data, 0644); err != nil {
|
||||||
|
r.logger.Printf("[WARN] Cannot write controller.yaml to %s: %v", yamlDest, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Printf("[INFO] Infrastructure config synced to %s", infraDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- auto-enable ---
|
||||||
|
|
||||||
|
// AutoEnableSmallApps auto-configures cross-drive backup for apps without HDD user data
|
||||||
|
// when at least 2 storage paths are registered. Apps with existing cross-drive config
|
||||||
|
// (even if disabled) are never modified.
|
||||||
|
func (r *CrossDriveRunner) AutoEnableSmallApps() {
|
||||||
|
storagePaths := r.sett.GetStoragePaths()
|
||||||
|
if len(storagePaths) < 2 {
|
||||||
|
return // no secondary drive available
|
||||||
|
}
|
||||||
|
|
||||||
|
deployed := r.stackProvider.ListDeployedStacks()
|
||||||
|
existingConfigs := r.sett.GetAllCrossDriveConfigs()
|
||||||
|
|
||||||
|
for _, stack := range deployed {
|
||||||
|
// Skip if already has cross-drive config (user has touched it)
|
||||||
|
if _, exists := existingConfigs[stack.Name]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if app has HDD mounts (large user data — needs manual config)
|
||||||
|
if mounts := r.stackProvider.GetStackHDDMounts(stack.Name); len(mounts) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find destination: first storage path that differs from the app's home drive
|
||||||
|
appDrive := r.getAppDrivePath(stack.Name)
|
||||||
|
var destPath string
|
||||||
|
for _, sp := range storagePaths {
|
||||||
|
if sp.Path != appDrive {
|
||||||
|
destPath = sp.Path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if destPath == "" {
|
||||||
|
continue // no suitable destination found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-configure daily rsync
|
||||||
|
cfg := &settings.CrossDriveBackup{
|
||||||
|
Enabled: true,
|
||||||
|
Method: "rsync",
|
||||||
|
DestinationPath: destPath,
|
||||||
|
Schedule: "daily",
|
||||||
|
}
|
||||||
|
if err := r.sett.SetCrossDriveConfig(stack.Name, cfg); err != nil {
|
||||||
|
r.logger.Printf("[WARN] Auto-enable Tier 2 failed for %s: %v", stack.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.logger.Printf("[INFO] Auto-enabled Tier 2 backup for %s → %s (no HDD mounts, daily rsync)", stack.Name, destPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
||||||
func (r *CrossDriveRunner) updateStatus(stackName, status, errMsg string, duration time.Duration, sizeHuman string) {
|
func (r *CrossDriveRunner) updateStatus(stackName, status, errMsg string, duration time.Duration, sizeHuman string) {
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ func SecondaryResticRepoPath(drivePath string) string {
|
|||||||
return filepath.Join(drivePath, "backups", "secondary", "restic")
|
return filepath.Join(drivePath, "backups", "secondary", "restic")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecondaryInfraPath returns the infrastructure config mirror directory on a drive's secondary backup.
|
||||||
|
func SecondaryInfraPath(drivePath string) string {
|
||||||
|
return filepath.Join(drivePath, "backups", "secondary", "_infra")
|
||||||
|
}
|
||||||
|
|
||||||
// AppDataDir returns the app data directory path on a drive.
|
// AppDataDir returns the app data directory path on a drive.
|
||||||
func AppDataDir(drivePath, stackName string) string {
|
func AppDataDir(drivePath, stackName string) string {
|
||||||
return filepath.Join(drivePath, "appdata", stackName)
|
return filepath.Join(drivePath, "appdata", stackName)
|
||||||
|
|||||||
Reference in New Issue
Block a user