diff --git a/README.md b/README.md index d51ab84..ec0066f 100644 --- a/README.md +++ b/README.md @@ -70,18 +70,18 @@ ls -l chkbit/chkbit ## Usage -Run `chkbit -u PATH` to create/update the chkbit index. +Run `chkbit update PATH` to create/update the chkbit index. chkbit will -- create a `.chkbit` index in every subdirectory of the path it was given. -- update the index with blake3 (see --algo) hashes for every file. +- create a `.chkbit` index in every subdirectory of the path it was given (see `--db` to use a database). +- update the index with blake3 (see `--algo`) hashes for every file. - report damage for files that failed the integrity check since the last run (check the exit status). -Run `chkbit PATH` to verify only. +Run `chkbit check PATH` to verify only. ``` -Usage: chkbit [ ...] [flags] +Usage: chkbit [flags] Ensures the safety of your files by verifying that their data integrity remains intact over time, especially during transfers and backups. @@ -89,20 +89,15 @@ intact over time, especially during transfers and backups. For help tips run "chkbit -H" or go to https://github.com/laktak/chkbit -Arguments: - [ ...] directories to check - Flags: -h, --help Show context-sensitive help. - -H, --tips show tips + --db use a index database instead of index files -m, --[no-]show-missing show missing files/directories -d, --[no-]include-dot include dot files -S, --[no-]skip-symlinks do not follow symlinks -R, --[no-]no-recurse do not recurse into subdirectories -D, --[no-]no-dir-in-index do not track directories in the index --no-config ignore the config file - --force force update of damaged items (advanced usage - only) -l, --log-file=STRING write to a logfile if specified --[no-]log-verbose verbose logging --algo="blake3" hash algorithm: md5, sha512, blake3 @@ -111,27 +106,36 @@ Flags: --ignore-name=".chkbitignore" filename that chkbit reads its ignore list from, needs to start with '.' - --db use a index database instead of index files -w, --workers=5 number of workers to use. For slow IO (like on a spinning disk) --workers=1 will be faster. --[no-]plain show plain status instead of being fancy -q, --[no-]quiet quiet, don't show progress/information -v, --[no-]verbose verbose output - -V, --version show version information - -Mode - -c, --check chkbit will verify files in readonly mode (default - mode) - -u, --update add and update indices - -a, --add-only only add new and modified files, do not check - existing (quicker) - --init-db initialize a new index database at the given path - for use with --db - -i, --show-ignored-only only show ignored files + +Commands: + check ... [flags] + chkbit will verify files in readonly mode + + update ... [flags] + add and update indices + + init-db [flags] + initialize a new index database at the given path for use with --db + + show-ignored-only ... [flags] + only show ignored files + + tips [flags] + show tips + + version [flags] + show version information + +Run "chkbit --help" for more information on a command. ``` ``` -$ chkbit -H +$ chkbit tips .chkbitignore rules: - each line should contain exactly one name @@ -162,7 +166,7 @@ Configuration file (json): { "include_dot": true } ``` -chkbit is set to use only 5 workers by default so it will not slow your system to a crawl. You can specify a higher number to make it a lot faster if the IO throughput can also keep up. For slow/spinning disks, use `--worker=1`. +chkbit is set to use 5 workers by default so it will not slow your system to a crawl. You can specify a higher number to make it a lot faster if the IO throughput can also keep up. For slow/spinning disks, use `--worker=1`. ## Repair @@ -266,7 +270,7 @@ On Linux/macOS you can try: Create test and set the modified time: ``` $ echo foo1 > test; touch -t 201501010000 test -$ chkbit -u . +$ chkbit update . new ./test Processed 1 file. @@ -283,7 +287,7 @@ Processed 1 file. Now update test with a new modified: ``` $ echo foo2 > test; touch -t 201501010001 test # update test & modified -$ chkbit -u . +$ chkbit update . upd ./test Processed 1 file. @@ -300,7 +304,7 @@ Processed 1 file. Now update test with the same modified to simulate damage: ``` $ echo foo3 > test; touch -t 201501010001 test -$ chkbit -u . +$ chkbit update . DMG ./test Processed 1 file. @@ -314,5 +318,3 @@ error: detected 1 file with damage! `DMG` indicates damage. - - diff --git a/cmd/chkbit/main.go b/cmd/chkbit/main.go index 6c3a228..259821f 100644 --- a/cmd/chkbit/main.go +++ b/cmd/chkbit/main.go @@ -26,6 +26,14 @@ const ( Fancy ) +type Command int + +const ( + Check Command = iota + Update + Show +) + const ( updateInterval = time.Millisecond * 700 sizeMB int64 = 1024 * 1024 @@ -44,35 +52,49 @@ var ( ) type CLI struct { - Paths []string `arg:"" optional:"" name:"paths" help:"directories to check"` - Tips bool `short:"H" help:"show tips"` - Check bool `short:"c" help:"chkbit will verify files in readonly mode (default mode)" xor:"mode" group:"Mode"` - Update bool `short:"u" help:"add and update indices" xor:"mode" group:"Mode"` - AddOnly bool `short:"a" help:"only add new and modified files, do not check existing (quicker)" xor:"mode" group:"Mode"` - InitDb bool `help:"initialize a new index database at the given path for use with --db" xor:"mode" group:"Mode"` - ShowIgnoredOnly bool `short:"i" help:"only show ignored files" xor:"mode" group:"Mode"` - ShowMissing bool `short:"m" help:"show missing files/directories" negatable:""` - IncludeDot bool `short:"d" help:"include dot files" negatable:""` - SkipSymlinks bool `short:"S" help:"do not follow symlinks" negatable:""` - NoRecurse bool `short:"R" help:"do not recurse into subdirectories" negatable:""` - NoDirInIndex bool `short:"D" help:"do not track directories in the index" negatable:""` - NoConfig bool `help:"ignore the config file"` - Force bool `help:"force update of damaged items (advanced usage only)"` - LogFile string `short:"l" help:"write to a logfile if specified"` - LogVerbose bool `help:"verbose logging" negatable:""` - Algo string `default:"blake3" help:"hash algorithm: md5, sha512, blake3"` - IndexName string `default:".chkbit" help:"filename where chkbit stores its hashes, needs to start with '.'"` - IgnoreName string `default:".chkbitignore" help:"filename that chkbit reads its ignore list from, needs to start with '.'"` - Db bool `help:"use a index database instead of index files"` - Workers int `short:"w" default:"5" help:"number of workers to use. For slow IO (like on a spinning disk) --workers=1 will be faster."` - Plain bool `help:"show plain status instead of being fancy" negatable:""` - Quiet bool `short:"q" help:"quiet, don't show progress/information" negatable:""` - Verbose bool `short:"v" help:"verbose output" negatable:""` - Version bool `short:"V" help:"show version information"` + Check struct { + Paths []string `arg:"" name:"paths" help:"directories to check"` + } `cmd:"" help:"chkbit will verify files in readonly mode"` + + Update struct { + Paths []string `arg:"" name:"paths" help:"directories to update"` + AddOnly bool `short:"a" help:"only add new and modified files, do not check existing (quicker)"` + Force bool `help:"force update of damaged items (advanced usage only)"` + } `cmd:"" help:"add and update indices"` + + InitDb struct { + Path string `arg:"" help:"directory for the database"` + Force bool `help:"force init if a database already exists"` + } `cmd:"" help:"initialize a new index database at the given path for use with --db"` + + ShowIgnoredOnly struct { + Paths []string `arg:"" name:"paths" help:"directories to list"` + } `cmd:"" help:"only show ignored files"` + + Tips struct { + } `cmd:"" help:"show tips"` + + Version struct { + } `cmd:"" help:"show version information"` + + Db bool `help:"use a index database instead of index files"` + ShowMissing bool `short:"m" help:"show missing files/directories" negatable:""` + IncludeDot bool `short:"d" help:"include dot files" negatable:""` + SkipSymlinks bool `short:"S" help:"do not follow symlinks" negatable:""` + NoRecurse bool `short:"R" help:"do not recurse into subdirectories" negatable:""` + NoDirInIndex bool `short:"D" help:"do not track directories in the index" negatable:""` + NoConfig bool `help:"ignore the config file"` + LogFile string `short:"l" help:"write to a logfile if specified"` + LogVerbose bool `help:"verbose logging" negatable:""` + Algo string `default:"blake3" help:"hash algorithm: md5, sha512, blake3"` + IndexName string `default:".chkbit" help:"filename where chkbit stores its hashes, needs to start with '.'"` + IgnoreName string `default:".chkbitignore" help:"filename that chkbit reads its ignore list from, needs to start with '.'"` + Workers int `short:"w" default:"5" help:"number of workers to use. For slow IO (like on a spinning disk) --workers=1 will be faster."` + Plain bool `help:"show plain status instead of being fancy" negatable:""` + Quiet bool `short:"q" help:"quiet, don't show progress/information" negatable:""` + Verbose bool `short:"v" help:"verbose output" negatable:""` } -var cli CLI - type Main struct { context *chkbit.Context dmgList []string @@ -164,29 +186,37 @@ func (m *Main) showStatus() { } } -func (m *Main) process() (bool, error) { - // verify mode - var b01 = map[bool]int8{false: 0, true: 1} - if b01[cli.Check]+b01[cli.Update]+b01[cli.AddOnly]+b01[cli.ShowIgnoredOnly] > 1 { - return false, errors.New("can only run one mode at a time") - } +func (m *Main) process(cmd Command, cli CLI) (bool, error) { var err error m.context, err = chkbit.NewContext(cli.Workers, cli.Algo, cli.IndexName, cli.IgnoreName) if err != nil { return false, err } - m.context.ForceUpdateDmg = cli.Force - m.context.UpdateIndex = cli.Update || cli.AddOnly - m.context.AddOnly = cli.AddOnly - m.context.ShowIgnoredOnly = cli.ShowIgnoredOnly + + var pathList []string + switch cmd { + case Check: + pathList = cli.Check.Paths + m.log("chkbit check " + strings.Join(pathList, ", ")) + case Update: + pathList = cli.Update.Paths + m.context.UpdateIndex = true + m.context.AddOnly = cli.Update.AddOnly + m.context.ForceUpdateDmg = cli.Update.Force + m.log("chkbit update " + strings.Join(pathList, ", ")) + case Show: + pathList = cli.ShowIgnoredOnly.Paths + m.context.ShowIgnoredOnly = true + m.log("chkbit show-ignored-only " + strings.Join(pathList, ", ")) + } + m.context.ShowMissing = cli.ShowMissing m.context.IncludeDot = cli.IncludeDot m.context.SkipSymlinks = cli.SkipSymlinks m.context.SkipSubdirectories = cli.NoRecurse m.context.TrackDirectories = !cli.NoDirInIndex - pathList := cli.Paths if cli.Db { var root string root, pathList, err = m.context.UseIndexDb(pathList) @@ -269,7 +299,7 @@ func (m *Main) printResult() error { if m.context.NumDel > 0 { del = fmt.Sprintf("\n- %s would have been removed", util.LangNum1Choice(m.context.NumDel, "file/directory", "files/directories")) } - cprint(termAlertFG, fmt.Sprintf("No changes were made (specify -u to update):\n- %s would have been added\n- %s would have been updated%s", + cprint(termAlertFG, fmt.Sprintf("No changes were made:\n- %s would have been added\n- %s would have been updated%s", util.LangNum1MutateSuffix(m.context.NumNew, "file"), util.LangNum1MutateSuffix(m.context.NumUpd, "file"), del)) @@ -314,7 +344,10 @@ func (m *Main) run() int { configPath = filepath.Join(configRoot, "chkbit/config.json") } - kong.Parse(&cli, + var cli CLI + var ctx *kong.Context + var cmd Command + ctx = kong.Parse(&cli, kong.Name("chkbit"), kong.Description(headerHelp), kong.UsageOnError(), @@ -323,37 +356,40 @@ func (m *Main) run() int { if cli.NoConfig { cli = CLI{} - kong.Parse(&cli, + ctx = kong.Parse(&cli, kong.Name("chkbit"), kong.Description(headerHelp), kong.UsageOnError(), ) } - if cli.Tips { - fmt.Println(strings.ReplaceAll(helpTips, "", configPath)) - os.Exit(0) - } - - if cli.Version { - fmt.Println("github.com/laktak/chkbit") - fmt.Println(appVersion) - return 0 - } - - if cli.InitDb { - if len(cli.Paths) != 1 { - fmt.Println("error: specify a path") - return 1 - } - if err := chkbit.InitializeIndexDb(cli.Paths[0], cli.IndexName, cli.Force); err != nil { + switch ctx.Command() { + case "check ": + cmd = Check + case "update ": + cmd = Update + case "show-ignored-only ": + cmd = Show + case "init-db ": + m.log("chkbit init-db " + cli.InitDb.Path) + if err := chkbit.InitializeIndexDb(cli.InitDb.Path, cli.IndexName, cli.InitDb.Force); err != nil { fmt.Println("error: " + err.Error()) return 1 } return 0 + case "tips": + fmt.Println(strings.ReplaceAll(helpTips, "", configPath)) + return 0 + case "version": + fmt.Println("github.com/laktak/chkbit") + fmt.Println(appVersion) + return 0 + default: + fmt.Println("unknown: " + ctx.Command()) + return 1 } - m.verbose = cli.Verbose || cli.ShowIgnoredOnly + m.verbose = cli.Verbose || cmd == Show if cli.LogFile != "" { m.logVerbose = cli.LogVerbose f, err := os.OpenFile(cli.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) @@ -375,23 +411,17 @@ func (m *Main) run() int { m.progress = Fancy } - if len(cli.Paths) > 0 { - m.log("chkbit " + strings.Join(cli.Paths, ", ")) - - if showRes, err := m.process(); err == nil { - if showRes && !cli.ShowIgnoredOnly { - if m.printResult() != nil { - return 1 - } + if showRes, err := m.process(cmd, cli); err == nil { + if showRes && cmd != Show { + if m.printResult() != nil { + return 1 } - } else { - fmt.Println("error: " + err.Error()) - return 1 } - } else { - fmt.Println("error: specify a path, see -h") + fmt.Println("error: " + err.Error()) + return 1 } + return 0 } diff --git a/scripts/run_test.go b/scripts/run_test.go index 46e912c..fd7ed1a 100644 --- a/scripts/run_test.go +++ b/scripts/run_test.go @@ -145,7 +145,7 @@ func TestRoot(t *testing.T) { // update index, no recourse t.Run("no-recourse", func(t *testing.T) { - cmd := runCmd("-umR", filepath.Join(root, "day/office")) + cmd := runCmd("update", "-mR", filepath.Join(root, "day/office")) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -160,7 +160,7 @@ func TestRoot(t *testing.T) { // update remaining index from root t.Run("update-remaining", func(t *testing.T) { - cmd := runCmd("-um", root) + cmd := runCmd("update", "-m", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -178,7 +178,7 @@ func TestRoot(t *testing.T) { os.RemoveAll(filepath.Join(root, "thing/change")) os.Remove(filepath.Join(root, "time/hour/minute/body-information.csv")) - cmd := runCmd("-m", root) + cmd := runCmd("check", "-m", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -190,7 +190,7 @@ func TestRoot(t *testing.T) { // do not report missing without -m t.Run("no-missing", func(t *testing.T) { - cmd := runCmd(root) + cmd := runCmd("check", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -202,7 +202,7 @@ func TestRoot(t *testing.T) { // check for missing and update t.Run("missing", func(t *testing.T) { - cmd := runCmd("-um", root) + cmd := runCmd("update", "-m", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -215,7 +215,7 @@ func TestRoot(t *testing.T) { // check again t.Run("repeat", func(t *testing.T) { for i := 0; i < 10; i++ { - cmd := runCmd("-uv", root) + cmd := runCmd("update", "-v", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -234,7 +234,7 @@ func TestRoot(t *testing.T) { genFiles(filepath.Join(root, "way/add"), 99) genFile(filepath.Join(root, "time/add-file.txt"), 500) - cmd := runCmd("-a", root) + cmd := runCmd("update", "-a", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -252,7 +252,7 @@ func TestRoot(t *testing.T) { // modify existing genFile(filepath.Join(root, "way/job/word-business.mp3"), 500) - cmd := runCmd("-a", root) + cmd := runCmd("update", "-a", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -267,7 +267,7 @@ func TestRoot(t *testing.T) { // update remaining t.Run("update-remaining-add", func(t *testing.T) { - cmd := runCmd("-u", root) + cmd := runCmd("update", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -282,7 +282,7 @@ func TestRoot(t *testing.T) { genFiles(filepath.Join(root, "way/.hidden"), 99) genFile(filepath.Join(root, "time/.ignored"), 999) - cmd := runCmd("-u", root) + cmd := runCmd("update", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -294,7 +294,7 @@ func TestRoot(t *testing.T) { // include dot t.Run("include-dot", func(t *testing.T) { - cmd := runCmd("-u", "-d", root) + cmd := runCmd("update", "-d", root) out, err := cmd.Output() if err != nil { t.Fatalf("failed with '%s'\n", err) @@ -334,7 +334,7 @@ func TestDMG(t *testing.T) { os.WriteFile(testFile, []byte("foo1"), 0644) os.Chtimes(testFile, t2, t2) - cmd := runCmd("-u", ".") + cmd := runCmd("update", ".") if out, err := cmd.Output(); err != nil { t.Fatalf("failed with '%s'\n", err) } else { @@ -347,7 +347,7 @@ func TestDMG(t *testing.T) { os.WriteFile(testFile, []byte("foo2"), 0644) os.Chtimes(testFile, t1, t1) - cmd := runCmd("-u", ".") + cmd := runCmd("update", ".") if out, err := cmd.Output(); err != nil { t.Fatalf("failed with '%s'\n", err) } else { @@ -360,7 +360,7 @@ func TestDMG(t *testing.T) { os.WriteFile(testFile, []byte("foo3"), 0644) os.Chtimes(testFile, t3, t3) - cmd := runCmd("-u", ".") + cmd := runCmd("update", ".") if out, err := cmd.Output(); err != nil { t.Fatalf("failed with '%s'\n", err) } else { @@ -373,7 +373,7 @@ func TestDMG(t *testing.T) { os.WriteFile(testFile, []byte("foo4"), 0644) os.Chtimes(testFile, t3, t3) - cmd := runCmd("-u", ".") + cmd := runCmd("update", ".") if out, err := cmd.Output(); err != nil { if cmd.ProcessState.ExitCode() != 1 { t.Fatalf("expected to fail with exit code 1 vs %d!", cmd.ProcessState.ExitCode())