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 <michael.eischer@fau.de>
This commit is contained in:
Srigovind Nayak
2025-10-05 19:08:52 +05:30
committed by GitHub
parent 22f254c9ca
commit 481fcb9ca7
4 changed files with 41 additions and 11 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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) {