v0.14.0: Per-drive backup architecture + storage path overhaul
Major refactor of backup and storage paths: - Per-drive restic repos at <drive>/backups/primary/restic/ - Per-app DB dumps at <drive>/backups/primary/<app>/db-dumps/ - Remove global BackupDir, DBDumpDir, ResticRepo config fields - Add SystemDataPath config (fallback for apps without HDD) - New backup/paths.go with pure path computation helpers - Add GetStackHDDPath to StackDataProvider interface - Restic methods now accept repoPath as parameter - Cross-drive backup uses new secondary path structure - Rename storage/ to appdata/ in scripts and compose templates - Update protected HDD paths (storage → appdata + backups) - Simplify backup UI (remove global path displays) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,22 +22,23 @@ type DBDumper interface {
|
||||
|
||||
// CrossDriveRunner handles per-app backup to secondary storage.
|
||||
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
|
||||
sett *settings.Settings
|
||||
stackProvider StackDataProvider
|
||||
dbDumper DBDumper
|
||||
systemDataPath string // fallback drive for SSD-only apps
|
||||
logger *log.Logger
|
||||
mu sync.Mutex
|
||||
running map[string]bool // per-app running state
|
||||
}
|
||||
|
||||
// NewCrossDriveRunner creates a new CrossDriveRunner.
|
||||
func NewCrossDriveRunner(sett *settings.Settings, provider StackDataProvider, logger *log.Logger) *CrossDriveRunner {
|
||||
func NewCrossDriveRunner(sett *settings.Settings, provider StackDataProvider, systemDataPath string, logger *log.Logger) *CrossDriveRunner {
|
||||
return &CrossDriveRunner{
|
||||
sett: sett,
|
||||
stackProvider: provider,
|
||||
logger: logger,
|
||||
running: make(map[string]bool),
|
||||
sett: sett,
|
||||
stackProvider: provider,
|
||||
systemDataPath: systemDataPath,
|
||||
logger: logger,
|
||||
running: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +48,12 @@ 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
|
||||
// getAppDrivePath returns the drive path for an app.
|
||||
func (r *CrossDriveRunner) getAppDrivePath(stackName string) string {
|
||||
if hddPath := r.stackProvider.GetStackHDDPath(stackName); hddPath != "" {
|
||||
return hddPath
|
||||
}
|
||||
return r.systemDataPath
|
||||
}
|
||||
|
||||
// RunAppBackup runs cross-drive backup for a single app.
|
||||
@@ -128,7 +132,7 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e
|
||||
// Calculate backup size
|
||||
var sizeHuman string
|
||||
if cfg.Method == "rsync" {
|
||||
destDir := filepath.Join(cfg.DestinationPath, "backups", "rsync", stackName)
|
||||
destDir := AppSecondaryRsyncPath(cfg.DestinationPath, stackName)
|
||||
if sz, err := dirSizeBytes(destDir); err == nil {
|
||||
sizeHuman = humanizeBytes(sz)
|
||||
}
|
||||
@@ -220,7 +224,7 @@ func (r *CrossDriveRunner) ValidateDestination(path string) error {
|
||||
// --- rsync ---
|
||||
|
||||
func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBase string, mounts []string) error {
|
||||
destDir := filepath.Join(destBase, "backups", "rsync", stackName)
|
||||
destDir := AppSecondaryRsyncPath(destBase, stackName)
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating rsync dest dir: %w", err)
|
||||
}
|
||||
@@ -261,7 +265,7 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
}
|
||||
}
|
||||
|
||||
// --- Copy DB dumps for this stack ---
|
||||
// --- Copy DB dumps for this stack from its home drive ---
|
||||
dbDestDir := filepath.Join(destDir, "_db")
|
||||
if err := os.MkdirAll(dbDestDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating DB dump dest dir: %w", err)
|
||||
@@ -294,7 +298,7 @@ func (r *CrossDriveRunner) runRsyncBackup(ctx context.Context, stackName, destBa
|
||||
// --- restic ---
|
||||
|
||||
func (r *CrossDriveRunner) runResticBackup(ctx context.Context, stackName, destBase string, mounts []string) error {
|
||||
repoPath := filepath.Join(destBase, "backups", "restic")
|
||||
repoPath := SecondaryResticRepoPath(destBase)
|
||||
|
||||
// Get or create the cross-drive restic password
|
||||
password, err := r.sett.GetOrCreateCrossDrivePassword()
|
||||
@@ -334,11 +338,11 @@ func (r *CrossDriveRunner) runResticBackup(ctx context.Context, stackName, destB
|
||||
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)
|
||||
}
|
||||
// Include DB dump dir for this app (from its home drive)
|
||||
appDrive := r.getAppDrivePath(stackName)
|
||||
dumpDir := AppDBDumpPath(appDrive, stackName)
|
||||
if _, err := os.Stat(dumpDir); err == nil {
|
||||
args = append(args, dumpDir)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "restic", args...)
|
||||
@@ -387,27 +391,26 @@ 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.
|
||||
// copyStackDBDumps copies DB dump files for the given stack from its home drive.
|
||||
// DB dumps are at <drive>/backups/primary/<stack>/db-dumps/<stack>_<dbtype>.sql.
|
||||
func (r *CrossDriveRunner) copyStackDBDumps(stackName, destDir string) error {
|
||||
if r.dbDumpDir == "" {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(r.dbDumpDir)
|
||||
appDrive := r.getAppDrivePath(stackName)
|
||||
dumpDir := AppDBDumpPath(appDrive, stackName)
|
||||
|
||||
entries, err := os.ReadDir(dumpDir)
|
||||
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) {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(r.dbDumpDir, e.Name())
|
||||
src := filepath.Join(dumpDir, e.Name())
|
||||
dst := filepath.Join(destDir, e.Name())
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
@@ -453,4 +456,3 @@ func dirSizeBytes(path string) (int64, error) {
|
||||
})
|
||||
return total, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user