Keep going past unreadable files with check --continue (closes #11)
check / check (push) Successful in 3m59s

With --continue, check stopped at the first file whose content or
checksum attribute it could not read, and at the first directory it
could not list. Each of these now counts as failed, its error, which
names the path, goes to stderr, and the walk goes on; the run still
exits non-zero. The walk shared with sum and clear takes the continue
setting, and they pass false. If the count that sizes the progress bar
cannot read a path, check --continue runs without the bar. Without
--continue the first such error still stops the run, and the summary
printed before it now counts an unreadable checksum attribute as
failed, as it already did for unreadable content.

Model: opus-5-5
This commit is contained in:
2026-10-06 00:56:34 +00:00
committed by sneak
parent 6a0dfa9f2c
commit a1764c17df
3 changed files with 78 additions and 10 deletions
+3
View File
@@ -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
+26 -10
View File
@@ -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)
+49
View File
@@ -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)
}
}