feat: Docker volume backup, Tier 2 restore, restore dropdown fixes (v0.33.0)
- Add Docker named volume backup to Tier 1 (dump to tar, include in restic) and Tier 2 (copy tars to rsync mirror _volumes/ dir) - Fix volume name resolution: use project-prefixed names (mealie_mealie_data) - Fix double Tier 1 in restore dropdown: filter snapshots by app's home drive - Add Tier 2 restore: RestoreAppFromTier2() restores from rsync mirror - Show Tier 2 entry in restore dropdown when cross-drive backup succeeded - Add .fab import link in restore section - Volume-aware restore type banners and backup content labels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,11 +22,17 @@ type DBDumper interface {
|
||||
DumpStackDB(ctx context.Context, stackName string) error
|
||||
}
|
||||
|
||||
// VolumeDumper can dump Docker named volumes for a specific stack.
|
||||
type VolumeDumper interface {
|
||||
DumpAppVolumes(stackName string) error
|
||||
}
|
||||
|
||||
// CrossDriveRunner handles per-app backup to secondary storage.
|
||||
type CrossDriveRunner struct {
|
||||
sett *settings.Settings
|
||||
stackProvider StackDataProvider
|
||||
dbDumper DBDumper
|
||||
volDumper VolumeDumper
|
||||
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)
|
||||
@@ -56,6 +62,11 @@ func (r *CrossDriveRunner) SetDBDumper(d DBDumper) {
|
||||
r.dbDumper = d
|
||||
}
|
||||
|
||||
// SetVolumeDumper sets the volume dumper for pre-backup Docker volume dumps.
|
||||
func (r *CrossDriveRunner) SetVolumeDumper(d VolumeDumper) {
|
||||
r.volDumper = d
|
||||
}
|
||||
|
||||
// GetAppDrivePath returns the drive path for an app (HDD path or system data path fallback).
|
||||
func (r *CrossDriveRunner) GetAppDrivePath(stackName string) string {
|
||||
if hddPath := r.stackProvider.GetStackHDDPath(stackName); hddPath != "" {
|
||||
@@ -125,7 +136,16 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e
|
||||
}
|
||||
if err := r.dbDumper.DumpStackDB(ctx, stackName); err != nil {
|
||||
r.logger.Printf("[WARN] [backup] Pre-backup DB dump failed for %s: %v — proceeding with user data backup", stackName, err)
|
||||
// Non-fatal: user data backup is still valuable without fresh dump
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger fresh volume dump for this app before cross-drive backup
|
||||
if r.volDumper != nil {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] RunAppBackup: triggering pre-backup volume dump for %s", stackName)
|
||||
}
|
||||
if err := r.volDumper.DumpAppVolumes(stackName); err != nil {
|
||||
r.logger.Printf("[WARN] [backup] Pre-backup volume dump failed for %s: %v — proceeding with backup", stackName, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +461,16 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
// Non-fatal: user data is the primary concern
|
||||
}
|
||||
|
||||
// --- Copy volume dumps for this stack from its home drive ---
|
||||
volDestDir := filepath.Join(destDir, "_volumes")
|
||||
if err := os.MkdirAll(volDestDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating volume dump dest dir: %w", err)
|
||||
}
|
||||
if err := r.copyStackVolumeDumps(stackName, volDestDir); err != nil {
|
||||
r.logger.Printf("[WARN] [backup] Cross-drive volume dump copy failed for %s: %v", stackName, err)
|
||||
// Non-fatal: user data is the primary concern
|
||||
}
|
||||
|
||||
// --- Rsync app config (compose dir) ---
|
||||
if composePath, ok := r.stackProvider.GetStackComposePath(stackName); ok {
|
||||
configSrcDir := filepath.Dir(composePath)
|
||||
@@ -494,6 +524,37 @@ func (r *CrossDriveRunner) copyStackDBDumps(stackName, destDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyStackVolumeDumps copies Docker volume dump tars for the given stack from its home drive.
|
||||
func (r *CrossDriveRunner) copyStackVolumeDumps(stackName, destDir string) error {
|
||||
appDrive := r.GetAppDrivePath(stackName)
|
||||
dumpDir := AppVolumeDumpPath(appDrive, stackName)
|
||||
|
||||
entries, err := os.ReadDir(dumpDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading volume dump dir: %w", err)
|
||||
}
|
||||
|
||||
copied := 0
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".tar") {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(dumpDir, e.Name())
|
||||
dst := filepath.Join(destDir, e.Name())
|
||||
if err := copyFile(src, dst); err != nil {
|
||||
return fmt.Errorf("copying %s: %w", e.Name(), err)
|
||||
}
|
||||
copied++
|
||||
}
|
||||
if copied > 0 {
|
||||
r.logger.Printf("[INFO] [backup] Copied %d volume dump(s) for %s", copied, stackName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- infra backup ---
|
||||
|
||||
// syncInfraConfig rsyncs infrastructure config (stacks dir + controller.yaml) to all
|
||||
|
||||
Reference in New Issue
Block a user