v0.12.6: fix rsync destination nesting and exclude app-internal DB dumps
Two rsync bugs found during Immich cross-drive backup testing: Fix 3: Simplified destination path structure in runRsyncBackup. Old SplitN logic kept "storage/immich" as a subpath, creating redundant nesting: backups/rsync/immich/storage/immich/<data>. New logic: single mount → rsync directly into the stack folder; multiple mounts → use each mount's leaf dir name as subfolder (disambiguated by _N suffix). Fix 4: Exclude app-internal DB dump files from rsync. Apps like Immich store periodic pg_dumps in <data>/backups/*.sql.gz. The controller already handles DB backups via pg_dump — copying them again wastes space. Added --exclude backups/*.sql.gz/sql/dump to the rsync command. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -203,20 +203,22 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
return fmt.Errorf("creating rsync dest dir: %w", err)
|
||||
}
|
||||
|
||||
for _, srcMount := range mounts {
|
||||
// Preserve directory structure: strip the storage path prefix to get relative subpath
|
||||
// e.g., /mnt/hdd_placeholder/storage/immich/ → storage/immich/
|
||||
rel := srcMount
|
||||
// Find the topmost non-root segment of the mount path (after the mount point itself)
|
||||
// Use a simple approach: keep everything from the first significant segment after /mnt/...
|
||||
parts := strings.SplitN(strings.TrimPrefix(srcMount, "/"), "/", 3)
|
||||
if len(parts) >= 3 {
|
||||
rel = parts[2] // e.g., "storage/immich"
|
||||
for i, srcMount := range mounts {
|
||||
var dstPath string
|
||||
if len(mounts) == 1 {
|
||||
// Single mount: rsync directly into the stack folder (no extra nesting)
|
||||
dstPath = destDir
|
||||
} else {
|
||||
rel = filepath.Base(srcMount)
|
||||
// Multiple mounts: use the leaf directory name as subfolder
|
||||
leaf := filepath.Base(srcMount)
|
||||
dstPath = filepath.Join(destDir, leaf)
|
||||
// Disambiguate duplicate leaf names (e.g. two mounts both named "data")
|
||||
if i > 0 {
|
||||
if _, err := os.Stat(dstPath); err == nil {
|
||||
dstPath = filepath.Join(destDir, fmt.Sprintf("%s_%d", leaf, i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(destDir, rel)
|
||||
if err := os.MkdirAll(dstPath, 0755); err != nil {
|
||||
return fmt.Errorf("creating rsync destination: %w", err)
|
||||
}
|
||||
@@ -225,7 +227,12 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
src := strings.TrimRight(srcMount, "/") + "/"
|
||||
dst := strings.TrimRight(dstPath, "/") + "/"
|
||||
|
||||
cmd := exec.CommandContext(ctx, "rsync", "-a", "--delete", src, dst)
|
||||
// Exclude app-internal DB dump files — the controller handles DB backups via pg_dump separately.
|
||||
cmd := exec.CommandContext(ctx, "rsync", "-a", "--delete",
|
||||
"--exclude", "backups/*.sql.gz",
|
||||
"--exclude", "backups/*.sql",
|
||||
"--exclude", "backups/*.dump",
|
||||
src, dst)
|
||||
r.logger.Printf("[DEBUG] rsync: %s → %s", src, dst)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("rsync failed for %s: %v (%s)", srcMount, err, strings.TrimSpace(string(out)))
|
||||
|
||||
Reference in New Issue
Block a user