Make scan paths required operands; add -x/--one-file-system
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
scan now takes one or more PATH operands (directories or regular files) via cobra flags instead of the -root flag with its /srv default; invoking scan with no operand is a usage error and a nonexistent operand is fatal. Filesystem boundaries are crossed by default; the new -x/--one-file-system flag (GNU du/rsync convention) stops the walk at each operand's filesystem, implemented by comparing lstat device IDs with build-tagged helpers for darwin's int32 Dev. Verified against a real mounted disk image: default crosses, -x does not, --one-file-system is identical to -x.
This commit is contained in:
157
scan.go
157
scan.go
@@ -5,12 +5,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// chunk is the number of bytes hashed from each end of a file.
|
||||
@@ -31,63 +30,66 @@ type fileRec struct {
|
||||
mtime int64
|
||||
}
|
||||
|
||||
// runScan implements the scan subcommand: three sequential passes (walk,
|
||||
// stat, hash) over the tree under -root, emitting one NUL-terminated
|
||||
// record per regular file on stdout.
|
||||
func runScan(args []string) {
|
||||
fl := flag.NewFlagSet("scan", flag.ExitOnError)
|
||||
root := fl.String("root", "/srv", "directory tree to scan")
|
||||
workers := fl.Int("workers", runtime.NumCPU(),
|
||||
"concurrent workers for the stat and hash passes")
|
||||
// The flag set uses ExitOnError, so Parse cannot return a non-nil
|
||||
// error; the check keeps the error handled explicitly.
|
||||
err := fl.Parse(args)
|
||||
if err != nil {
|
||||
os.Exit(exitUsage)
|
||||
// runScan implements the scan subcommand: three sequential passes
|
||||
// (walk, stat, hash) over the trees named by the PATH operands,
|
||||
// emitting one NUL-terminated record per regular file on stdout. Flag
|
||||
// parsing and the at-least-one-operand check are done by cobra.
|
||||
func runScan(roots []string, workers int, oneFS bool) {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
|
||||
if fl.NArg() > 0 {
|
||||
fmt.Fprintf(os.Stderr, "scan: unexpected argument %q\n", fl.Arg(0))
|
||||
os.Exit(exitUsage)
|
||||
// A nonexistent operand is a fatal error before any scanning.
|
||||
for _, root := range roots {
|
||||
_, err := os.Lstat(root)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *workers < 1 {
|
||||
*workers = 1
|
||||
}
|
||||
|
||||
st, err := os.Stat(*root)
|
||||
if err != nil {
|
||||
fatalf("root %s: %v", *root, err)
|
||||
}
|
||||
|
||||
if !st.IsDir() {
|
||||
fatalf("root %s: not a directory", *root)
|
||||
}
|
||||
|
||||
paths, walkErrs := walkPass(*root)
|
||||
recs, statErrs := statPass(paths, *workers)
|
||||
emitted, hashErrs := hashPass(recs, *workers)
|
||||
paths, walkErrs := walkPass(roots, oneFS)
|
||||
recs, statErrs := statPass(paths, workers)
|
||||
emitted, hashErrs := hashPass(recs, workers)
|
||||
|
||||
skipped := walkErrs + statErrs + hashErrs
|
||||
fmt.Fprintf(os.Stderr, "scan: %d files emitted, %d skipped\n",
|
||||
emitted, skipped)
|
||||
}
|
||||
|
||||
// walkPass enumerates every regular file under root. It never follows
|
||||
// symlinks, never descends into directories named .zfs, and warns and
|
||||
// continues on any per-path error.
|
||||
func walkPass(root string) ([]string, int) {
|
||||
var (
|
||||
paths []string
|
||||
errs int
|
||||
)
|
||||
// treeWalker carries the walk-pass state shared by all PATH operands.
|
||||
type treeWalker struct {
|
||||
prog *progress
|
||||
oneFS bool
|
||||
paths []string
|
||||
errs int
|
||||
}
|
||||
|
||||
// walkPass enumerates every regular file under each root operand in
|
||||
// order. It never follows symlinks, never descends into directories
|
||||
// named .zfs, and warns and continues on any per-path error. With
|
||||
// oneFS set it never descends into a directory on a different
|
||||
// filesystem than its root operand.
|
||||
func walkPass(roots []string, oneFS bool) ([]string, int) {
|
||||
w := &treeWalker{prog: newProgress("walk", -1), oneFS: oneFS}
|
||||
|
||||
for _, root := range roots {
|
||||
w.walkRoot(root)
|
||||
}
|
||||
|
||||
w.prog.finish()
|
||||
|
||||
return w.paths, w.errs
|
||||
}
|
||||
|
||||
// walkRoot walks a single PATH operand, appending regular-file paths.
|
||||
func (w *treeWalker) walkRoot(root string) {
|
||||
rootDev, rootDevOK := deviceOf(root)
|
||||
|
||||
prog := newProgress("walk", -1)
|
||||
walkErr := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
errs++
|
||||
w.errs++
|
||||
|
||||
prog.warnf("walk %s: %v", p, err)
|
||||
w.prog.warnf("walk %s: %v", p, err)
|
||||
|
||||
if d != nil && d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
@@ -97,13 +99,7 @@ func walkPass(root string) ([]string, int) {
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
// ZFS snapshot pseudo-dirs would list every file
|
||||
// once per snapshot; never descend.
|
||||
if d.Name() == ".zfs" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
return w.dirAction(p, d, rootDev, rootDevOK)
|
||||
}
|
||||
// Regular files only: skip symlinks, sockets, FIFOs, and
|
||||
// device nodes.
|
||||
@@ -111,20 +107,65 @@ func walkPass(root string) ([]string, int) {
|
||||
return nil
|
||||
}
|
||||
|
||||
paths = append(paths, p)
|
||||
w.paths = append(w.paths, p)
|
||||
|
||||
prog.increment()
|
||||
w.prog.increment()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
prog.finish()
|
||||
|
||||
if walkErr != nil {
|
||||
fatalf("walk %s: %v", root, walkErr)
|
||||
}
|
||||
}
|
||||
|
||||
return paths, errs
|
||||
// dirAction decides whether the walk descends into directory p.
|
||||
func (w *treeWalker) dirAction(p string, d fs.DirEntry, rootDev uint64, rootDevOK bool) error {
|
||||
// ZFS snapshot pseudo-dirs would list every file once per
|
||||
// snapshot; never descend.
|
||||
if d.Name() == ".zfs" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if !w.oneFS || !rootDevOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
w.errs++
|
||||
|
||||
w.prog.warnf("walk %s: %v", p, err)
|
||||
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if dev, ok := deviceOfInfo(info); ok && dev != rootDev {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deviceOf returns the filesystem device ID of path without following
|
||||
// symlinks.
|
||||
func deviceOf(path string) (uint64, bool) {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return deviceOfInfo(fi)
|
||||
}
|
||||
|
||||
// deviceOfInfo extracts the filesystem device ID from a FileInfo, when
|
||||
// the platform exposes one.
|
||||
func deviceOfInfo(fi fs.FileInfo) (uint64, bool) {
|
||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return statDev(st), true
|
||||
}
|
||||
|
||||
// statPass lstats every collected path in a worker pool, recording size
|
||||
|
||||
Reference in New Issue
Block a user