v0.12.3 — Security & correctness bug fixes (33 bugs)
CRITICAL: 10 data race and security fixes — backup.go mutex coverage (C1-C4), IsSystemDisk 12-bit major/minor (C5), /dev/ path validation (C6), extractName traversal (C7), TargetPath/DestinationPath against registered paths (C8-C9), ParseComposeHDDMounts Clean-before-prefix (C10). HIGH: 17 logic/resource fixes — ValidateDump bufio.Scanner (H1), single appDirSize() with 30s timeout (H2/H3), snapshot ID regex (H4), cross-drive restic prune (H5), temp file order (H6), dirSizeBytes errors (H7), atomic fstab (H8), IsDeviceMounted suffix check (H9), eMMC partition mapping (H10), bytesCopied mutex (H11), separator-aware migrate prefix (H13), DeleteStack error on compose-down (H14), docker 60s timeout (H16), NotificationPrefs deep-copy (H17), wipefs warning (H18), fstab rollback on mount fail (H19). MEDIUM: 7 code quality fixes — formatBytes dedup (M1), .tmp filter order (M2), sizeBytes string type (M3), elapsed in message (M6), LoadLocation fallback (M7), pathCovers separator (M10), cancelEditLabel textContent (M11). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -227,27 +227,29 @@ func (r *CrossDriveRunner) runResticBackup(ctx context.Context, stackName, destB
|
||||
return fmt.Errorf("getting restic password: %w", err)
|
||||
}
|
||||
|
||||
// Write password to a temp file (restic requires --password-file or env var)
|
||||
// H6: Write password to temp file with safe cleanup order (close before deferred remove).
|
||||
pwFile, err := os.CreateTemp("", "felhom-crossdrive-pw-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating password file: %w", err)
|
||||
}
|
||||
defer os.Remove(pwFile.Name())
|
||||
pwPath := pwFile.Name()
|
||||
if _, err := pwFile.WriteString(password); err != nil {
|
||||
pwFile.Close()
|
||||
os.Remove(pwPath)
|
||||
return fmt.Errorf("writing password file: %w", err)
|
||||
}
|
||||
pwFile.Close()
|
||||
defer os.Remove(pwPath)
|
||||
|
||||
// Ensure repo is initialized
|
||||
if err := r.ensureResticRepo(ctx, repoPath, pwFile.Name()); err != nil {
|
||||
if err := r.ensureResticRepo(ctx, repoPath, pwPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run restic backup
|
||||
args := []string{
|
||||
"backup", "--repo", repoPath,
|
||||
"--password-file", pwFile.Name(),
|
||||
"--password-file", pwPath,
|
||||
"--tag", stackName,
|
||||
"--tag", "cross-drive",
|
||||
}
|
||||
@@ -258,6 +260,26 @@ func (r *CrossDriveRunner) runResticBackup(ctx context.Context, stackName, destB
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("restic backup failed: %v (%s)", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
// H5: Prune old snapshots to prevent unbounded accumulation.
|
||||
return r.pruneResticRepo(ctx, repoPath, pwPath)
|
||||
}
|
||||
|
||||
// pruneResticRepo forgets old snapshots in a cross-drive restic repo, keeping recent ones.
|
||||
func (r *CrossDriveRunner) pruneResticRepo(ctx context.Context, repoPath, pwPath string) error {
|
||||
args := []string{
|
||||
"forget", "--repo", repoPath,
|
||||
"--password-file", pwPath,
|
||||
"--keep-daily", "7",
|
||||
"--keep-weekly", "4",
|
||||
"--prune",
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "restic", args...)
|
||||
r.logger.Printf("[DEBUG] restic forget (prune): %s", repoPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
// Non-fatal: log warning but don't fail the backup
|
||||
r.logger.Printf("[WARN] restic forget failed for %s: %v (%s)", repoPath, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -294,13 +316,16 @@ func (r *CrossDriveRunner) updateStatus(stackName, status, errMsg string, durati
|
||||
}
|
||||
|
||||
// dirSizeBytes returns the total byte size of all files under path.
|
||||
// H7: Walk errors are now propagated instead of silently swallowed.
|
||||
func dirSizeBytes(path string) (int64, error) {
|
||||
var total int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
if err != nil {
|
||||
return err // propagate permission/IO errors
|
||||
}
|
||||
if !info.IsDir() {
|
||||
total += info.Size()
|
||||
}
|
||||
total += info.Size()
|
||||
return nil
|
||||
})
|
||||
return total, err
|
||||
|
||||
Reference in New Issue
Block a user