Exit non-zero when snapshot create --skip-errors skips anything it cannot read (closes #310)
check / check (push) Canceled after 0s

A backup under --skip-errors printed its skip errors, then "Finished
successfully.", and exited 0, so a cron wrapper could not tell it from
a complete backup.

Each skipped entry, including a directory the walk could not list, is
now counted as examined and failed. If --skip-errors skipped anything
it could not read or list, the run still keeps its snapshots and
prunes as before, then fails with "Backup completed, but N file(s)
could not be read". A file removed during the run, even before the
walk reaches it, counts once as failed and leaves the exit status
alone; without the flag it no longer stops the backup. Excluded
entries are never counted.

Model: opus-5-5
This commit was merged in pull request #381.
This commit is contained in:
clawbot committed 2026-10-14 07:13:48 +02:00
1 parent 16b03dfda5
commit 7a950ea2aa
8 files changed
+292 -33

No files matched your search

+38 -9
View File
@@ -39,6 +39,11 @@ var (
// caller can report it apart from a failed backup.
var ErrPostBackupPruneFailed = errors.New("post-backup prune failed")
// ErrFilesUnreadable is returned when --skip-errors skipped files or
// directories it could not read or list. The snapshot is complete and
// kept, so the caller can report it apart from a failed backup.
var ErrFilesUnreadable = errors.New("file(s) could not be read")
// listRecentLimit caps how many snapshot rows are fetched from the
// local index for listing and purge operations.
const listRecentLimit = 10000
@@ -130,12 +135,16 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error {
}
// Process each named snapshot
filesUnreadable := 0
for snapIdx, snapName := range snapshotNames {
err = v.createNamedSnapshot(
unreadable, err := v.createNamedSnapshot(
opts, hostname, snapName, snapIdx+1, len(snapshotNames))
if err != nil {
return err
}
filesUnreadable += unreadable
}
// Print overall summary if multiple snapshots
@@ -151,6 +160,19 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error {
}
}
return v.finishCreateSnapshot(filesUnreadable)
}
// finishCreateSnapshot ends a run whose snapshots are all complete and
// kept. Like finishRestore, it returns an error when --skip-errors
// skipped anything it could not read or list; otherwise it prints the
// closing line. A file removed during the run is counted as failed in
// the summary but is not an error, with or without --skip-errors.
func (v *Vaultik) finishCreateSnapshot(filesUnreadable int) error {
if filesUnreadable > 0 {
return fmt.Errorf("%d %w", filesUnreadable, ErrFilesUnreadable)
}
// Terminus must obey the --cron invariant: silent on total
// success only. UI.Complete is dropped in cron/quiet mode (that's
// the success path), but if any warnings fired during the run we
@@ -240,16 +262,22 @@ type snapshotStats struct {
totalBlobsUploaded int
uploadDuration time.Duration
// The part of totalFilesFailed that --skip-errors skipped because it
// could not be read or listed.
totalFilesUnreadable int
// The sizes of all blobs the snapshot references, set by
// finalizeSnapshotMetadata once snapshot_blobs is populated.
blobSize int64
blobUncompressedSize int64
}
// createNamedSnapshot creates a single named snapshot
// createNamedSnapshot creates a single named snapshot and returns the
// number of entries --skip-errors skipped because it could not read or
// list them.
func (v *Vaultik) createNamedSnapshot(
opts *SnapshotCreateOptions, hostname, snapName string, idx, total int,
) error {
) (int, error) {
snapshotStartTime := time.Now()
if total > 1 {
@@ -258,12 +286,12 @@ func (v *Vaultik) createNamedSnapshot(
resolvedDirs, err := v.resolveSnapshotPaths(snapName)
if err != nil {
return err
return 0, err
}
previousSnapshotID, err := v.previousSnapshotID(snapName)
if err != nil {
return err
return 0, err
}
scanner := v.ScannerFactory(snapshot.ScannerParams{
@@ -278,7 +306,7 @@ func (v *Vaultik) createNamedSnapshot(
snapshotID, err := v.SnapshotManager.CreateSnapshotWithName(
v.ctx, hostname, snapName, v.Globals.Version, v.Globals.Commit)
if err != nil {
return fmt.Errorf("creating snapshot: %w", err)
return 0, fmt.Errorf("creating snapshot: %w", err)
}
log.Info("Beginning snapshot", "snapshot_id", snapshotID, "name", snapName)
@@ -286,12 +314,12 @@ func (v *Vaultik) createNamedSnapshot(
stats, err := v.scanAllDirectories(scanner, resolvedDirs, snapshotID)
if err != nil {
return err
return 0, err
}
err = v.finalizeSnapshotMetadata(snapshotID, stats)
if err != nil {
return err
return 0, err
}
log.Info("Snapshot complete",
@@ -304,7 +332,7 @@ func (v *Vaultik) createNamedSnapshot(
v.printSnapshotSummary(snapshotID, snapshotStartTime, stats)
return nil
return stats.totalFilesUnreadable, nil
}
// previousSnapshotID returns the ID of the most recent completed snapshot
@@ -390,6 +418,7 @@ func (v *Vaultik) scanAllDirectories(
stats.totalBlobs += result.BlobsCreated
stats.totalFilesSkipped += result.FilesSkipped
stats.totalFilesFailed += result.FilesFailed
stats.totalFilesUnreadable += result.FilesUnreadable
stats.totalBytesSkipped += result.BytesSkipped
stats.totalFilesDeleted += result.FilesDeleted
stats.totalBytesDeleted += result.BytesDeleted