Compare commits

..

No commits in common. "master" and "v5.1.0" have entirely different histories.

7 changed files with 217 additions and 322 deletions

View File

@ -8,10 +8,10 @@ chkbit is a tool that ensures the safety of your files by checking if their *dat
- [How it works](#how-it-works) - [How it works](#how-it-works)
- [Installation](#installation) - [Installation](#installation)
- [chkbit as a Go module](#chkbit-as-a-go-module)
- [Usage](#usage) - [Usage](#usage)
- [Repair](#repair) - [Repair](#repair)
- [Ignore files](#ignore-files) - [Ignore files](#ignore-files)
- [chkbit as a Go module](#chkbit-as-a-go-module)
- [FAQ](#faq) - [FAQ](#faq)
@ -50,7 +50,7 @@ Building from the source requires Go.
- Either install it directly - Either install it directly
```shell ```shell
go install github.com/laktak/chkbit/v5/cmd/chkbit@latest go install github.com/laktak/chkbit@latest
``` ```
- or clone and build - or clone and build
@ -58,11 +58,22 @@ go install github.com/laktak/chkbit/v5/cmd/chkbit@latest
```shell ```shell
git clone https://github.com/laktak/chkbit git clone https://github.com/laktak/chkbit
chkbit/scripts/build chkbit/scripts/build
# binary: # output is here:
ls -l chkbit/chkbit chkbit/chkbit
``` ```
## chkbit as a Go module
chkbit is can also be used in other Go programs.
```
go get github.com/laktak/chkbit
```
For more information see the linked documentation on [pkg.go.dev](https://pkg.go.dev/github.com/laktak/chkbit).
## Usage ## Usage
Run `chkbit -u PATH` to create/update the chkbit index. Run `chkbit -u PATH` to create/update the chkbit index.
@ -84,18 +95,15 @@ Arguments:
Flags: Flags:
-h, --help Show context-sensitive help. -h, --help Show context-sensitive help.
-H, --tips Show tips. -H, --tips Show tips.
-c, --check check mode: chkbit will verify files in readonly mode (default mode) -u, --update update indices (without this chkbit will verify files in readonly mode)
-u, --update update mode: add and update indices --show-ignored-only only show ignored files
-a, --add-only add mode: only add new files, do not check existing (quicker)
-i, --show-ignored-only show-ignored mode: only show ignored files
-m, --show-missing show missing files/directories -m, --show-missing show missing files/directories
--force force update of damaged items (advanced usage only) --algo="blake3" hash algorithm: md5, sha512, blake3 (default: blake3)
-S, --skip-symlinks do not follow symlinks -f, --force force update of damaged items
-R, --no-recurse do not recurse into subdirectories -s, --skip-symlinks do not follow symlinks
-D, --no-dir-in-index do not track directories in the index -D, --no-dir-in-index do not track directories in the index
-l, --log-file=STRING write to a logfile if specified -l, --log-file=STRING write to a logfile if specified
--log-verbose verbose logging --log-verbose verbose logging
--algo="blake3" hash algorithm: md5, sha512, blake3 (default: blake3)
--index-name=".chkbit" filename where chkbit stores its hashes, needs to start with '.' (default: .chkbit) --index-name=".chkbit" filename where chkbit stores its hashes, needs to start with '.' (default: .chkbit)
--ignore-name=".chkbitignore" filename that chkbit reads its ignore list from, needs to start with '.' (default: .chkbitignore) --ignore-name=".chkbitignore" filename that chkbit reads its ignore list from, needs to start with '.' (default: .chkbitignore)
-w, --workers=5 number of workers to use (default: 5) -w, --workers=5 number of workers to use (default: 5)
@ -155,17 +163,6 @@ Add a `.chkbitignore` file containing the names of the files/directories you wis
- hidden files (starting with a `.`) are ignored by default - hidden files (starting with a `.`) are ignored by default
## chkbit as a Go module
chkbit is can also be used in other Go programs.
```
go get github.com/laktak/chkbit/v5
```
For more information see the documentation on [pkg.go.dev](https://pkg.go.dev/github.com/laktak/chkbit/v5).
## FAQ ## FAQ
### Should I run `chkbit` on my whole drive? ### Should I run `chkbit` on my whole drive?

View File

@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/laktak/chkbit/v5" "github.com/laktak/chkbit"
"github.com/laktak/chkbit/v5/cmd/chkbit/util" "github.com/laktak/chkbit/cmd/chkbit/util"
"github.com/laktak/lterm" "github.com/laktak/lterm"
) )
@ -44,18 +44,15 @@ var (
var cli struct { var cli struct {
Paths []string `arg:"" optional:"" name:"paths" help:"directories to check"` Paths []string `arg:"" optional:"" name:"paths" help:"directories to check"`
Tips bool `short:"H" help:"Show tips."` Tips bool `short:"H" help:"Show tips."`
Check bool `short:"c" help:"check mode: chkbit will verify files in readonly mode (default mode)"` Update bool `short:"u" help:"update indices (without this chkbit will verify files in readonly mode)"`
Update bool `short:"u" help:"update mode: add and update indices"` ShowIgnoredOnly bool `help:"only show ignored files"`
AddOnly bool `short:"a" help:"add mode: only add new files, do not check existing (quicker)"`
ShowIgnoredOnly bool `short:"i" help:"show-ignored mode: only show ignored files"`
ShowMissing bool `short:"m" help:"show missing files/directories"` ShowMissing bool `short:"m" help:"show missing files/directories"`
Force bool `help:"force update of damaged items (advanced usage only)"` Algo string `default:"blake3" help:"hash algorithm: md5, sha512, blake3 (default: blake3)"`
SkipSymlinks bool `short:"S" help:"do not follow symlinks"` Force bool `short:"f" help:"force update of damaged items"`
NoRecurse bool `short:"R" help:"do not recurse into subdirectories"` SkipSymlinks bool `short:"s" help:"do not follow symlinks"`
NoDirInIndex bool `short:"D" help:"do not track directories in the index"` NoDirInIndex bool `short:"D" help:"do not track directories in the index"`
LogFile string `short:"l" help:"write to a logfile if specified"` LogFile string `short:"l" help:"write to a logfile if specified"`
LogVerbose bool `help:"verbose logging"` LogVerbose bool `help:"verbose logging"`
Algo string `default:"blake3" help:"hash algorithm: md5, sha512, blake3 (default: blake3)"`
IndexName string `default:".chkbit" help:"filename where chkbit stores its hashes, needs to start with '.' (default: .chkbit)"` IndexName string `default:".chkbit" help:"filename where chkbit stores its hashes, needs to start with '.' (default: .chkbit)"`
IgnoreName string `default:".chkbitignore" help:"filename that chkbit reads its ignore list from, needs to start with '.' (default: .chkbitignore)"` IgnoreName string `default:".chkbitignore" help:"filename that chkbit reads its ignore list from, needs to start with '.' (default: .chkbitignore)"`
Workers int `short:"w" default:"5" help:"number of workers to use (default: 5)"` Workers int `short:"w" default:"5" help:"number of workers to use (default: 5)"`
@ -66,7 +63,6 @@ var cli struct {
} }
type Main struct { type Main struct {
context *chkbit.Context
dmgList []string dmgList []string
errList []string errList []string
verbose bool verbose bool
@ -108,12 +104,12 @@ func (m *Main) logStatus(stat chkbit.Status, message string) bool {
return false return false
} }
func (m *Main) showStatus() { func (m *Main) showStatus(context *chkbit.Context) {
last := time.Now().Add(-updateInterval) last := time.Now().Add(-updateInterval)
stat := "" stat := ""
for { for {
select { select {
case item := <-m.context.LogQueue: case item := <-context.LogQueue:
if item == nil { if item == nil {
if m.progress == Fancy { if m.progress == Fancy {
lterm.Printline("") lterm.Printline("")
@ -124,10 +120,10 @@ func (m *Main) showStatus() {
if m.progress == Fancy { if m.progress == Fancy {
lterm.Write(termBG, termFG1, stat, lterm.ClearLine(0), lterm.Reset, "\r") lterm.Write(termBG, termFG1, stat, lterm.ClearLine(0), lterm.Reset, "\r")
} else { } else {
fmt.Print(m.context.NumTotal, "\r") fmt.Print(context.NumTotal, "\r")
} }
} }
case perf := <-m.context.PerfQueue: case perf := <-context.PerfQueue:
now := time.Now() now := time.Now()
m.fps.Push(now, perf.NumFiles) m.fps.Push(now, perf.NumFiles)
m.bps.Push(now, perf.NumBytes) m.bps.Push(now, perf.NumBytes)
@ -137,11 +133,11 @@ func (m *Main) showStatus() {
statF := fmt.Sprintf("%d files/s", m.fps.Last()) statF := fmt.Sprintf("%d files/s", m.fps.Last())
statB := fmt.Sprintf("%d MB/s", m.bps.Last()/sizeMB) statB := fmt.Sprintf("%d MB/s", m.bps.Last()/sizeMB)
stat = "RW" stat = "RW"
if !m.context.UpdateIndex { if !context.UpdateIndex {
stat = "RO" stat = "RO"
} }
stat = fmt.Sprintf("[%s:%d] %5d files $ %s %-13s $ %s %-13s", stat = fmt.Sprintf("[%s:%d] %5d files $ %s %-13s $ %s %-13s",
stat, m.context.NumWorkers, m.context.NumTotal, stat, context.NumWorkers, context.NumTotal,
util.Sparkline(m.fps.Stats), statF, util.Sparkline(m.fps.Stats), statF,
util.Sparkline(m.bps.Stats), statB) util.Sparkline(m.bps.Stats), statB)
stat = util.LeftTruncate(stat, m.termWidth-1) stat = util.LeftTruncate(stat, m.termWidth-1)
@ -149,49 +145,44 @@ func (m *Main) showStatus() {
stat = strings.Replace(stat, "$", termSepFG+termSep+termFG3, 1) stat = strings.Replace(stat, "$", termSepFG+termSep+termFG3, 1)
lterm.Write(termBG, termFG1, stat, lterm.ClearLine(0), lterm.Reset, "\r") lterm.Write(termBG, termFG1, stat, lterm.ClearLine(0), lterm.Reset, "\r")
} else if m.progress == Plain { } else if m.progress == Plain {
fmt.Print(m.context.NumTotal, "\r") fmt.Print(context.NumTotal, "\r")
} }
} }
} }
} }
} }
func (m *Main) process() bool { func (m *Main) process() *chkbit.Context {
// verify mode if cli.Update && cli.ShowIgnoredOnly {
var b01 = map[bool]int8{false: 0, true: 1} fmt.Println("Error: use either --update or --show-ignored-only!")
if b01[cli.Check]+b01[cli.Update]+b01[cli.AddOnly]+b01[cli.ShowIgnoredOnly] > 1 { return nil
fmt.Println("Error: can only run one mode at a time!")
os.Exit(1)
} }
var err error context, err := chkbit.NewContext(cli.Workers, cli.Algo, cli.IndexName, cli.IgnoreName)
m.context, err = chkbit.NewContext(cli.Workers, cli.Algo, cli.IndexName, cli.IgnoreName)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return false return nil
} }
m.context.ForceUpdateDmg = cli.Force context.ForceUpdateDmg = cli.Force
m.context.UpdateIndex = cli.Update || cli.AddOnly context.UpdateIndex = cli.Update
m.context.AddOnly = cli.AddOnly context.ShowIgnoredOnly = cli.ShowIgnoredOnly
m.context.ShowIgnoredOnly = cli.ShowIgnoredOnly context.ShowMissing = cli.ShowMissing
m.context.ShowMissing = cli.ShowMissing context.SkipSymlinks = cli.SkipSymlinks
m.context.SkipSymlinks = cli.SkipSymlinks context.TrackDirectories = !cli.NoDirInIndex
m.context.SkipSubdirectories = cli.NoRecurse
m.context.TrackDirectories = !cli.NoDirInIndex
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
m.showStatus() m.showStatus(context)
}() }()
m.context.Start(cli.Paths) context.Start(cli.Paths)
wg.Wait() wg.Wait()
return true return context
} }
func (m *Main) printResult() { func (m *Main) printResult(context *chkbit.Context) {
cprint := func(col, text string) { cprint := func(col, text string) {
if m.progress != Quiet { if m.progress != Quiet {
if m.progress == Fancy { if m.progress == Fancy {
@ -214,14 +205,14 @@ func (m *Main) printResult() {
if m.progress != Quiet { if m.progress != Quiet {
mode := "" mode := ""
if !m.context.UpdateIndex { if !context.UpdateIndex {
mode = " in readonly mode" mode = " in readonly mode"
} }
status := fmt.Sprintf("Processed %s%s.", util.LangNum1MutateSuffix(m.context.NumTotal, "file"), mode) status := fmt.Sprintf("Processed %s%s.", util.LangNum1MutateSuffix(context.NumTotal, "file"), mode)
cprint(termOKFG, status) cprint(termOKFG, status)
m.log(status) m.log(status)
if m.progress == Fancy && m.context.NumTotal > 0 { if m.progress == Fancy && context.NumTotal > 0 {
elapsed := time.Since(m.fps.Start) elapsed := time.Since(m.fps.Start)
elapsedS := elapsed.Seconds() elapsedS := elapsed.Seconds()
fmt.Println("-", elapsed.Truncate(time.Second), "elapsed") fmt.Println("-", elapsed.Truncate(time.Second), "elapsed")
@ -230,24 +221,24 @@ func (m *Main) printResult() {
} }
del := "" del := ""
if m.context.UpdateIndex { if context.UpdateIndex {
if m.context.NumIdxUpd > 0 { if context.NumIdxUpd > 0 {
if m.context.NumDel > 0 { if context.NumDel > 0 {
del = fmt.Sprintf("\n- %s been removed", util.LangNum1Choice(m.context.NumDel, "file/directory has", "files/directories have")) del = fmt.Sprintf("\n- %s been removed", util.LangNum1Choice(context.NumDel, "file/directory has", "files/directories have"))
} }
cprint(termOKFG, fmt.Sprintf("- %s updated\n- %s added\n- %s updated%s", cprint(termOKFG, fmt.Sprintf("- %s updated\n- %s added\n- %s updated%s",
util.LangNum1Choice(m.context.NumIdxUpd, "directory was", "directories were"), util.LangNum1Choice(context.NumIdxUpd, "directory was", "directories were"),
util.LangNum1Choice(m.context.NumNew, "file hash was", "file hashes were"), util.LangNum1Choice(context.NumNew, "file hash was", "file hashes were"),
util.LangNum1Choice(m.context.NumUpd, "file hash was", "file hashes were"), util.LangNum1Choice(context.NumUpd, "file hash was", "file hashes were"),
del)) del))
} }
} else if m.context.NumNew+m.context.NumUpd+m.context.NumDel > 0 { } else if context.NumNew+context.NumUpd+context.NumDel > 0 {
if m.context.NumDel > 0 { if context.NumDel > 0 {
del = fmt.Sprintf("\n- %s would have been removed", util.LangNum1Choice(m.context.NumDel, "file/directory", "files/directories")) del = fmt.Sprintf("\n- %s would have been removed", util.LangNum1Choice(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 (specify -u to update):\n- %s would have been added\n- %s would have been updated%s",
util.LangNum1MutateSuffix(m.context.NumNew, "file"), util.LangNum1MutateSuffix(context.NumNew, "file"),
util.LangNum1MutateSuffix(m.context.NumUpd, "file"), util.LangNum1MutateSuffix(context.NumUpd, "file"),
del)) del))
} }
} }
@ -324,8 +315,9 @@ func (m *Main) run() {
if len(cli.Paths) > 0 { if len(cli.Paths) > 0 {
m.log("chkbit " + strings.Join(cli.Paths, ", ")) m.log("chkbit " + strings.Join(cli.Paths, ", "))
if m.process() && !m.context.ShowIgnoredOnly { context := m.process()
m.printResult() if context != nil && !context.ShowIgnoredOnly {
m.printResult(context)
} }
} else { } else {
fmt.Println("specify a path to check, see -h") fmt.Println("specify a path to check, see -h")

View File

@ -10,14 +10,12 @@ import (
type Context struct { type Context struct {
NumWorkers int NumWorkers int
UpdateIndex bool UpdateIndex bool
AddOnly bool
ShowIgnoredOnly bool ShowIgnoredOnly bool
ShowMissing bool ShowMissing bool
ForceUpdateDmg bool ForceUpdateDmg bool
HashAlgo string HashAlgo string
TrackDirectories bool TrackDirectories bool
SkipSymlinks bool SkipSymlinks bool
SkipSubdirectories bool
IndexFilename string IndexFilename string
IgnoreFilename string IgnoreFilename string
WorkQueue chan *WorkItem WorkQueue chan *WorkItem
@ -25,7 +23,6 @@ type Context struct {
PerfQueue chan *PerfEvent PerfQueue chan *PerfEvent
wg sync.WaitGroup wg sync.WaitGroup
mutex sync.Mutex
NumTotal int NumTotal int
NumIdxUpd int NumIdxUpd int
NumNew int NumNew int
@ -55,8 +52,6 @@ func NewContext(numWorkers int, hashAlgo string, indexFilename string, ignoreFil
} }
func (context *Context) log(stat Status, message string) { func (context *Context) log(stat Status, message string) {
context.mutex.Lock()
defer context.mutex.Unlock()
switch stat { switch stat {
case STATUS_ERR_DMG: case STATUS_ERR_DMG:
context.NumTotal++ context.NumTotal++
@ -72,9 +67,7 @@ func (context *Context) log(stat Status, message string) {
context.NumTotal++ context.NumTotal++
context.NumNew++ context.NumNew++
case STATUS_OK: case STATUS_OK:
if !context.AddOnly {
context.NumTotal++ context.NumTotal++
}
case STATUS_MISSING: case STATUS_MISSING:
context.NumDel++ context.NumDel++
//case STATUS_PANIC: //case STATUS_PANIC:
@ -187,9 +180,7 @@ func (context *Context) scanDir(root string, parentIgnore *Ignore) {
context.addWork(root, filesToIndex, dirList, ignore) context.addWork(root, filesToIndex, dirList, ignore)
if !context.SkipSubdirectories {
for _, name := range dirList { for _, name := range dirList {
context.scanDir(filepath.Join(root, name), ignore) context.scanDir(filepath.Join(root, name), ignore)
} }
} }
}

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/laktak/chkbit/v5 module github.com/laktak/chkbit
go 1.22.3 go 1.22.3

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"slices" "slices"
) )
@ -68,6 +69,10 @@ func (i *Index) getIndexFilepath() string {
return filepath.Join(i.path, i.context.IndexFilename) return filepath.Join(i.path, i.context.IndexFilename)
} }
func (i *Index) setMod(value bool) {
i.modified = value
}
func (i *Index) logFilePanic(name string, message string) { func (i *Index) logFilePanic(name string, message string) {
i.context.log(STATUS_PANIC, filepath.Join(i.path, name)+": "+message) i.context.log(STATUS_PANIC, filepath.Join(i.path, name)+": "+message)
} }
@ -91,7 +96,7 @@ func (i *Index) calcHashes(ignore *Ignore) {
var info *idxInfo var info *idxInfo
algo := i.context.HashAlgo algo := i.context.HashAlgo
if val, ok := i.cur[name]; ok { if val, ok := i.cur[name]; ok {
// existing file // existing
if val.LegacyHash != nil { if val.LegacyHash != nil {
// convert from py1 to new format // convert from py1 to new format
val = idxInfo{ val = idxInfo{
@ -104,13 +109,8 @@ func (i *Index) calcHashes(ignore *Ignore) {
if val.Algo != nil { if val.Algo != nil {
algo = *val.Algo algo = *val.Algo
} }
if i.context.AddOnly {
info = &val
} else {
info, err = i.calcFile(name, algo) info, err = i.calcFile(name, algo)
}
} else { } else {
// new file
if i.readonly { if i.readonly {
info = &idxInfo{Algo: &algo} info = &idxInfo{Algo: &algo}
} else { } else {
@ -137,14 +137,14 @@ func (i *Index) checkFix(forceUpdateDmg bool) {
for name, b := range i.new { for name, b := range i.new {
if a, ok := i.cur[name]; !ok { if a, ok := i.cur[name]; !ok {
i.logFile(STATUS_NEW, name) i.logFile(STATUS_NEW, name)
i.modified = true i.setMod(true)
} else { } else {
amod := int64(a.ModTime) amod := int64(a.ModTime)
bmod := int64(b.ModTime) bmod := int64(b.ModTime)
if a.Hash != nil && b.Hash != nil && *a.Hash == *b.Hash { if a.Hash != nil && b.Hash != nil && *a.Hash == *b.Hash {
i.logFile(STATUS_OK, name) i.logFile(STATUS_OK, name)
if amod != bmod { if amod != bmod {
i.modified = true i.setMod(true)
} }
continue continue
} }
@ -155,27 +155,24 @@ func (i *Index) checkFix(forceUpdateDmg bool) {
// keep DMG entry // keep DMG entry
i.new[name] = a i.new[name] = a
} else { } else {
i.modified = true i.setMod(true)
} }
} else if amod < bmod { } else if amod < bmod {
i.logFile(STATUS_UPDATE, name) i.logFile(STATUS_UPDATE, name)
i.modified = true i.setMod(true)
} else if amod > bmod { } else if amod > bmod {
i.logFile(STATUS_UP_WARN_OLD, name) i.logFile(STATUS_UP_WARN_OLD, name)
i.modified = true i.setMod(true)
} }
} }
} }
// track missing if i.context.ShowMissing {
for name := range i.cur { for name := range i.cur {
if _, ok := i.new[name]; !ok { if _, ok := i.new[name]; !ok {
i.modified = true
if i.context.ShowMissing {
i.logFile(STATUS_MISSING, name) i.logFile(STATUS_MISSING, name)
i.setMod(true)
} }
} }
}
// dirs // dirs
m := make(map[string]bool) m := make(map[string]bool)
for _, n := range i.newDirList { for _, n := range i.newDirList {
@ -183,18 +180,13 @@ func (i *Index) checkFix(forceUpdateDmg bool) {
} }
for _, name := range i.curDirList { for _, name := range i.curDirList {
if !m[name] { if !m[name] {
i.modified = true
if i.context.ShowMissing {
i.logDir(STATUS_MISSING, name+"/") i.logDir(STATUS_MISSING, name+"/")
i.setMod(true)
} }
} }
}
if len(i.newDirList) != len(i.curDirList) {
// added
i.modified = true
}
} }
}
func (i *Index) calcFile(name string, a string) (*idxInfo, error) { func (i *Index) calcFile(name string, a string) (*idxInfo, error) {
path := filepath.Join(i.path, name) path := filepath.Join(i.path, name)
@ -239,7 +231,7 @@ func (i *Index) save() (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
i.modified = false i.setMod(false)
return true, nil return true, nil
} else { } else {
return false, nil return false, nil
@ -249,11 +241,13 @@ func (i *Index) save() (bool, error) {
func (i *Index) load() error { func (i *Index) load() error {
if _, err := os.Stat(i.getIndexFilepath()); err != nil { if _, err := os.Stat(i.getIndexFilepath()); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
// todo
i.setMod(true)
return nil return nil
} }
return err return err
} }
i.modified = false i.setMod(false)
file, err := os.ReadFile(i.getIndexFilepath()) file, err := os.ReadFile(i.getIndexFilepath())
if err != nil { if err != nil {
return err return err
@ -275,7 +269,7 @@ func (i *Index) load() error {
} else { } else {
} }
if data.IdxHash != hashMd5(text) { if data.IdxHash != hashMd5(text) {
i.modified = true i.setMod(true)
i.logFile(STATUS_ERR_IDX, i.getIndexFilepath()) i.logFile(STATUS_ERR_IDX, i.getIndexFilepath())
} }
} else { } else {
@ -292,12 +286,12 @@ func (i *Index) load() error {
} }
} }
} }
// dirs
if data.Dir != nil { if data.Dir != nil {
slices.Sort(data.Dir) slices.Sort(data.Dir)
i.curDirList = data.Dir i.curDirList = data.Dir
if i.context.TrackDirectories && !reflect.DeepEqual(i.curDirList, i.newDirList) {
i.setMod(true)
}
} }
return nil return nil
} }

View File

@ -67,12 +67,7 @@ func setDate(filename string, r int) {
os.Chtimes(filename, date, date) os.Chtimes(filename, date, date)
} }
func genFile(path string, size int) { func genFile(dir string, a int) {
os.WriteFile(path, make([]byte, size), 0644)
setDate(path, size*size)
}
func genFiles(dir string, a int) {
os.MkdirAll(dir, 0755) os.MkdirAll(dir, 0755)
for i := 1; i <= 5; i++ { for i := 1; i <= 5; i++ {
size := a*i*wordIdx*100 + extIdx size := a*i*wordIdx*100 + extIdx
@ -83,7 +78,9 @@ func genFiles(dir string, a int) {
} }
file += "." + nextExt() file += "." + nextExt()
genFile(filepath.Join(dir, file), size) path := filepath.Join(dir, file)
os.WriteFile(path, make([]byte, size), 0644)
setDate(path, size*size)
} }
} }
@ -92,11 +89,11 @@ func genDir(root string) {
for i := 1; i <= 5; i++ { for i := 1; i <= 5; i++ {
dir := filepath.Join(root, start, nextWord()) dir := filepath.Join(root, start, nextWord())
genFiles(dir, 1) genFile(dir, 1)
if wordIdx%3 == 0 { if wordIdx%3 == 0 {
dir = filepath.Join(dir, nextWord()) dir = filepath.Join(dir, nextWord())
genFiles(dir, 1) genFile(dir, 1)
} }
} }
} }
@ -118,8 +115,6 @@ func setupMiscFiles() {
genDir(root) genDir(root)
os.MkdirAll(filepath.Join(root, "day/car/empty"), 0755)
rootPeople := filepath.Join(root, "people") rootPeople := filepath.Join(root, "people")
testPeople := filepath.Join(testDir, "people") testPeople := filepath.Join(testDir, "people")
@ -142,124 +137,58 @@ func TestRoot(t *testing.T) {
tool := getCmd() tool := getCmd()
root := filepath.Join(testDir, "root") root := filepath.Join(testDir, "root")
// update index, no recourse // step1: update index
t.Run("no-recourse", func(t *testing.T) {
cmd := exec.Command(tool, "-umR", filepath.Join(root, "day/office"))
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkOut(t, sout, "Processed 5 files")
checkOut(t, sout, "- 1 directory was updated")
checkOut(t, sout, "- 5 file hashes were added")
checkOut(t, sout, "- 0 file hashes were updated")
checkNotOut(t, sout, "removed")
})
// update remaining index from root
t.Run("update-remaining", func(t *testing.T) {
cmd := exec.Command(tool, "-um", root) cmd := exec.Command(tool, "-um", root)
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step1 failed with '%s'\n", err)
} }
sout := string(out) sout := string(out)
checkOut(t, sout, "Processed 300 files") checkOut(t, sout, "67 directories were updated")
checkOut(t, sout, "- 66 directories were updated") checkOut(t, sout, "300 file hashes were added")
checkOut(t, sout, "- 295 file hashes were added")
checkOut(t, sout, "- 0 file hashes were updated")
checkNotOut(t, sout, "removed") checkNotOut(t, sout, "removed")
})
// delete files, check for missing // step2: delete files, check for missing
t.Run("delete", func(t *testing.T) {
os.RemoveAll(filepath.Join(root, "thing/change")) os.RemoveAll(filepath.Join(root, "thing/change"))
os.Remove(filepath.Join(root, "time/hour/minute/body-information.csv")) os.Remove(filepath.Join(root, "time/hour/minute/body-information.csv"))
cmd := exec.Command(tool, "-m", root) cmd = exec.Command(tool, "-m", root)
out, err := cmd.Output() out, err = cmd.Output()
if err != nil { if err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step2 failed with '%s'\n", err)
} }
sout := string(out) sout = string(out)
checkOut(t, sout, "del /tmp/chkbit/root/thing/change/") checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
checkOut(t, sout, "2 files/directories would have been removed") checkOut(t, sout, "2 files/directories would have been removed")
})
// do not report missing without -m // step2a: do not report missing without -m
t.Run("no-missing", func(t *testing.T) { cmd = exec.Command(tool, root)
cmd := exec.Command(tool, root) out, err = cmd.Output()
out, err := cmd.Output()
if err != nil { if err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step2a failed with '%s'\n", err)
} }
sout := string(out) sout = string(out)
checkNotOut(t, sout, "del ") checkNotOut(t, sout, "del ")
checkNotOut(t, sout, "removed") checkNotOut(t, sout, "removed")
})
// check for missing and update // step3: check for missing and update
t.Run("missing", func(t *testing.T) { cmd = exec.Command(tool, "-um", root)
cmd := exec.Command(tool, "-um", root) out, err = cmd.Output()
out, err := cmd.Output()
if err != nil { if err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step3 failed with '%s'\n", err)
} }
sout := string(out) sout = string(out)
checkOut(t, sout, "del /tmp/chkbit/root/thing/change/") checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
checkOut(t, sout, "2 files/directories have been removed") checkOut(t, sout, "2 files/directories have been removed")
})
// check again // step4: check again
t.Run("repeat", func(t *testing.T) { cmd = exec.Command(tool, "-u", root)
for i := 0; i < 10; i++ { out, err = cmd.Output()
cmd := exec.Command(tool, "-uv", root)
out, err := cmd.Output()
if err != nil { if err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step4 failed with '%s'\n", err)
} }
sout := string(out) sout = string(out)
checkOut(t, sout, "Processed 289 files") checkOut(t, sout, "Processed 289 files")
checkNotOut(t, sout, "removed")
checkNotOut(t, sout, "updated")
checkNotOut(t, sout, "added")
}
})
// add files only
t.Run("add-only", func(t *testing.T) {
genFiles(filepath.Join(root, "way/add"), 99)
genFile(filepath.Join(root, "time/add-file.txt"), 500)
// modify existing, will not be reported:
genFile(filepath.Join(root, "way/job/word-business.mp3"), 500)
cmd := exec.Command(tool, "-a", root)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkOut(t, sout, "Processed 6 files")
checkOut(t, sout, "- 3 directories were updated")
checkOut(t, sout, "- 6 file hashes were added")
checkOut(t, sout, "- 0 file hashes were updated")
})
// update remaining
t.Run("update-remaining-add", func(t *testing.T) {
cmd := exec.Command(tool, "-u", root)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkOut(t, sout, "Processed 295 files")
checkOut(t, sout, "- 1 directory was updated")
checkOut(t, sout, "- 0 file hashes were added")
checkOut(t, sout, "- 1 file hash was updated")
})
} }
func TestDMG(t *testing.T) { func TestDMG(t *testing.T) {
@ -285,58 +214,50 @@ func TestDMG(t *testing.T) {
t2, _ := time.Parse(time.RFC3339, "2022-02-01T12:00:00Z") t2, _ := time.Parse(time.RFC3339, "2022-02-01T12:00:00Z")
t3, _ := time.Parse(time.RFC3339, "2022-02-01T13:00:00Z") t3, _ := time.Parse(time.RFC3339, "2022-02-01T13:00:00Z")
// create test and set the modified time" // step1: create test and set the modified time"
t.Run("create", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo1"), 0644) os.WriteFile(testFile, []byte("foo1"), 0644)
os.Chtimes(testFile, t2, t2) os.Chtimes(testFile, t2, t2)
cmd := exec.Command(tool, "-u", ".") cmd := exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil { if out, err := cmd.Output(); err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step1 failed with '%s'\n", err)
} else { } else {
checkOut(t, string(out), "new test.txt") checkOut(t, string(out), "new test.txt")
} }
})
// update test with different content & old modified (expect 'old')" // step2: update test with different content & old modified (expect 'old')"
t.Run("expect-old", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo2"), 0644) os.WriteFile(testFile, []byte("foo2"), 0644)
os.Chtimes(testFile, t1, t1) os.Chtimes(testFile, t1, t1)
cmd := exec.Command(tool, "-u", ".") cmd = exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil { if out, err := cmd.Output(); err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step2 failed with '%s'\n", err)
} else { } else {
checkOut(t, string(out), "old test.txt") checkOut(t, string(out), "old test.txt")
} }
})
// update test & new modified (expect 'upd')" // step3: update test & new modified (expect 'upd')"
t.Run("expect-upd", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo3"), 0644) os.WriteFile(testFile, []byte("foo3"), 0644)
os.Chtimes(testFile, t3, t3) os.Chtimes(testFile, t3, t3)
cmd := exec.Command(tool, "-u", ".") cmd = exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil { if out, err := cmd.Output(); err != nil {
t.Fatalf("failed with '%s'\n", err) t.Fatalf("step3 failed with '%s'\n", err)
} else { } else {
checkOut(t, string(out), "upd test.txt") checkOut(t, string(out), "upd test.txt")
} }
})
// Now update test with the same modified to simulate damage (expect DMG)" // step4: Now update test with the same modified to simulate damage (expect DMG)"
t.Run("expect-DMG", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo4"), 0644) os.WriteFile(testFile, []byte("foo4"), 0644)
os.Chtimes(testFile, t3, t3) os.Chtimes(testFile, t3, t3)
cmd := exec.Command(tool, "-u", ".") cmd = exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil { if out, err := cmd.Output(); err != nil {
if cmd.ProcessState.ExitCode() != 1 { if cmd.ProcessState.ExitCode() != 1 {
t.Fatalf("expected to fail with exit code 1 vs %d!", cmd.ProcessState.ExitCode()) t.Fatalf("step4 expected to fail with exit code 1 vs %d!", cmd.ProcessState.ExitCode())
} }
checkOut(t, string(out), "DMG test.txt") checkOut(t, string(out), "DMG test.txt")
} else { } else {
t.Fatal("expected to fail!") t.Fatal("step4 expected to fail!")
} }
})
} }