v0.12.8: complete cross-drive backup + per-tier UI
- Cross-drive now copies DB dumps (_db/) and config (_config/) alongside user data - restic cross-drive includes config dir + full DB dump dir - UI: per-tier rows (1. mentés / 2. mentés) instead of per-layer (DB/Konfig/Data) - UI: BackupContents label shows what each tier protects (DB + Konfig + Adatok) - UI: rsync backups show browsable indicator (📁) - Cleanup: removed unused filterSnapshotsByPaths + pathCovers from router.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ type CrossDriveRunner struct {
|
||||
sett *settings.Settings
|
||||
stackProvider StackDataProvider
|
||||
dbDumper DBDumper
|
||||
dbDumpDir string // path to DB dump directory (e.g., /srv/backups/db-dumps)
|
||||
logger *log.Logger
|
||||
mu sync.Mutex
|
||||
running map[string]bool // per-app running state
|
||||
@@ -46,6 +47,11 @@ func (r *CrossDriveRunner) SetDBDumper(d DBDumper) {
|
||||
r.dbDumper = d
|
||||
}
|
||||
|
||||
// SetDBDumpDir sets the path to the DB dump directory for cross-drive backups.
|
||||
func (r *CrossDriveRunner) SetDBDumpDir(dir string) {
|
||||
r.dbDumpDir = dir
|
||||
}
|
||||
|
||||
// RunAppBackup runs cross-drive backup for a single app.
|
||||
func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) error {
|
||||
cfg := r.sett.GetCrossDriveConfig(stackName)
|
||||
@@ -258,6 +264,34 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
return fmt.Errorf("rsync failed for %s: %v (%s)", srcMount, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Copy DB dumps for this stack ---
|
||||
dbDestDir := filepath.Join(destDir, "_db")
|
||||
if err := os.MkdirAll(dbDestDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating DB dump dest dir: %w", err)
|
||||
}
|
||||
if err := r.copyStackDBDumps(stackName, dbDestDir); err != nil {
|
||||
r.logger.Printf("[WARN] Cross-drive DB 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)
|
||||
configDestDir := filepath.Join(destDir, "_config")
|
||||
if err := os.MkdirAll(configDestDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating config dest dir: %w", err)
|
||||
}
|
||||
src := strings.TrimRight(configSrcDir, "/") + "/"
|
||||
dst := strings.TrimRight(configDestDir, "/") + "/"
|
||||
cmd := exec.CommandContext(ctx, "rsync", "-a", "--delete", src, dst)
|
||||
r.logger.Printf("[DEBUG] rsync config: %s → %s", src, dst)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
r.logger.Printf("[WARN] Cross-drive config rsync failed for %s: %v (%s)", stackName, err, strings.TrimSpace(string(out)))
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -298,7 +332,18 @@ func (r *CrossDriveRunner) runResticBackup(ctx context.Context, stackName, destB
|
||||
"--tag", stackName,
|
||||
"--tag", "cross-drive",
|
||||
}
|
||||
// Include user data (HDD mounts)
|
||||
args = append(args, mounts...)
|
||||
// Include app config dir (compose + app.yaml + .felhom.yml)
|
||||
if composePath, ok := r.stackProvider.GetStackComposePath(stackName); ok {
|
||||
args = append(args, filepath.Dir(composePath))
|
||||
}
|
||||
// Include DB dump dir (all stacks' dumps — restic deduplicates)
|
||||
if r.dbDumpDir != "" {
|
||||
if _, err := os.Stat(r.dbDumpDir); err == nil {
|
||||
args = append(args, r.dbDumpDir)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "restic", args...)
|
||||
r.logger.Printf("[DEBUG] restic backup: %v", args)
|
||||
@@ -346,6 +391,43 @@ func (r *CrossDriveRunner) ensureResticRepo(ctx context.Context, repoPath, pwFil
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyStackDBDumps copies DB dump files for the given stack to destDir.
|
||||
// DB dump files are named <stackName>_<dbtype>.sql (e.g., immich_postgres.sql).
|
||||
// Small files — uses plain file copy, not rsync.
|
||||
func (r *CrossDriveRunner) copyStackDBDumps(stackName, destDir string) error {
|
||||
if r.dbDumpDir == "" {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(r.dbDumpDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading DB dump dir: %w", err)
|
||||
}
|
||||
prefix := stackName + "_"
|
||||
copied := 0
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasPrefix(e.Name(), prefix) {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(r.dbDumpDir, e.Name())
|
||||
dst := filepath.Join(destDir, e.Name())
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", e.Name(), err)
|
||||
}
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing %s: %w", e.Name(), err)
|
||||
}
|
||||
copied++
|
||||
}
|
||||
if copied > 0 {
|
||||
r.logger.Printf("[DEBUG] Copied %d DB dump file(s) to %s", copied, destDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func (r *CrossDriveRunner) updateStatus(stackName, status, errMsg string, duration time.Duration, sizeHuman string) {
|
||||
|
||||
Reference in New Issue
Block a user