diff --git a/TODO.md b/TODO.md index f07fc05..5f97468 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,9 @@ today) with lint, fmt, fmt-check, check, and hooks targets. # Completed Steps +* 2026-10-06: `check --continue` keeps going past a file or directory + it cannot read: it counts it as failed, prints the error and the + path on stderr, and checks the rest of the tree * 2026-10-05: `make try` runs on three small files in a temporary directory that it removes afterwards, also when a step fails, instead of on a fixed directory on one person's machine diff --git a/attrsum.go b/attrsum.go index fce1043..a94527f 100644 --- a/attrsum.go +++ b/attrsum.go @@ -259,7 +259,7 @@ func newSumCmd(opts *options) *cobra.Command { } func processSumAdd(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { - return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { + return walkAndProcess(opts, dir, false, stats, bar, func(p string, info os.FileInfo, s *Stats) error { if hasXattr(p, checksumKey) { atomic.AddInt64(&s.FilesSkipped, 1) @@ -278,7 +278,7 @@ func processSumAdd(opts *options, dir string, stats *Stats, bar *progressbar.Pro } func processSumUpdate(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { - return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { + return walkAndProcess(opts, dir, false, stats, bar, func(p string, info os.FileInfo, s *Stats) error { t, err := readSumTime(p) if err != nil || info.ModTime().After(t) { werr := writeChecksumAndTime(opts, p, info, s) @@ -369,7 +369,7 @@ func newClearCmd(opts *options) *cobra.Command { } func processClear(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { - return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { + return walkAndProcess(opts, dir, false, stats, bar, func(p string, info os.FileInfo, s *Stats) error { cleared, err := clearOne(opts, p) if err != nil { atomic.AddInt64(&s.FilesFailed, 1) @@ -449,8 +449,10 @@ func runCheck(opts *options, args []string, cont bool) error { var bar *progressbar.ProgressBar if !opts.quiet { + // With --continue, a path the count cannot read leaves the run + // without a progress bar; the walk below reports it as failed. bar, err = countAndBar(opts, paths, "Verifying checksums") - if err != nil { + if err != nil && !cont { return err } } @@ -481,7 +483,7 @@ func processCheck(opts *options, dir string, cont bool, stats *Stats, bar *progr // Track initial failed count to detect failures during this walk. initialFailed := atomic.LoadInt64(&stats.FilesFailed) - err := walkAndProcess(opts, dir, stats, bar, func(p string, _ os.FileInfo, s *Stats) error { + err := walkAndProcess(opts, dir, cont, stats, bar, func(p string, _ os.FileInfo, s *Stats) error { return checkOne(opts, p, cont, s) }) if err != nil { @@ -505,7 +507,7 @@ func checkOne(opts *options, p string, cont bool, s *Stats) error { exp, err := xattr.Get(p, checksumKey) if err != nil { if !errors.Is(err, xattr.ENOATTR) { - return err + return unreadable(cont, s, err) } return missingChecksum(opts, p, cont, s) @@ -513,9 +515,7 @@ func checkOne(opts *options, p string, cont bool, s *Stats) error { act, bytesRead, err := fileMultihash(p) if err != nil { - atomic.AddInt64(&s.FilesFailed, 1) - - return err + return unreadable(cont, s, err) } ok := bytes.Equal(exp, act) @@ -551,6 +551,21 @@ func missingChecksum(opts *options, p string, cont bool, s *Stats) error { return errVerification } +// unreadable counts a file or directory that could not be read as failed. +// With --continue it prints err, which names the path, to stderr and +// returns nil so the walk goes on; otherwise it returns err. +func unreadable(cont bool, s *Stats, err error) error { + atomic.AddInt64(&s.FilesFailed, 1) + + if !cont { + return err + } + + log.Print(err) + + return nil +} + // reportCheck prints a per-file verification result when verbose output is on. func reportCheck(opts *options, p, actual string, ok bool) { if !opts.verbose || opts.quiet { @@ -650,6 +665,7 @@ func newProgressBar(total int64, description string) *progressbar.ProgressBar { func walkAndProcess( opts *options, root string, + cont bool, stats *Stats, bar *progressbar.ProgressBar, fn func(string, os.FileInfo, *Stats) error, @@ -658,7 +674,7 @@ func walkAndProcess( return filepath.Walk(root, func(p string, info os.FileInfo, err error) error { if err != nil { - return err + return unreadable(cont, stats, err) } skip, skipErr := walkSkip(opts, root, p, info) diff --git a/attrsum_test.go b/attrsum_test.go index 26d37a2..a3c7ce6 100644 --- a/attrsum_test.go +++ b/attrsum_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "path/filepath" "strings" @@ -271,3 +272,51 @@ func TestPermissionErrors(t *testing.T) { t.Fatalf("expected permission error on check, got nil") } } + +func TestCheckContinuePastUnreadable(t *testing.T) { + t.Parallel() + + opts := &options{} + dir := t.TempDir() + skipIfNoXattr(t, dir) + + writeFile(t, dir, "a.txt", "one") + secret := writeFile(t, dir, "b.txt", "two") + writeFile(t, dir, "c/d.txt", "three") + writeFile(t, dir, "e.txt", "four") + + err := processSumAdd(opts, dir, newTestStats(), nil) + if err != nil { + t.Fatalf("add: %v", err) + } + + // An unreadable file and an unlistable directory sit between the + // readable files a.txt and e.txt. + sub := filepath.Join(dir, "c") + + err = os.Chmod(secret, noPerm) + if err != nil { + t.Fatalf("chmod file: %v", err) + } + + defer func() { _ = os.Chmod(secret, filePerm) }() + + err = os.Chmod(sub, noPerm) + if err != nil { + t.Fatalf("chmod dir: %v", err) + } + + defer func() { _ = os.Chmod(sub, dirPerm) }() + + stats := newTestStats() + + err = processCheck(opts, dir, true, stats, nil) + if !errors.Is(err, errVerification) { + t.Fatalf("expected verification error, got %v", err) + } + + if stats.FilesProcessed != 2 || stats.FilesFailed != 2 { + t.Fatalf("expected 2 verified and 2 failed, got %d and %d", + stats.FilesProcessed, stats.FilesFailed) + } +}