From b169f0a9dd35a7381774eb176d4badf64d403560 Mon Sep 17 00:00:00 2001 From: es80 <49912978+es80@users.noreply.github.com> Date: Tue, 3 Dec 2019 21:54:52 +0000 Subject: [PATCH] Adding a -recursive flag (#15) added -recursive flag, can pass directories cshatag can now be passed directories as command-line arguments. os.Exit is only ever called from main so that the program always attempts to process all files before exiting. Also, recursive mode now just skips non-regular files without producing an error. --- README.md | 37 ++++++++++--------- check.go | 33 +++++++++-------- cshatag.1 | 26 +++++++------- main.go | 90 ++++++++++++++++++++++++++++++++++++++-------- tests/.gitignore | 1 + tests/6.expected | 1 + tests/7.expected | 3 ++ tests/run_tests.sh | 18 ++++++++++ 8 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 tests/6.expected create mode 100644 tests/7.expected diff --git a/README.md b/README.md index f85d390..47c4330 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ NAME cshatag - compiled shatag SYNOPSIS - cshatag [OPTIONS] FILE [FILE2...] + cshatag [OPTIONS] FILE [FILE...] DESCRIPTION cshatag is a minimal and fast re-implementation of shatag @@ -31,41 +31,44 @@ DESCRIPTION only mtime has changed, checksum stayed the same mtime stayed the same but checksum changed - cshatag aims to be format-compatible with shatag and uses the same - extended attributes (see the COMPATIBILITY section). + cshatag aims to be format-compatible with shatag and uses the same ex‐ + tended attributes (see the COMPATIBILITY section). cshatag was written in C in 2012 and has been rewritten in Go in 2019. OPTIONS - -remove remove cshatag's xattrs from FILE - -q quiet mode - don't report files - -qq quiet2 mode - only report files and errors + -recursive recursively process the contents of directories + -remove remove cshatag's xattrs from FILE + -q quiet mode - don't report files + -qq quiet2 mode - only report files and errors EXAMPLES - Typically, cshatag will be called from find: - # find . -xdev -type f -print0 | xargs -0 cshatag > cshatag.log - Errors like corrupt files will then be printed to stderr or grep for + Check all regular files in the file tree below the current working di‐ + rectory: + # cshatag -recursive . > cshatag.log + Errors like corrupt files will then be printed to stderr or grep for "corrupt" in cshatag.log. To remove the extended attributes from all files: - # find . -xdev -type f -print0 | xargs -0 cshatag -remove + # cshatag -recursive -remove . RETURN VALUE 0 Success 1 Wrong number of arguments - 2 File could not be opened - 3 File is not a regular file - 4 Extended attributs could not be written to file - 5 File is corrupt + 2 One or more files could not be opened + 3 One or more files is not a regular file + 4 Extended attributes could not be written to one or more files + 5 At least one file was found to be corrupt + 6 More than one type of error occurred COMPATIBILITY - cshatag writes the user.shatag.ts field with full integer nanosecond + cshatag writes the user.shatag.ts field with full integer nanosecond precision, while python uses a double for the whole mtime and loses the last few digits. AUTHOR - Jakob Unterwurzacher , - https://github.com/rfjakob/cshatag + Jakob Unterwurzacher , https://github.com/rf‐ + jakob/cshatag COPYRIGHT Copyright 2012 Jakob Unterwurzacher. MIT License. diff --git a/check.go b/check.go index 9db6ca9..e065358 100644 --- a/check.go +++ b/check.go @@ -62,14 +62,10 @@ func getStoredAttr(f *os.File) (attr fileAttr, err error) { } // getMtime reads the actual modification time of file "f" from disk. -func getMtime(f *os.File) (ts fileTimestamp) { +func getMtime(f *os.File) (ts fileTimestamp, err error) { fi, err := f.Stat() if err != nil { - fmt.Fprintln(os.Stderr, err) - } - if !fi.Mode().IsRegular() { - fmt.Printf("Error: %q is not a regular file\n", f.Name()) - os.Exit(3) + return } ts.s = uint64(fi.ModTime().Unix()) ts.ns = uint32(fi.ModTime().Nanosecond()) @@ -79,15 +75,19 @@ func getMtime(f *os.File) (ts fileTimestamp) { // getActualAttr reads the actual modification time and hashes the file content. func getActualAttr(f *os.File) (attr fileAttr, err error) { attr.sha256 = []byte(zeroSha256) - attr.ts = getMtime(f) + attr.ts, err = getMtime(f) + if err != nil { + return attr, err + } h := sha256.New() if _, err := io.Copy(h, f); err != nil { - fmt.Println(err) - os.Exit(2) + return attr, err } // Check if the file was modified while we were computing the hash - ts2 := getMtime(f) - if attr.ts != ts2 { + ts2, err := getMtime(f) + if err != nil { + return attr, err + } else if attr.ts != ts2 { return attr, syscall.EINPROGRESS } attr.sha256 = []byte(fmt.Sprintf("%x", h.Sum(nil))) @@ -147,7 +147,7 @@ func checkFile(fn string) { f, err := os.Open(fn) if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) - stats.errors++ + stats.errorsOpening++ return } defer f.Close() @@ -155,7 +155,7 @@ func checkFile(fn string) { if args.remove { if err = removeAttr(f); err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) - stats.errors++ + stats.errorsOther++ return } if !args.q { @@ -173,7 +173,12 @@ func checkFile(fn string) { } stats.inprogress++ return + } else if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + stats.errorsOther++ + return } + if stored.ts == actual.ts { if bytes.Equal(stored.sha256, actual.sha256) { if !args.q { @@ -203,7 +208,7 @@ func checkFile(fn string) { err = storeAttr(f, actual) if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) - stats.errors++ + stats.errorsWritingXattr++ return } } diff --git a/cshatag.1 b/cshatag.1 index 4f82180..e82c734 100644 --- a/cshatag.1 +++ b/cshatag.1 @@ -45,24 +45,26 @@ rewritten in Go in 2019. .SH OPTIONS --remove remove cshatag's xattrs from FILE +-recursive recursively process the contents of directories .br --q quiet mode - don't report files +-remove remove cshatag's xattrs from FILE .br --qq quiet2 mode - only report files and errors +-q quiet mode - don't report files +.br +-qq quiet2 mode - only report files and errors .SH EXAMPLES -Typically, cshatag will be called from find: +Check all regular files in the file tree below the current working directory: .br -# find . -xdev -type f -print0 | xargs -0 cshatag > cshatag.log +# cshatag -recursive . > cshatag.log .br Errors like corrupt files will then be printed to stderr or grep for "corrupt" in cshatag.log. To remove the extended attributes from all files: .br -# find . -xdev -type f -print0 | xargs -0 cshatag -remove +# cshatag -recursive -remove . .SH "RETURN VALUE" @@ -70,13 +72,15 @@ To remove the extended attributes from all files: .br 1 Wrong number of arguments .br -2 File could not be opened +2 One or more files could not be opened +.br +3 One or more files is not a regular file .br -3 File is not a regular file +4 Extended attributes could not be written to one or more files .br -4 Extended attributs could not be written to file +5 At least one file was found to be corrupt .br -5 File is corrupt +6 More than one type of error occurred .SH COMPATIBILITY @@ -92,5 +96,3 @@ Copyright 2012 Jakob Unterwurzacher. MIT License. .SH "SEE ALSO" shatag(1), sha256sum(1), getfattr(1), setfattr(1) - - diff --git a/main.go b/main.go index c7c2721..99a331a 100644 --- a/main.go +++ b/main.go @@ -4,24 +4,68 @@ import ( "flag" "fmt" "os" + "path/filepath" ) var GitVersion = "" var stats struct { - total int - errors int - inprogress int - corrupt int - timechange int - outdated int - ok int + total int + errorsNotRegular int + errorsOpening int + errorsWritingXattr int + errorsOther int + inprogress int + corrupt int + timechange int + outdated int + ok int } var args struct { - remove bool - q bool - qq bool + remove bool + recursive bool + q bool + qq bool +} + +// walkFn is used when `cshatag` is called with the `--recursive` option. It is the function called +// for each file or directory visited whilst traversing the file tree. +func walkFn(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Fprintf(os.Stderr, "Error accessing %q: %v\n", path, err) + stats.errorsOpening++ + } else if info.Mode().IsRegular() { + checkFile(path) + } else if !info.IsDir() { + if !args.qq { + fmt.Printf(" %s\n", path) + } + } + return nil +} + +// processArg is called for each command-line argument given. For regular files it will call +// `checkFile`. Directories will be processed recursively provided the `--recursive` flag is set. +// Symbolic links are not followed. +func processArg(fn string) { + fi, err := os.Lstat(fn) // Using Lstat to be consistent with filepath.Walk for symbolic links. + if err != nil { + fmt.Fprintln(os.Stderr, err) + stats.errorsOpening++ + } else if fi.Mode().IsRegular() { + checkFile(fn) + } else if fi.IsDir() { + if args.recursive { + filepath.Walk(fn, walkFn) + } else { + fmt.Fprintf(os.Stderr, "Error: %q is a directory, did you mean to use the '-recursive' option?\n", fn) + stats.errorsNotRegular++ + } + } else { + fmt.Fprintf(os.Stderr, "Error: %q is not a regular file.\n", fn) + stats.errorsNotRegular++ + } } func main() { @@ -34,6 +78,8 @@ func main() { flag.BoolVar(&args.remove, "remove", false, "Remove any previously stored extended attributes.") flag.BoolVar(&args.q, "q", false, "quiet: don't print files") flag.BoolVar(&args.qq, "qq", false, "quiet²: Only print files and errors") + flag.BoolVar(&args.recursive, "recursive", false, "Recursively descend into subdirectories. "+ + "Symbolic links are not followed.") flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s %s\n", myname, GitVersion) fmt.Fprintf(os.Stderr, "Usage: %s [OPTION] FILE [FILE ...]\n", myname) @@ -51,13 +97,27 @@ func main() { } for _, fn := range flag.Args() { - checkFile(fn) - } - if (stats.ok + stats.outdated + stats.timechange) == stats.total { - os.Exit(0) + processArg(fn) } + if stats.corrupt > 0 { os.Exit(5) } - os.Exit(2) + + totalErrors := stats.errorsOpening + stats.errorsNotRegular + stats.errorsWritingXattr + + stats.errorsOther + if totalErrors > 0 { + if stats.errorsOpening == totalErrors { + os.Exit(2) + } else if stats.errorsNotRegular == totalErrors { + os.Exit(3) + } else if stats.errorsWritingXattr == totalErrors { + os.Exit(4) + } + os.Exit(6) + } + if (stats.ok + stats.outdated + stats.timechange) == stats.total { + os.Exit(0) + } + os.Exit(6) } diff --git a/tests/.gitignore b/tests/.gitignore index 5a9e448..1556048 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,4 @@ +foo/foo.txt foo.txt *.out *.err diff --git a/tests/6.expected b/tests/6.expected new file mode 100644 index 0000000..2042e79 --- /dev/null +++ b/tests/6.expected @@ -0,0 +1 @@ +Error: "foo" is a directory, did you mean to use the '-recursive' option? diff --git a/tests/7.expected b/tests/7.expected new file mode 100644 index 0000000..9eeabd1 --- /dev/null +++ b/tests/7.expected @@ -0,0 +1,3 @@ + foo/foo.txt + stored: 0000000000000000000000000000000000000000000000000000000000000000 0000000000.000000000 + actual: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 1546297200.000000000 diff --git a/tests/run_tests.sh b/tests/run_tests.sh index ee6cbe4..7f13e06 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -114,6 +114,7 @@ if [[ $RES -ne 3 ]]; then echo "should have returned an error code 3, but returned $RES" exit 1 fi +rm -f symlink1 echo "*** Testing timechange ***" echo same > foo.txt @@ -123,4 +124,21 @@ TZ=CET touch -t 201901010001 foo.txt ../cshatag foo.txt > 5.out diff -u 5.expected 5.out +echo "*** Testing recursive flag ***" +rm -rf foo +mkdir foo +TZ=CET touch -t 201901010000 foo/foo.txt +set +e +../cshatag foo 2> 6.err +RES=$? +set -e +if [[ $RES -ne 3 ]]; then + echo "should have returned error code 3" + exit 1 +fi +diff -u 6.expected 6.err +../cshatag --recursive foo > 7.out +diff -u 7.expected 7.out +rm -rf foo + echo "*** ALL TESTS PASSED ***"