Exit non-zero when snapshot create --skip-errors skips anything (closes #310)
check / check (push) Successful in 12m20s

A backup under --skip-errors printed its skip errors and failed count,
then "Finished successfully.", and exited 0, so a cron wrapper could
not tell it from a complete backup. A directory the walk could not list
was not counted at all.

Each entry the walk skips is now counted as examined and failed. When
anything failed under --skip-errors, the run keeps its snapshots, runs
the post-backup prune as before, and then returns an error that the
command reports as "Backup completed, but N file(s) failed to back up".

Judgement call: without --skip-errors a file removed during the run is
still counted as failed and the exit status stays 0.

Model: opus-5-5
This commit is contained in:
2026-10-14 02:43:50 +00:00
parent 65036bba1b
commit 2510c81dcf
8 changed files with 149 additions and 24 deletions
+15 -8
View File
@@ -141,10 +141,11 @@ type ScannerConfig struct {
// ScanResult contains the results of a scan operation. Files and bytes
// are counted per file: BytesScanned is the size of the new and changed
// files, BytesSkipped that of the unchanged ones. FilesFailed counts the
// new and changed files that phase 2 could not store; FilesScanned
// includes them and BytesScanned does not. ChunksCreated counts the
// chunks that were not already stored, and NewChunkSize is their total
// size.
// new and changed files that phase 2 could not store and, under
// --skip-errors, each entry the walk could not read, such as a directory
// it could not list; FilesScanned includes them and BytesScanned does
// not. ChunksCreated counts the chunks that were not already stored, and
// NewChunkSize is their total size.
type ScanResult struct {
FilesScanned int
FilesSkipped int
@@ -883,7 +884,7 @@ func (s *Scanner) scanPhase(
walkFn := func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return s.handleWalkError(filePath, err)
return s.handleWalkError(filePath, err, result)
}
// Check context cancellation
@@ -959,7 +960,7 @@ func (s *Scanner) recordSpecialEntry(
if info.Mode()&os.ModeSymlink != 0 {
file, err := s.buildSymlinkEntry(filePath, info)
if err != nil {
return true, s.handleWalkError(filePath, err)
return true, s.handleWalkError(filePath, err, result)
}
existingFiles[filePath] = struct{}{}
@@ -983,14 +984,20 @@ func (s *Scanner) recordSpecialEntry(
}
// handleWalkError deals with a filesystem error surfaced by the walk:
// skipped (with a UI notice) under --skip-errors, fatal otherwise.
func (s *Scanner) handleWalkError(filePath string, err error) error {
// under --skip-errors the entry is skipped with a UI notice and counted
// as examined and failed, otherwise the error is fatal.
func (s *Scanner) handleWalkError(
filePath string, err error, result *ScanResult,
) error {
if s.skipErrors {
log.Error("Failed to access file (skipping due to --skip-errors)",
"path", filePath, "error", err)
s.ui.Errorf("Failed to access %s: %v. Skipping (--skip-errors).",
s.ui.Path(filePath), err)
result.FilesScanned++
result.FilesFailed++
return nil // Continue scanning
}