fix(backup): 4 bug fixes from v0.14.1 code review (v0.14.2)
Bug 1 (HIGH): add --exclude _* to rsync --delete so _db/ and _config/ directories are never deleted between backup runs (crossdrive.go) Bug 2 (MEDIUM): refactor RunDBDumps/RunBackup/RunFullBackup to use acquireRunning/releaseRunning helpers; extract runDBDumpsInternal and runBackupInternal so all three public entry points set m.running and RunFullBackup no longer deadlocks calling the public methods (backup.go) Bug 3 (MEDIUM): log [WARN] when GetDiskUsage returns nil in ValidateDestination instead of silently skipping space checks (crossdrive.go) Bug 4 (MEDIUM): add [WARN] on empty SystemDataPath in NewManager; add [ERROR] in GetAppDrivePath; guard DumpStackDB against empty/relative paths (backup.go) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,9 @@ type BackupStatus struct {
|
||||
|
||||
// NewManager creates a new backup manager.
|
||||
func NewManager(cfg *config.Config, pinger *monitor.Pinger, sett *settings.Settings, logger *log.Logger) *Manager {
|
||||
if cfg.Paths.SystemDataPath == "" {
|
||||
logger.Printf("[WARN] SystemDataPath is empty in config — SSD-only apps will not have correct backup paths")
|
||||
}
|
||||
return &Manager{
|
||||
cfg: cfg,
|
||||
restic: NewResticManager(cfg, logger),
|
||||
@@ -150,6 +153,9 @@ func (m *Manager) GetAppDrivePath(stackName string) string {
|
||||
return hddPath
|
||||
}
|
||||
}
|
||||
if m.systemDataPath == "" {
|
||||
m.logger.Printf("[ERROR] systemDataPath is empty — cannot determine drive for %s", stackName)
|
||||
}
|
||||
return m.systemDataPath
|
||||
}
|
||||
|
||||
@@ -179,6 +185,15 @@ func (m *Manager) activeDrives() []string {
|
||||
|
||||
// RunDBDumps discovers and dumps all databases to per-drive, per-app paths.
|
||||
func (m *Manager) RunDBDumps(ctx context.Context) error {
|
||||
if err := m.acquireRunning(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.releaseRunning()
|
||||
return m.runDBDumpsInternal(ctx)
|
||||
}
|
||||
|
||||
// runDBDumpsInternal is the implementation of RunDBDumps. Caller must hold the running flag.
|
||||
func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
m.logger.Printf("[INFO] Starting database dump run")
|
||||
|
||||
@@ -270,6 +285,15 @@ func (m *Manager) RunDBDumps(ctx context.Context) error {
|
||||
|
||||
// RunBackup runs per-drive restic backup snapshots.
|
||||
func (m *Manager) RunBackup(ctx context.Context) error {
|
||||
if err := m.acquireRunning(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.releaseRunning()
|
||||
return m.runBackupInternal(ctx)
|
||||
}
|
||||
|
||||
// runBackupInternal is the implementation of RunBackup. Caller must hold the running flag.
|
||||
func (m *Manager) runBackupInternal(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
m.logger.Printf("[INFO] Starting restic backup (per-drive)")
|
||||
|
||||
@@ -452,27 +476,18 @@ func (m *Manager) RunIntegrityCheck(ctx context.Context) error {
|
||||
|
||||
// RunFullBackup runs DB dumps followed by restic backup.
|
||||
func (m *Manager) RunFullBackup(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
if m.running {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("backup already in progress")
|
||||
if err := m.acquireRunning(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.running = true
|
||||
m.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
m.running = false
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
defer m.releaseRunning()
|
||||
|
||||
// Step 1: DB dumps
|
||||
if err := m.RunDBDumps(ctx); err != nil {
|
||||
if err := m.runDBDumpsInternal(ctx); err != nil {
|
||||
m.logger.Printf("[WARN] DB dump had errors, continuing with backup anyway")
|
||||
}
|
||||
|
||||
// Step 2: Restic backup
|
||||
return m.RunBackup(ctx)
|
||||
return m.runBackupInternal(ctx)
|
||||
}
|
||||
|
||||
// GetStatus returns the current backup status.
|
||||
@@ -498,6 +513,24 @@ func (m *Manager) IsRunning() bool {
|
||||
return m.running
|
||||
}
|
||||
|
||||
// acquireRunning atomically sets the running flag. Returns error if already running.
|
||||
func (m *Manager) acquireRunning() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.running {
|
||||
return fmt.Errorf("backup already in progress")
|
||||
}
|
||||
m.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// releaseRunning clears the running flag.
|
||||
func (m *Manager) releaseRunning() {
|
||||
m.mu.Lock()
|
||||
m.running = false
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetResticPassword returns the restic repository encryption password.
|
||||
func (m *Manager) GetResticPassword() (string, error) {
|
||||
return m.restic.GetPassword()
|
||||
@@ -568,6 +601,9 @@ func (m *Manager) DumpStackDB(ctx context.Context, stackName string) error {
|
||||
}
|
||||
|
||||
drivePath := m.GetAppDrivePath(stackName)
|
||||
if drivePath == "" || !filepath.IsAbs(drivePath) {
|
||||
return fmt.Errorf("cannot determine absolute drive path for %s (systemDataPath not configured?)", stackName)
|
||||
}
|
||||
dumpDir := AppDBDumpPath(drivePath, stackName)
|
||||
|
||||
m.logger.Printf("[INFO] Running pre-backup DB dump for %s (%d database(s)) → %s", stackName, len(stackDBs), dumpDir)
|
||||
|
||||
Reference in New Issue
Block a user