git » summer » commit 2dbb2cb

Support multiple roots

author Alberto Bertogli
2023-08-23 17:22:57 UTC
committer Alberto Bertogli
2023-08-23 17:22:57 UTC
parent 7722f1f489579fed98ba406023cf946f413d5e11

Support multiple roots

This patch adds support for multiple roots, so we can do things like
`summer -x update /home /etc /usr`.

For the purposes of cross-filesystem detection, each root is treated
independently.

summer.go +55 -27
test/help.t +25 -50
test/multiroot.t +59 -0

diff --git a/summer.go b/summer.go
index e7f19e1..479e77f 100644
--- a/summer.go
+++ b/summer.go
@@ -21,16 +21,19 @@ problems).  Not intended to detect malicious modification.
 
 Checksums are written to/read from each file's extended attributes.
 
+Paths given can be files or directories. If a directory is given, it is
+processed recursively.
+
 Usage:
 
-  summer [flags] update <dir>
-      Verify checksums in the given directory, and update them for new or
-      changed files.
-  summer [flags] verify <dir>
-      Verify checksums in the given directory.
-  summer [flags] generate <dir>
-      Write checksums for the given directory. Files with pre-existing
-      checksums are left untouched, and checksums are not verified.
+  summer [flags] update <paths>
+      Verify checksums in the given paths, and update them for new or changed
+      files.
+  summer [flags] verify <paths>
+      Verify checksums in the given paths.
+  summer [flags] generate <paths>
+      Write checksums for the given paths. Files with pre-existing checksums
+      are left untouched, and checksums are not verified.
       Useful when generating checksums for a lot of files for the first time,
       as is faster to resume work if interrupted.
   summer [flags] version
@@ -93,9 +96,12 @@ func main() {
 	}
 
 	op := flag.Arg(0)
-	root := flag.Arg(1)
+	roots := []string{}
+	if flag.NArg() > 1 {
+		roots = flag.Args()[1:]
+	}
 
-	if op != "version" && root == "" {
+	if op != "version" && len(roots) == 0 {
 		Usage()
 		os.Exit(1)
 	}
@@ -105,11 +111,11 @@ func main() {
 
 	switch op {
 	case "generate":
-		err = generate(root)
+		err = generate(roots)
 	case "verify":
-		err = verify(root)
+		err = verify(roots)
 	case "update":
-		err = update(root)
+		err = update(roots)
 	case "version":
 		PrintVersion()
 	default:
@@ -156,7 +162,7 @@ type ChecksumV1 struct {
 	ModTimeUsec int64
 }
 
-func openAndInfo(path string, d fs.DirEntry, err error, rootDev uint64) (bool, *os.File, fs.FileInfo, error) {
+func openAndInfo(path string, d fs.DirEntry, err error, rootDev deviceID) (bool, *os.File, fs.FileInfo, error) {
 	// Excluded check must come first, because it can be use to skip
 	// directories that would otherwise cause errors.
 	if isExcluded(path) {
@@ -195,11 +201,13 @@ func openAndInfo(path string, d fs.DirEntry, err error, rootDev uint64) (bool, *
 	return true, fd, info, nil
 }
 
-func getDevice(info fs.FileInfo) uint64 {
-	return info.Sys().(*syscall.Stat_t).Dev
+type deviceID uint64
+
+func getDevice(info fs.FileInfo) deviceID {
+	return deviceID(info.Sys().(*syscall.Stat_t).Dev)
 }
 
-func getDeviceForPath(path string) uint64 {
+func getDeviceForPath(path string) deviceID {
 	fi, err := os.Stat(path)
 	if err != nil {
 		// Doesn't matter, because we'll get an error during WalkDir.
@@ -208,8 +216,8 @@ func getDeviceForPath(path string) uint64 {
 	return getDevice(fi)
 }
 
-func generate(root string) error {
-	rootDev := getDeviceForPath(root)
+func generate(roots []string) error {
+	rootDev := deviceID(0)
 	p := NewProgress(options.isTTY)
 	defer p.Stop()
 
@@ -249,12 +257,18 @@ func generate(root string) error {
 		return nil
 	}
 
-	err := filepath.WalkDir(root, fn)
-	return err
+	for _, root := range roots {
+		rootDev = getDeviceForPath(root)
+		err := filepath.WalkDir(root, fn)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
-func verify(root string) error {
-	rootDev := getDeviceForPath(root)
+func verify(roots []string) error {
+	rootDev := deviceID(0)
 	p := NewProgress(options.isTTY)
 	defer p.Stop()
 
@@ -301,7 +315,14 @@ func verify(root string) error {
 		return nil
 	}
 
-	err := filepath.WalkDir(root, fn)
+	var err error
+	for _, root := range roots {
+		rootDev = getDeviceForPath(root)
+		err = filepath.WalkDir(root, fn)
+		if err != nil {
+			break
+		}
+	}
 
 	if p.corrupted > 0 && err == nil {
 		err = fmt.Errorf("detected %d corrupted files", p.corrupted)
@@ -309,8 +330,8 @@ func verify(root string) error {
 	return err
 }
 
-func update(root string) error {
-	rootDev := getDeviceForPath(root)
+func update(roots []string) error {
+	rootDev := deviceID(0)
 	p := NewProgress(options.isTTY)
 	defer p.Stop()
 
@@ -362,7 +383,14 @@ func update(root string) error {
 		return nil
 	}
 
-	err := filepath.WalkDir(root, fn)
+	var err error
+	for _, root := range roots {
+		rootDev = getDeviceForPath(root)
+		err = filepath.WalkDir(root, fn)
+		if err != nil {
+			break
+		}
+	}
 
 	if p.corrupted > 0 && err == nil {
 		err = fmt.Errorf("detected %d corrupted files", p.corrupted)
diff --git a/test/help.t b/test/help.t
index 4d5cea6..d3b11a1 100644
--- a/test/help.t
+++ b/test/help.t
@@ -12,16 +12,19 @@ No arguments.
   
   Checksums are written to/read from each file's extended attributes.
   
+  Paths given can be files or directories. If a directory is given, it is
+  processed recursively.
+  
   Usage:
   
-    summer [flags] update <dir>
-        Verify checksums in the given directory, and update them for new or
-        changed files.
-    summer [flags] verify <dir>
-        Verify checksums in the given directory.
-    summer [flags] generate <dir>
-        Write checksums for the given directory. Files with pre-existing
-        checksums are left untouched, and checksums are not verified.
+    summer [flags] update <paths>
+        Verify checksums in the given paths, and update them for new or changed
+        files.
+    summer [flags] verify <paths>
+        Verify checksums in the given paths.
+    summer [flags] generate <paths>
+        Write checksums for the given paths. Files with pre-existing checksums
+        are left untouched, and checksums are not verified.
         Useful when generating checksums for a lot of files for the first time,
         as is faster to resume work if interrupted.
     summer [flags] version
@@ -51,16 +54,19 @@ Too few arguments.
   
   Checksums are written to/read from each file's extended attributes.
   
+  Paths given can be files or directories. If a directory is given, it is
+  processed recursively.
+  
   Usage:
   
-    summer [flags] update <dir>
-        Verify checksums in the given directory, and update them for new or
-        changed files.
-    summer [flags] verify <dir>
-        Verify checksums in the given directory.
-    summer [flags] generate <dir>
-        Write checksums for the given directory. Files with pre-existing
-        checksums are left untouched, and checksums are not verified.
+    summer [flags] update <paths>
+        Verify checksums in the given paths, and update them for new or changed
+        files.
+    summer [flags] verify <paths>
+        Verify checksums in the given paths.
+    summer [flags] generate <paths>
+        Write checksums for the given paths. Files with pre-existing checksums
+        are left untouched, and checksums are not verified.
         Useful when generating checksums for a lot of files for the first time,
         as is faster to resume work if interrupted.
     summer [flags] version
@@ -82,40 +88,9 @@ Too few arguments.
 
 No valid path (the argument is given, but it is empty).
 
-  $ summer weifmws ""
-  # summer 🌞 🏖
-  
-  Utility to detect accidental data corruption (e.g. bitrot, storage media
-  problems).  Not intended to detect malicious modification.
-  
-  Checksums are written to/read from each file's extended attributes.
-  
-  Usage:
-  
-    summer [flags] update <dir>
-        Verify checksums in the given directory, and update them for new or
-        changed files.
-    summer [flags] verify <dir>
-        Verify checksums in the given directory.
-    summer [flags] generate <dir>
-        Write checksums for the given directory. Files with pre-existing
-        checksums are left untouched, and checksums are not verified.
-        Useful when generating checksums for a lot of files for the first time,
-        as is faster to resume work if interrupted.
-    summer [flags] version
-        Print software version information.
-  
-  Flags:
-    -exclude value
-      \texclude these paths (can be repeated) (esc)
-    -excludere value
-      \texclude paths matching this regexp (can be repeated) (esc)
-    -forcetty
-      \tforce TTY output (esc)
-    -n\tdry-run mode (do not write anything) (esc)
-    -q\tquiet mode (esc)
-    -v\tverbose mode (list each file) (esc)
-    -x\tdon't cross filesystem boundaries (esc)
+  $ summer verify ""
+  0s: 0 matched, 0 modified, 0 new, 0 corrupted
+  lstat : no such file or directory
   [1]
 
 
diff --git a/test/multiroot.t b/test/multiroot.t
new file mode 100644
index 0000000..4253d7a
--- /dev/null
+++ b/test/multiroot.t
@@ -0,0 +1,59 @@
+
+Tests for handling multiple roots on the same command invocation.
+
+  $ alias summer="$TESTDIR/../summer"
+  $ mkdir A B C
+  $ touch A/a1 A/a2 B/b1 C/c1
+
+  $ summer generate A B C
+  0s: 0 matched, 0 modified, 4 new, 0 corrupted
+  $ summer update A B C
+  0s: 4 matched, 0 modified, 0 new, 0 corrupted
+  $ summer verify A B C
+  0s: 4 matched, 0 modified, 0 new, 0 corrupted
+
+
+Test that individual files work well as roots (common use case).
+
+  $ touch B/b2
+  $ summer generate A/* B/* C/c1
+  0s: 0 matched, 0 modified, 1 new, 0 corrupted
+  $ summer update A/* B/* C/c1
+  0s: 5 matched, 0 modified, 0 new, 0 corrupted
+  $ summer verify A/* B/* C/c1
+  0s: 5 matched, 0 modified, 0 new, 0 corrupted
+
+
+Check the order is as expected.
+
+  $ summer -v update A B C
+  "A/a1": match \(checksum:0, mtime:\d+\) (re)
+  "A/a2": match \(checksum:0, mtime:\d+\) (re)
+  "B/b1": match \(checksum:0, mtime:\d+\) (re)
+  "B/b2": match \(checksum:0, mtime:\d+\) (re)
+  "C/c1": match \(checksum:0, mtime:\d+\) (re)
+  0s: 5 matched, 0 modified, 0 new, 0 corrupted
+
+
+Check how we handle getting an error in the middle.
+
+  $ chmod 0000 B/b1
+
+  $ summer -v verify A B C
+  "A/a1": match \(checksum:0, mtime:\d+\) (re)
+  "A/a2": match \(checksum:0, mtime:\d+\) (re)
+  0s: 2 matched, 0 modified, 0 new, 0 corrupted
+  open B/b1: permission denied
+  [1]
+
+  $ summer -v update A B C
+  "A/a1": match \(checksum:0, mtime:\d+\) (re)
+  "A/a2": match \(checksum:0, mtime:\d+\) (re)
+  0s: 2 matched, 0 modified, 0 new, 0 corrupted
+  open B/b1: permission denied
+  [1]
+
+  $ summer -v generate A B C
+  0s: 0 matched, 0 modified, 0 new, 0 corrupted
+  open B/b1: permission denied
+  [1]