From 481fcb9ca78d009bea9b19aed6ac22f323838c57 Mon Sep 17 00:00:00 2001 From: Srigovind Nayak <5201843+konidev20@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:08:52 +0530 Subject: [PATCH] backup: return exit code 3 if not all targets are available (#5347) to make the exit code behaviour consistent with files inaccessible during the backup phase, making this change to exit with code 3 if not all target files/folders are accessible for backup --------- Co-authored-by: Michael Eischer --- changelog/unreleased/issue-4467 | 11 ++++++++++ cmd/restic/cmd_backup.go | 25 ++++++++++++++--------- cmd/restic/cmd_backup_integration_test.go | 13 +++++++++++- cmd/restic/cmd_backup_test.go | 3 +++ 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 changelog/unreleased/issue-4467 diff --git a/changelog/unreleased/issue-4467 b/changelog/unreleased/issue-4467 new file mode 100644 index 000000000..fe42657cb --- /dev/null +++ b/changelog/unreleased/issue-4467 @@ -0,0 +1,11 @@ +Bugfix: Exit with code 3 when some `backup` source files do not exist + +Restic used to exit with code 0 even when some backup sources did not exist. Restic +would exit with code 3 only when child directories or files did not exist. This +could cause confusion and unexpected behavior in scripts that relied on the exit +code to determine if the backup was successful. + +Restic now exits with code 3 when some backup sources do not exist. + +https://github.com/restic/restic/issues/4467 +https://github.com/restic/restic/pull/5347 \ No newline at end of file diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 6d6483b19..d0f7f849f 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -157,6 +157,9 @@ var backupFSTestHook func(fs fs.FS) fs.FS // ErrInvalidSourceData is used to report an incomplete backup var ErrInvalidSourceData = errors.New("at least one source file could not be read") +// ErrNoSourceData is used to report that no source data was found +var ErrNoSourceData = errors.Fatal("all source directories/files do not exist") + // filterExisting returns a slice of all existing items, or an error if no // items exist at all. func filterExisting(items []string, warnf func(msg string, args ...interface{})) (result []string, err error) { @@ -171,10 +174,12 @@ func filterExisting(items []string, warnf func(msg string, args ...interface{})) } if len(result) == 0 { - return nil, errors.Fatal("all source directories/files do not exist") + return nil, ErrNoSourceData + } else if len(result) < len(items) { + return result, ErrInvalidSourceData } - return + return result, nil } // readLines reads all lines from the named file and returns them as a @@ -437,12 +442,7 @@ func collectTargets(opts BackupOptions, args []string, warnf func(msg string, ar return nil, errors.Fatal("nothing to backup, please specify source files/dirs") } - targets, err = filterExisting(targets, warnf) - if err != nil { - return nil, err - } - - return targets, nil + return filterExisting(targets, warnf) } // parent returns the ID of the parent snapshot. If there is none, nil is @@ -496,9 +496,14 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter return err } + success := true targets, err := collectTargets(opts, args, printer.E, term.InputRaw()) if err != nil { - return err + if errors.Is(err, ErrInvalidSourceData) { + success = false + } else { + return err + } } timeStamp := time.Now() @@ -632,7 +637,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter arch.SelectByName = selectByNameFilter arch.Select = selectFilter arch.WithAtime = opts.WithAtime - success := true + arch.Error = func(item string, err error) error { success = false reterr := progressReporter.Error(item, err) diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index ff58420cd..d8b766d8c 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/restic/restic/internal/data" + "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" @@ -271,7 +272,17 @@ func TestBackupNonExistingFile(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, "", dirs, opts, env.gopts) + // mix of existing and non-existing files + err := testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts) + rtest.Assert(t, err != nil, "expected error for non-existing file") + rtest.Assert(t, errors.Is(err, ErrInvalidSourceData), "expected ErrInvalidSourceData; got %v", err) + // only non-existing file + dirs = []string{ + filepath.Join(p, "nonexisting"), + } + err = testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts) + rtest.Assert(t, err != nil, "expected error for non-existing file") + rtest.Assert(t, errors.Is(err, ErrNoSourceData), "expected ErrNoSourceData; got %v", err) } func TestBackupSelfHealing(t *testing.T) { diff --git a/cmd/restic/cmd_backup_test.go b/cmd/restic/cmd_backup_test.go index b607532b4..e6f00aa5d 100644 --- a/cmd/restic/cmd_backup_test.go +++ b/cmd/restic/cmd_backup_test.go @@ -71,6 +71,9 @@ func TestCollectTargets(t *testing.T) { rtest.OK(t, err) sort.Strings(targets) rtest.Equals(t, expect, targets) + + _, err = collectTargets(opts, []string{filepath.Join(dir, "cmdline arg"), filepath.Join(dir, "non-existing-file")}, t.Logf, nil) + rtest.Assert(t, err == ErrInvalidSourceData, "expected error when not all targets exist") } func TestReadFilenamesRaw(t *testing.T) {