Compare commits

..

16 Commits

Author SHA1 Message Date
Christian Zangl
8eab0f1ceb
new --add-only mode 2024-08-22 16:02:58 +02:00
Christian Zangl
b9326ba7ab
fix tracking of removed files/dirs 2024-08-22 11:33:35 +02:00
Christian Zangl
9cd60dc8eb
improve help 2024-08-21 21:45:31 +02:00
Christian Zangl
ef18e44ef0
update flags (breaks -s -> -S, -f removed) 2024-08-21 21:18:29 +02:00
Christian Zangl
76c46c2cb4
add --no-recurse flag 2024-08-21 21:14:13 +02:00
Christian Zangl
fb45c82625
readme 2024-08-21 20:41:40 +02:00
Christian Zangl
94945925a4
fix go version path 2024-08-21 14:06:05 +02:00
Christian Zangl
d3f4629994
fix sync 2024-08-21 13:46:29 +02:00
Christian Zangl
a1327a4d0c
track directories and report missing files/dirs (-m) #16 2024-08-20 21:58:41 +02:00
Christian Zangl
181b3d8c9a
refactor 2024-08-20 16:45:07 +02:00
Christian Zangl
4bbd7b421e
refactor 2024-08-19 22:00:38 +02:00
Christian Zangl
16f38a9929
refactor 2024-08-19 20:48:51 +02:00
Christian Zangl
36bea4fdb7
readme 2024-08-19 16:42:51 +02:00
Christian Zangl
7951d22b75
new tests 2024-08-19 16:25:22 +02:00
Christian Zangl
6f454f1836
fix status 2024-08-18 21:36:22 +02:00
Christian Zangl
24b3a88576
fix status 2024-08-18 16:05:33 +02:00
17 changed files with 721 additions and 328 deletions

View File

@ -18,13 +18,9 @@ jobs:
- name: chkfmt - name: chkfmt
run: scripts/chkfmt run: scripts/chkfmt
- name: prep-test
run: scripts/run_test_prep
- name: tests - name: tests
run: | run: |
scripts/tests scripts/tests
scripts/run_tests
- name: xbuild - name: xbuild
run: scripts/xbuild run: scripts/xbuild

View File

@ -17,13 +17,9 @@ jobs:
- name: chkfmt - name: chkfmt
run: scripts/chkfmt run: scripts/chkfmt
- name: prep-test
run: scripts/run_test_prep
- name: tests - name: tests
run: | run: |
scripts/tests scripts/tests
scripts/run_tests
- name: xbuild - name: xbuild
run: version=${GITHUB_REF#$"refs/tags/v"} scripts/xbuild run: version=${GITHUB_REF#$"refs/tags/v"} scripts/xbuild

View File

@ -3,15 +3,16 @@
chkbit is a tool that ensures the safety of your files by checking if their *data integrity remains intact over time*, especially during transfers and backups. It helps detect issues like disk damage, filesystem errors, and malware interference. chkbit is a tool that ensures the safety of your files by checking if their *data integrity remains intact over time*, especially during transfers and backups. It helps detect issues like disk damage, filesystem errors, and malware interference.
![gif of chkbit](https://raw.githubusercontent.com/laktak/chkbit-py/readme/readme/chkbit-py.gif "chkbit") ![gif of chkbit](https://raw.githubusercontent.com/wiki/laktak/chkbit/readme/chkbit.gif "chkbit")
- [How it works](#how-it-works) - [How it works](#how-it-works)
- [Installation](#installation) - [Installation](#installation)
- [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)
- [Development](#development)
## How it works ## How it works
@ -27,11 +28,39 @@ Remember to always maintain multiple backups for comprehensive data protection.
## Installation ## Installation
- Install via [Homebrew](https://formulae.brew.sh/formula/chkbit) for macOS and Linux:
``` ### Binary releases
brew install chkbit
``` You can download the official chkbit binaries from the releases page and place it in your `PATH`.
- Download for [Linux, macOS or Windows](https://github.com/laktak/chkbit/releases).
- https://github.com/laktak/chkbit/releases
### Homebrew (macOS and Linux)
For macOS and Linux it can also be installed via [Homebrew](https://formulae.brew.sh/formula/chkbit):
```shell
brew install chkbit
```
### Build from Source
Building from the source requires Go.
- Either install it directly
```shell
go install github.com/laktak/chkbit/v5/cmd/chkbit@latest
```
- or clone and build
```shell
git clone https://github.com/laktak/chkbit
chkbit/scripts/build
# binary:
ls -l chkbit/chkbit
```
## Usage ## Usage
@ -55,13 +84,18 @@ Arguments:
Flags: Flags:
-h, --help Show context-sensitive help. -h, --help Show context-sensitive help.
-H, --tips Show tips. -H, --tips Show tips.
-u, --update update indices (without this chkbit will verify files in readonly mode) -c, --check check mode: chkbit will verify files in readonly mode (default mode)
--show-ignored-only only show ignored files -u, --update update mode: add and update indices
--algo="blake3" hash algorithm: md5, sha512, blake3 (default: blake3) -a, --add-only add mode: only add new files, do not check existing (quicker)
-f, --force force update of damaged items -i, --show-ignored-only show-ignored mode: only show ignored files
-s, --skip-symlinks do not follow symlinks -m, --show-missing show missing files/directories
--force force update of damaged items (advanced usage only)
-S, --skip-symlinks do not follow symlinks
-R, --no-recurse do not recurse into subdirectories
-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)
@ -71,6 +105,27 @@ Flags:
-V, --version show version information -V, --version show version information
``` ```
```
$ chkbit -H
.chkbitignore rules:
each line should contain exactly one name
you may use Unix shell-style wildcards (see README)
lines starting with '#' are skipped
lines starting with '/' are only applied to the current directory
Status codes:
DMG: error, data damage detected
EIX: error, index damaged
old: warning, file replaced by an older version
new: new file
upd: file updated
ok : check ok
del: file/directory removed
ign: ignored (see .chkbitignore)
EXC: exception/panic
```
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. 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.
@ -100,6 +155,17 @@ 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

@ -1,6 +1,6 @@
package main package main
var headerHelp = `Checks the data integrity of your files. var headerHelp = `Checks the data integrity of your files.
For help tips run "chkbit -H" or go to For help tips run "chkbit -H" or go to
https://github.com/laktak/chkbit https://github.com/laktak/chkbit
` `
@ -19,6 +19,7 @@ Status codes:
new: new file new: new file
upd: file updated upd: file updated
ok : check ok ok : check ok
del: file/directory removed
ign: ignored (see .chkbitignore) ign: ignored (see .chkbitignore)
EXC: exception/panic EXC: exception/panic
` `

View File

@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/laktak/chkbit" "github.com/laktak/chkbit/v5"
"github.com/laktak/chkbit/cmd/chkbit/util" "github.com/laktak/chkbit/v5/cmd/chkbit/util"
"github.com/laktak/lterm" "github.com/laktak/lterm"
) )
@ -25,7 +25,7 @@ const (
) )
const ( const (
updateInterval = time.Millisecond * 300 updateInterval = time.Millisecond * 700
sizeMB int64 = 1024 * 1024 sizeMB int64 = 1024 * 1024
) )
@ -44,13 +44,18 @@ 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."`
Update bool `short:"u" help:"update indices (without this chkbit will verify files in readonly mode)"` Check bool `short:"c" help:"check mode: chkbit will verify files in readonly mode (default mode)"`
ShowIgnoredOnly bool `help:"only show ignored files"` Update bool `short:"u" help:"update mode: add and update indices"`
Algo string `default:"blake3" help:"hash algorithm: md5, sha512, blake3 (default: blake3)"` AddOnly bool `short:"a" help:"add mode: only add new files, do not check existing (quicker)"`
Force bool `short:"f" help:"force update of damaged items"` ShowIgnoredOnly bool `short:"i" help:"show-ignored mode: only show ignored files"`
SkipSymlinks bool `short:"s" help:"do not follow symlinks"` ShowMissing bool `short:"m" help:"show missing files/directories"`
Force bool `help:"force update of damaged items (advanced usage only)"`
SkipSymlinks bool `short:"S" help:"do not follow symlinks"`
NoRecurse bool `short:"R" help:"do not recurse into subdirectories"`
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)"`
@ -61,76 +66,68 @@ var cli struct {
} }
type Main struct { type Main struct {
context *chkbit.Context
dmgList []string dmgList []string
errList []string errList []string
numIdxUpd int
numNew int
numUpd int
verbose bool verbose bool
logger *log.Logger logger *log.Logger
logVerbose bool logVerbose bool
progress Progress progress Progress
total int
termWidth int termWidth int
fps *RateCalc fps *util.RateCalc
bps *RateCalc bps *util.RateCalc
} }
func (m *Main) log(text string) { func (m *Main) log(text string) {
m.logger.Println(time.Now().UTC().Format("2006-01-02 15:04:05"), text) m.logger.Println(time.Now().UTC().Format("2006-01-02 15:04:05"), text)
} }
func (m *Main) logStatus(stat chkbit.Status, path string) { func (m *Main) logStatus(stat chkbit.Status, message string) bool {
if stat == chkbit.STATUS_UPDATE_INDEX { if stat == chkbit.STATUS_UPDATE_INDEX {
m.numIdxUpd++ return false
} else {
if stat == chkbit.STATUS_ERR_DMG {
m.total++
m.dmgList = append(m.dmgList, path)
} else if stat == chkbit.STATUS_PANIC {
m.errList = append(m.errList, path)
} else if stat == chkbit.STATUS_OK || stat == chkbit.STATUS_UPDATE || stat == chkbit.STATUS_NEW {
m.total++
if stat == chkbit.STATUS_UPDATE {
m.numUpd++
} else if stat == chkbit.STATUS_NEW {
m.numNew++
}
}
if m.logVerbose || stat != chkbit.STATUS_OK && stat != chkbit.STATUS_IGNORE {
m.log(stat.String() + " " + path)
}
if m.verbose || !stat.IsVerbose() {
col := ""
if stat.IsErrorOrWarning() {
col = termAlertFG
}
lterm.Printline(col, stat.String(), " ", path, lterm.Reset)
}
} }
if stat == chkbit.STATUS_ERR_DMG {
m.dmgList = append(m.dmgList, message)
} else if stat == chkbit.STATUS_PANIC {
m.errList = append(m.errList, message)
}
if m.logVerbose || !stat.IsVerbose() {
m.log(stat.String() + " " + message)
}
if m.verbose || !stat.IsVerbose() {
col := ""
if stat.IsErrorOrWarning() {
col = termAlertFG
}
lterm.Printline(col, stat.String(), " ", message, lterm.Reset)
return true
}
return false
} }
func (m *Main) showStatus(context *chkbit.Context) { func (m *Main) showStatus() {
last := time.Now().Add(-updateInterval) last := time.Now().Add(-updateInterval)
stat := "" stat := ""
for { for {
select { select {
case item := <-context.LogQueue: case item := <-m.context.LogQueue:
if item == nil { if item == nil {
if m.progress == Fancy { if m.progress == Fancy {
lterm.Printline("") lterm.Printline("")
} }
return return
} }
m.logStatus(item.Stat, item.Message) if m.logStatus(item.Stat, item.Message) {
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.total, "\r") fmt.Print(m.context.NumTotal, "\r")
}
} }
case perf := <-context.PerfQueue: case perf := <-m.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)
@ -140,11 +137,11 @@ func (m *Main) showStatus(context *chkbit.Context) {
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 !context.Update { if !m.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, context.NumWorkers, m.total, stat, m.context.NumWorkers, m.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)
@ -152,38 +149,49 @@ func (m *Main) showStatus(context *chkbit.Context) {
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.total, "\r") fmt.Print(m.context.NumTotal, "\r")
} }
} }
} }
} }
} }
func (m *Main) process() *chkbit.Context { func (m *Main) process() bool {
if cli.Update && cli.ShowIgnoredOnly { // verify mode
fmt.Println("Error: use either --update or --show-ignored-only!") var b01 = map[bool]int8{false: 0, true: 1}
return nil if b01[cli.Check]+b01[cli.Update]+b01[cli.AddOnly]+b01[cli.ShowIgnoredOnly] > 1 {
fmt.Println("Error: can only run one mode at a time!")
os.Exit(1)
} }
context, err := chkbit.NewContext(cli.Workers, cli.Force, cli.Update, cli.ShowIgnoredOnly, cli.Algo, cli.SkipSymlinks, cli.IndexName, cli.IgnoreName) var err error
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 nil return false
} }
m.context.ForceUpdateDmg = cli.Force
m.context.UpdateIndex = cli.Update || cli.AddOnly
m.context.AddOnly = cli.AddOnly
m.context.ShowIgnoredOnly = cli.ShowIgnoredOnly
m.context.ShowMissing = cli.ShowMissing
m.context.SkipSymlinks = cli.SkipSymlinks
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(context) m.showStatus()
}() }()
context.Start(cli.Paths) m.context.Start(cli.Paths)
wg.Wait() wg.Wait()
return context return true
} }
func (m *Main) printResult(context *chkbit.Context) { func (m *Main) printResult() {
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 {
@ -206,14 +214,14 @@ func (m *Main) printResult(context *chkbit.Context) {
if m.progress != Quiet { if m.progress != Quiet {
mode := "" mode := ""
if !context.Update { if !m.context.UpdateIndex {
mode = " in readonly mode" mode = " in readonly mode"
} }
status := fmt.Sprintf("Processed %s%s.", util.LangNum1MutateSuffix(m.total, "file"), mode) status := fmt.Sprintf("Processed %s%s.", util.LangNum1MutateSuffix(m.context.NumTotal, "file"), mode)
cprint(termOKFG, status) cprint(termOKFG, status)
m.log(status) m.log(status)
if m.progress == Fancy && m.total > 0 { if m.progress == Fancy && m.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")
@ -221,17 +229,26 @@ func (m *Main) printResult(context *chkbit.Context) {
fmt.Printf("- %.2f MB/second\n", (float64(m.bps.Total)+float64(m.bps.Current))/float64(sizeMB)/elapsedS) fmt.Printf("- %.2f MB/second\n", (float64(m.bps.Total)+float64(m.bps.Current))/float64(sizeMB)/elapsedS)
} }
if context.Update { del := ""
if m.numIdxUpd > 0 { if m.context.UpdateIndex {
cprint(termOKFG, fmt.Sprintf("- %s updated\n- %s added\n- %s updated", if m.context.NumIdxUpd > 0 {
util.LangNum1Choice(m.numIdxUpd, "directory was", "directories were"), if m.context.NumDel > 0 {
util.LangNum1Choice(m.numNew, "file hash was", "file hashes were"), del = fmt.Sprintf("\n- %s been removed", util.LangNum1Choice(m.context.NumDel, "file/directory has", "files/directories have"))
util.LangNum1Choice(m.numUpd, "file hash was", "file hashes were"))) }
cprint(termOKFG, fmt.Sprintf("- %s updated\n- %s added\n- %s updated%s",
util.LangNum1Choice(m.context.NumIdxUpd, "directory was", "directories were"),
util.LangNum1Choice(m.context.NumNew, "file hash was", "file hashes were"),
util.LangNum1Choice(m.context.NumUpd, "file hash was", "file hashes were"),
del))
} }
} else if m.numNew+m.numUpd > 0 { } else if m.context.NumNew+m.context.NumUpd+m.context.NumDel > 0 {
cprint(termAlertFG, fmt.Sprintf("No changes were made (specify -u to update):\n- %s would have been added and\n- %s would have been updated.", if m.context.NumDel > 0 {
util.LangNum1MutateSuffix(m.numNew, "file"), del = fmt.Sprintf("\n- %s would have been removed", util.LangNum1Choice(m.context.NumDel, "file/directory", "files/directories"))
util.LangNum1MutateSuffix(m.numUpd, "file"))) }
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(m.context.NumUpd, "file"),
del))
} }
} }
@ -307,9 +324,8 @@ 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, ", "))
context := m.process() if m.process() && !m.context.ShowIgnoredOnly {
if context != nil && !context.ShowIgnoredOnly { m.printResult()
m.printResult(context)
} }
} else { } else {
fmt.Println("specify a path to check, see -h") fmt.Println("specify a path to check, see -h")
@ -328,8 +344,8 @@ func main() {
m := &Main{ m := &Main{
logger: log.New(io.Discard, "", 0), logger: log.New(io.Discard, "", 0),
termWidth: termWidth, termWidth: termWidth,
fps: NewRateCalc(time.Second, (termWidth-70)/2), fps: util.NewRateCalc(time.Second, (termWidth-70)/2),
bps: NewRateCalc(time.Second, (termWidth-70)/2), bps: util.NewRateCalc(time.Second, (termWidth-70)/2),
} }
m.run() m.run()
} }

View File

@ -1,4 +1,4 @@
package main package util
import ( import (
"time" "time"

View File

@ -8,21 +8,32 @@ import (
) )
type Context struct { type Context struct {
NumWorkers int NumWorkers int
Force bool UpdateIndex bool
Update bool AddOnly bool
ShowIgnoredOnly bool ShowIgnoredOnly bool
HashAlgo string ShowMissing bool
SkipSymlinks bool ForceUpdateDmg bool
IndexFilename string HashAlgo string
IgnoreFilename string TrackDirectories bool
WorkQueue chan *WorkItem SkipSymlinks bool
LogQueue chan *LogEvent SkipSubdirectories bool
PerfQueue chan *PerfEvent IndexFilename string
wg sync.WaitGroup IgnoreFilename string
WorkQueue chan *WorkItem
LogQueue chan *LogEvent
PerfQueue chan *PerfEvent
wg sync.WaitGroup
mutex sync.Mutex
NumTotal int
NumIdxUpd int
NumNew int
NumUpd int
NumDel int
} }
func NewContext(numWorkers int, force bool, update bool, showIgnoredOnly bool, hashAlgo string, skipSymlinks bool, indexFilename string, ignoreFilename string) (*Context, error) { func NewContext(numWorkers int, hashAlgo string, indexFilename string, ignoreFilename string) (*Context, error) {
if indexFilename[0] != '.' { if indexFilename[0] != '.' {
return nil, errors.New("The index filename must start with a dot!") return nil, errors.New("The index filename must start with a dot!")
} }
@ -33,21 +44,44 @@ func NewContext(numWorkers int, force bool, update bool, showIgnoredOnly bool, h
return nil, errors.New(hashAlgo + " is unknown.") return nil, errors.New(hashAlgo + " is unknown.")
} }
return &Context{ return &Context{
NumWorkers: numWorkers, NumWorkers: numWorkers,
Force: force, HashAlgo: hashAlgo,
Update: update, IndexFilename: indexFilename,
ShowIgnoredOnly: showIgnoredOnly, IgnoreFilename: ignoreFilename,
HashAlgo: hashAlgo, WorkQueue: make(chan *WorkItem, numWorkers*10),
SkipSymlinks: skipSymlinks, LogQueue: make(chan *LogEvent, numWorkers*100),
IndexFilename: indexFilename, PerfQueue: make(chan *PerfEvent, numWorkers*10),
IgnoreFilename: ignoreFilename,
WorkQueue: make(chan *WorkItem, numWorkers*10),
LogQueue: make(chan *LogEvent, numWorkers*100),
PerfQueue: make(chan *PerfEvent, numWorkers*10),
}, nil }, nil
} }
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 {
case STATUS_ERR_DMG:
context.NumTotal++
case STATUS_UPDATE_INDEX:
context.NumIdxUpd++
case STATUS_UP_WARN_OLD:
context.NumTotal++
context.NumUpd++
case STATUS_UPDATE:
context.NumTotal++
context.NumUpd++
case STATUS_NEW:
context.NumTotal++
context.NumNew++
case STATUS_OK:
if !context.AddOnly {
context.NumTotal++
}
case STATUS_MISSING:
context.NumDel++
//case STATUS_PANIC:
//case STATUS_ERR_IDX:
//case STATUS_IGNORE:
}
context.LogQueue <- &LogEvent{stat, message} context.LogQueue <- &LogEvent{stat, message}
} }
@ -63,8 +97,8 @@ func (context *Context) perfMonBytes(numBytes int64) {
context.PerfQueue <- &PerfEvent{0, numBytes} context.PerfQueue <- &PerfEvent{0, numBytes}
} }
func (context *Context) addWork(path string, filesToIndex []string, ignore *Ignore) { func (context *Context) addWork(path string, filesToIndex []string, dirList []string, ignore *Ignore) {
context.WorkQueue <- &WorkItem{path, filesToIndex, ignore} context.WorkQueue <- &WorkItem{path, filesToIndex, dirList, ignore}
} }
func (context *Context) endWork() { func (context *Context) endWork() {
@ -76,12 +110,18 @@ func (context *Context) isChkbitFile(name string) bool {
} }
func (context *Context) Start(pathList []string) { func (context *Context) Start(pathList []string) {
context.NumTotal = 0
context.NumIdxUpd = 0
context.NumNew = 0
context.NumUpd = 0
context.NumDel = 0
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(context.NumWorkers) wg.Add(context.NumWorkers)
for i := 0; i < context.NumWorkers; i++ { for i := 0; i < context.NumWorkers; i++ {
go func(id int) { go func(id int) {
defer wg.Done() defer wg.Done()
context.RunWorker(id) context.runWorker(id)
}(i) }(i)
} }
go func() { go func() {
@ -121,6 +161,11 @@ func (context *Context) scanDir(root string, parentIgnore *Ignore) {
var dirList []string var dirList []string
var filesToIndex []string var filesToIndex []string
ignore, err := GetIgnore(context, root, parentIgnore)
if err != nil {
context.logErr(root+"/", err)
}
for _, file := range files { for _, file := range files {
path := filepath.Join(root, file.Name()) path := filepath.Join(root, file.Name())
if file.Name()[0] == '.' { if file.Name()[0] == '.' {
@ -130,24 +175,21 @@ func (context *Context) scanDir(root string, parentIgnore *Ignore) {
continue continue
} }
if isDir(file, path) { if isDir(file, path) {
dirList = append(dirList, file.Name()) if !ignore.shouldIgnore(file.Name()) {
dirList = append(dirList, file.Name())
} else {
context.log(STATUS_IGNORE, file.Name()+"/")
}
} else if file.Type().IsRegular() { } else if file.Type().IsRegular() {
filesToIndex = append(filesToIndex, file.Name()) filesToIndex = append(filesToIndex, file.Name())
} }
} }
ignore, err := GetIgnore(context, root, parentIgnore) context.addWork(root, filesToIndex, dirList, ignore)
if err != nil {
context.logErr(root+"/", err)
}
context.addWork(root, filesToIndex, ignore) if !context.SkipSubdirectories {
for _, name := range dirList {
for _, name := range dirList {
if !ignore.shouldIgnore(name) {
context.scanDir(filepath.Join(root, name), ignore) context.scanDir(filepath.Join(root, name), ignore)
} else {
context.log(STATUS_IGNORE, name+"/")
} }
} }
} }

2
go.mod
View File

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

View File

@ -50,7 +50,7 @@ func Hashfile(path string, hashAlgo string, perfMonBytes func(int64)) (string, e
return hex.EncodeToString(h.Sum(nil)), nil return hex.EncodeToString(h.Sum(nil)), nil
} }
func HashMd5(data []byte) string { func hashMd5(data []byte) string {
h := md5.New() h := md5.New()
h.Write(data) h.Write(data)
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))

155
index.go
View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"os" "os"
"path/filepath" "path/filepath"
"slices"
) )
const VERSION = 2 // index version const VERSION = 2 // index version
@ -12,47 +13,54 @@ var (
algoMd5 = "md5" algoMd5 = "md5"
) )
type IdxInfo struct { type idxInfo struct {
ModTime int64 `json:"mod"` ModTime int64 `json:"mod"`
Algo *string `json:"a,omitempty"` Algo *string `json:"a,omitempty"`
Hash *string `json:"h,omitempty"` Hash *string `json:"h,omitempty"`
LegacyHash *string `json:"md5,omitempty"` LegacyHash *string `json:"md5,omitempty"`
} }
type IndexFile struct { type indexFile struct {
V int `json:"v"` V int `json:"v"`
// IdxRaw -> map[string]idxInfo
IdxRaw json.RawMessage `json:"idx"` IdxRaw json.RawMessage `json:"idx"`
IdxHash string `json:"idx_hash"` IdxHash string `json:"idx_hash"`
// 2024-08 optional, list of subdirectories
Dir []string `json:"dirlist,omitempty"`
} }
type IdxInfo1 struct { type idxInfo1 struct {
ModTime int64 `json:"mod"` ModTime int64 `json:"mod"`
Hash string `json:"md5"` Hash string `json:"md5"`
} }
type IndexFile1 struct { type indexFile1 struct {
Data map[string]IdxInfo1 `json:"data"` Data map[string]idxInfo1 `json:"data"`
} }
type Index struct { type Index struct {
context *Context context *Context
path string path string
files []string files []string
cur map[string]IdxInfo cur map[string]idxInfo
new map[string]IdxInfo new map[string]idxInfo
updates []string curDirList []string
modified bool newDirList []string
readonly bool modified bool
readonly bool
} }
func NewIndex(context *Context, path string, files []string, readonly bool) *Index { func newIndex(context *Context, path string, files []string, dirList []string, readonly bool) *Index {
slices.Sort(dirList)
return &Index{ return &Index{
context: context, context: context,
path: path, path: path,
files: files, files: files,
cur: make(map[string]IdxInfo), cur: make(map[string]idxInfo),
new: make(map[string]IdxInfo), new: make(map[string]idxInfo),
readonly: readonly, curDirList: make([]string, 0),
newDirList: dirList,
readonly: readonly,
} }
} }
@ -60,10 +68,6 @@ 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)
} }
@ -72,6 +76,10 @@ func (i *Index) logFile(stat Status, name string) {
i.context.log(stat, filepath.Join(i.path, name)) i.context.log(stat, filepath.Join(i.path, name))
} }
func (i *Index) logDir(stat Status, name string) {
i.context.log(stat, filepath.Join(i.path, name)+"/")
}
func (i *Index) calcHashes(ignore *Ignore) { func (i *Index) calcHashes(ignore *Ignore) {
for _, name := range i.files { for _, name := range i.files {
if ignore != nil && ignore.shouldIgnore(name) { if ignore != nil && ignore.shouldIgnore(name) {
@ -80,13 +88,13 @@ func (i *Index) calcHashes(ignore *Ignore) {
} }
var err error var err error
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 // existing file
if val.LegacyHash != nil { if val.LegacyHash != nil {
// convert from py1 to new format // convert from py1 to new format
val = IdxInfo{ val = idxInfo{
ModTime: val.ModTime, ModTime: val.ModTime,
Algo: &algoMd5, Algo: &algoMd5,
Hash: val.LegacyHash, Hash: val.LegacyHash,
@ -96,10 +104,15 @@ func (i *Index) calcHashes(ignore *Ignore) {
if val.Algo != nil { if val.Algo != nil {
algo = *val.Algo algo = *val.Algo
} }
info, err = i.calcFile(name, algo) if i.context.AddOnly {
info = &val
} else {
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 {
info, err = i.calcFile(name, algo) info, err = i.calcFile(name, algo)
} }
@ -120,42 +133,70 @@ func (i *Index) showIgnoredOnly(ignore *Ignore) {
} }
} }
func (i *Index) checkFix(force bool) { 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.setMod(true) i.modified = true
continue
} 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.setMod(true) i.modified = true
} }
continue continue
} }
if amod == bmod { if amod == bmod {
i.logFile(STATUS_ERR_DMG, name) i.logFile(STATUS_ERR_DMG, name)
if !force { if !forceUpdateDmg {
// keep DMG entry
i.new[name] = a i.new[name] = a
} else { } else {
i.setMod(true) i.modified = true
} }
} else if amod < bmod { } else if amod < bmod {
i.logFile(STATUS_UPDATE, name) i.logFile(STATUS_UPDATE, name)
i.setMod(true) i.modified = true
} else if amod > bmod { } else if amod > bmod {
i.logFile(STATUS_WARN_OLD, name) i.logFile(STATUS_UP_WARN_OLD, name)
i.setMod(true) i.modified = true
} }
} }
} }
// track missing
for name := range i.cur {
if _, ok := i.new[name]; !ok {
i.modified = true
if i.context.ShowMissing {
i.logFile(STATUS_MISSING, name)
}
}
}
// dirs
m := make(map[string]bool)
for _, n := range i.newDirList {
m[n] = true
}
for _, name := range i.curDirList {
if !m[name] {
i.modified = true
if i.context.ShowMissing {
i.logDir(STATUS_MISSING, name+"/")
}
}
}
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)
info, _ := os.Stat(path) info, _ := os.Stat(path)
mtime := int64(info.ModTime().UnixNano() / 1e6) mtime := int64(info.ModTime().UnixNano() / 1e6)
@ -164,7 +205,7 @@ func (i *Index) calcFile(name string, a string) (*IdxInfo, error) {
return nil, err return nil, err
} }
i.context.perfMonFiles(1) i.context.perfMonFiles(1)
return &IdxInfo{ return &idxInfo{
ModTime: mtime, ModTime: mtime,
Algo: &a, Algo: &a,
Hash: &h, Hash: &h,
@ -181,10 +222,13 @@ func (i *Index) save() (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
data := IndexFile{ data := indexFile{
V: VERSION, V: VERSION,
IdxRaw: text, IdxRaw: text,
IdxHash: HashMd5(text), IdxHash: hashMd5(text),
}
if i.context.TrackDirectories {
data.Dir = i.newDirList
} }
file, err := json.Marshal(data) file, err := json.Marshal(data)
@ -195,7 +239,7 @@ func (i *Index) save() (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
i.setMod(false) i.modified = false
return true, nil return true, nil
} else { } else {
return false, nil return false, nil
@ -209,12 +253,12 @@ func (i *Index) load() error {
} }
return err return err
} }
i.setMod(false) i.modified = false
file, err := os.ReadFile(i.getIndexFilepath()) file, err := os.ReadFile(i.getIndexFilepath())
if err != nil { if err != nil {
return err return err
} }
var data IndexFile var data indexFile
err = json.Unmarshal(file, &data) err = json.Unmarshal(file, &data)
if err != nil { if err != nil {
return err return err
@ -225,22 +269,22 @@ func (i *Index) load() error {
return err return err
} }
text := data.IdxRaw text := data.IdxRaw
if data.IdxHash != HashMd5(text) { if data.IdxHash != hashMd5(text) {
// old versions may have save the JSON encoded with extra spaces // old versions may have saved the JSON encoded with extra spaces
text, _ = json.Marshal(data.IdxRaw) text, _ = json.Marshal(data.IdxRaw)
} else { } else {
} }
if data.IdxHash != HashMd5(text) { if data.IdxHash != hashMd5(text) {
i.setMod(true) i.modified = true
i.logFile(STATUS_ERR_IDX, i.getIndexFilepath()) i.logFile(STATUS_ERR_IDX, i.getIndexFilepath())
} }
} else { } else {
var data1 IndexFile1 var data1 indexFile1
json.Unmarshal(file, &data1) json.Unmarshal(file, &data1)
if data1.Data != nil { if data1.Data != nil {
// convert from js to new format // convert from js to new format
for name, item := range data1.Data { for name, item := range data1.Data {
i.cur[name] = IdxInfo{ i.cur[name] = idxInfo{
ModTime: item.ModTime, ModTime: item.ModTime,
Algo: &algoMd5, Algo: &algoMd5,
Hash: &item.Hash, Hash: &item.Hash,
@ -248,5 +292,12 @@ func (i *Index) load() error {
} }
} }
} }
// dirs
if data.Dir != nil {
slices.Sort(data.Dir)
i.curDirList = data.Dir
}
return nil return nil
} }

342
scripts/run_test.go Normal file
View File

@ -0,0 +1,342 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
// perform integration test using the compiled binary
var testDir = "/tmp/chkbit"
func getCmd() string {
_, filename, _, _ := runtime.Caller(0)
prjRoot := filepath.Dir(filepath.Dir(filename))
return filepath.Join(prjRoot, "chkbit")
}
func checkOut(t *testing.T, sout string, expected string) {
if !strings.Contains(sout, expected) {
t.Errorf("Expected '%s' in output, got '%s'\n", expected, sout)
}
}
func checkNotOut(t *testing.T, sout string, notExpected string) {
if strings.Contains(sout, notExpected) {
t.Errorf("Did not expect '%s' in output, got '%s'\n", notExpected, sout)
}
}
// misc files
var (
startList = []string{"time", "year", "people", "way", "day", "thing"}
wordList = []string{"life", "world", "school", "state", "family", "student", "group", "country", "problem", "hand", "part", "place", "case", "week", "company", "system", "program", "work", "government", "number", "night", "point", "home", "water", "room", "mother", "area", "money", "story", "fact", "month", "lot", "right", "study", "book", "eye", "job", "word", "business", "issue", "side", "kind", "head", "house", "service", "friend", "father", "power", "hour", "game", "line", "end", "member", "law", "car", "city", "community", "name", "president", "team", "minute", "idea", "kid", "body", "information", "back", "face", "others", "level", "office", "door", "health", "person", "art", "war", "history", "party", "result", "change", "morning", "reason", "research", "moment", "air", "teacher", "force", "education"}
extList = []string{"txt", "md", "pdf", "jpg", "jpeg", "png", "mp4", "mp3", "csv"}
startDate = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
endDate = time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC)
dateList = []time.Time{}
wordIdx = 0
extIdx = 0
dateIdx = 0
)
func nextWord() string {
word := wordList[wordIdx%len(wordList)]
wordIdx++
return word
}
func nextExt() string {
ext := extList[extIdx%len(extList)]
extIdx++
return ext
}
func setDate(filename string, r int) {
date := dateList[dateIdx%len(dateList)]
m := 17 * dateIdx / len(dateList)
date = date.Add(time.Duration(m) * time.Hour)
dateIdx++
os.Chtimes(filename, date, date)
}
func genFile(path string, size int) {
os.WriteFile(path, make([]byte, size), 0644)
setDate(path, size*size)
}
func genFiles(dir string, a int) {
os.MkdirAll(dir, 0755)
for i := 1; i <= 5; i++ {
size := a*i*wordIdx*100 + extIdx
file := nextWord() + "-" + nextWord()
if i%3 == 0 {
file += "-" + nextWord()
}
file += "." + nextExt()
genFile(filepath.Join(dir, file), size)
}
}
func genDir(root string) {
for _, start := range startList {
for i := 1; i <= 5; i++ {
dir := filepath.Join(root, start, nextWord())
genFiles(dir, 1)
if wordIdx%3 == 0 {
dir = filepath.Join(dir, nextWord())
genFiles(dir, 1)
}
}
}
}
func setupMiscFiles() {
var c int64 = 50
interval := (int64)(endDate.Sub(startDate).Seconds()) / c
for i := range make([]int64, c) {
dateList = append(dateList, startDate.Add(time.Duration(interval*(int64)(i))*time.Second))
}
root := filepath.Join(testDir, "root")
if err := os.RemoveAll(testDir); err != nil {
fmt.Println("Failed to clean", err)
panic(err)
}
genDir(root)
os.MkdirAll(filepath.Join(root, "day/car/empty"), 0755)
rootPeople := filepath.Join(root, "people")
testPeople := filepath.Join(testDir, "people")
err := os.Rename(rootPeople, testPeople)
if err != nil {
fmt.Println("Rename failed", err)
panic(err)
}
err = os.Symlink(testPeople, rootPeople)
if err != nil {
fmt.Println("Symlink failed", err)
panic(err)
}
}
func TestRoot(t *testing.T) {
setupMiscFiles()
tool := getCmd()
root := filepath.Join(testDir, "root")
// update index, no recourse
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)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkOut(t, sout, "Processed 300 files")
checkOut(t, sout, "- 66 directories were updated")
checkOut(t, sout, "- 295 file hashes were added")
checkOut(t, sout, "- 0 file hashes were updated")
checkNotOut(t, sout, "removed")
})
// delete files, check for missing
t.Run("delete", func(t *testing.T) {
os.RemoveAll(filepath.Join(root, "thing/change"))
os.Remove(filepath.Join(root, "time/hour/minute/body-information.csv"))
cmd := exec.Command(tool, "-m", root)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
checkOut(t, sout, "2 files/directories would have been removed")
})
// do not report missing without -m
t.Run("no-missing", func(t *testing.T) {
cmd := exec.Command(tool, root)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkNotOut(t, sout, "del ")
checkNotOut(t, sout, "removed")
})
// check for missing and update
t.Run("missing", func(t *testing.T) {
cmd := exec.Command(tool, "-um", root)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
checkOut(t, sout, "2 files/directories have been removed")
})
// check again
t.Run("repeat", func(t *testing.T) {
for i := 0; i < 10; i++ {
cmd := exec.Command(tool, "-uv", root)
out, err := cmd.Output()
if err != nil {
t.Fatalf("failed with '%s'\n", err)
}
sout := string(out)
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) {
testDmg := filepath.Join(testDir, "test_dmg")
if err := os.RemoveAll(testDmg); err != nil {
fmt.Println("Failed to clean", err)
panic(err)
}
if err := os.MkdirAll(testDmg, 0755); err != nil {
fmt.Println("Failed to create test directory", err)
panic(err)
}
if err := os.Chdir(testDmg); err != nil {
fmt.Println("Failed to cd test directory", err)
panic(err)
}
tool := getCmd()
testFile := filepath.Join(testDmg, "test.txt")
t1, _ := time.Parse(time.RFC3339, "2022-02-01T11:00:00Z")
t2, _ := time.Parse(time.RFC3339, "2022-02-01T12:00:00Z")
t3, _ := time.Parse(time.RFC3339, "2022-02-01T13:00:00Z")
// create test and set the modified time"
t.Run("create", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo1"), 0644)
os.Chtimes(testFile, t2, t2)
cmd := exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil {
t.Fatalf("failed with '%s'\n", err)
} else {
checkOut(t, string(out), "new test.txt")
}
})
// update test with different content & old modified (expect 'old')"
t.Run("expect-old", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo2"), 0644)
os.Chtimes(testFile, t1, t1)
cmd := exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil {
t.Fatalf("failed with '%s'\n", err)
} else {
checkOut(t, string(out), "old test.txt")
}
})
// update test & new modified (expect 'upd')"
t.Run("expect-upd", func(t *testing.T) {
os.WriteFile(testFile, []byte("foo3"), 0644)
os.Chtimes(testFile, t3, t3)
cmd := exec.Command(tool, "-u", ".")
if out, err := cmd.Output(); err != nil {
t.Fatalf("failed with '%s'\n", err)
} else {
checkOut(t, string(out), "upd test.txt")
}
})
// 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.Chtimes(testFile, t3, t3)
cmd := exec.Command(tool, "-u", ".")
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())
}
checkOut(t, string(out), "DMG test.txt")
} else {
t.Fatal("expected to fail!")
}
})
}

View File

@ -1,13 +0,0 @@
#!/bin/bash
export TZ='UTC'
root="/tmp/chkbit"
go run scripts/run_test_prep.go
cd $root/root
mv $root/root/people $root/people
ln -s ../people people
ln -s ../../people/face/office-door.pdf day/friend/office-door.pdf
find -L | wc -l

View File

@ -1,89 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"time"
)
var (
startList = []string{"time", "year", "people", "way", "day", "thing"}
wordList = []string{"life", "world", "school", "state", "family", "student", "group", "country", "problem", "hand", "part", "place", "case", "week", "company", "system", "program", "work", "government", "number", "night", "point", "home", "water", "room", "mother", "area", "money", "story", "fact", "month", "lot", "right", "study", "book", "eye", "job", "word", "business", "issue", "side", "kind", "head", "house", "service", "friend", "father", "power", "hour", "game", "line", "end", "member", "law", "car", "city", "community", "name", "president", "team", "minute", "idea", "kid", "body", "information", "back", "face", "others", "level", "office", "door", "health", "person", "art", "war", "history", "party", "result", "change", "morning", "reason", "research", "moment", "air", "teacher", "force", "education"}
extList = []string{"txt", "md", "pdf", "jpg", "jpeg", "png", "mp4", "mp3", "csv"}
startDate = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
endDate = time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC)
dateList = []time.Time{}
wordIdx = 0
extIdx = 0
dateIdx = 0
)
func nextWord() string {
word := wordList[wordIdx%len(wordList)]
wordIdx++
return word
}
func nextExt() string {
ext := extList[extIdx%len(extList)]
extIdx++
return ext
}
func setDate(filename string, r int) {
date := dateList[dateIdx%len(dateList)]
m := 17 * dateIdx / len(dateList)
date = date.Add(time.Duration(m) * time.Hour)
dateIdx++
os.Chtimes(filename, date, date)
}
func genFile(dir string, a int) {
os.MkdirAll(dir, 0755)
for i := 1; i <= 5; i++ {
size := a*i*wordIdx*100 + extIdx
file := nextWord() + "-" + nextWord()
if i%3 == 0 {
file += "-" + nextWord()
}
file += "." + nextExt()
path := filepath.Join(dir, file)
os.WriteFile(path, make([]byte, size), 0644)
setDate(path, size*size)
}
}
func genDir(root string) {
for _, start := range startList {
for i := 1; i <= 5; i++ {
dir := filepath.Join(root, start, nextWord())
genFile(dir, 1)
if wordIdx%3 == 0 {
dir = filepath.Join(dir, nextWord())
genFile(dir, 1)
}
}
}
}
func main() {
root := "/tmp/chkbit"
var c int64 = 50
interval := (int64)(endDate.Sub(startDate).Seconds()) / c
for i := range make([]int64, c) {
dateList = append(dateList, startDate.Add(time.Duration(interval*(int64)(i))*time.Second))
}
if err := os.RemoveAll(root); err == nil {
genDir(filepath.Join(root, "root"))
fmt.Println("Ready.")
} else {
fmt.Println("Failed to clean")
}
}

View File

@ -1,21 +0,0 @@
#!/bin/bash
set -e
export TZ='UTC'
script_dir=$(dirname "$(realpath "$0")")
base_dir=$(dirname "$script_dir")
#dir=$(realpath "$script_dir/../testdata/run_test")
root="/tmp/chkbit/root"
if [[ ! -d $root ]]; then
echo "must run run_test_prep first"
exit 1
fi
# setup
$script_dir/build
"$base_dir/chkbit" -u /tmp/chkbit
# todo: validate

View File

@ -4,4 +4,8 @@ set -e
script_dir=$(dirname "$(realpath "$0")") script_dir=$(dirname "$(realpath "$0")")
cd $script_dir/.. cd $script_dir/..
go test -v ./cmd/chkbit/util # prep
$script_dir/build
go test -v ./cmd/chkbit/util -count=1
go test -v ./scripts -count=1

View File

@ -4,14 +4,15 @@ type Status string
const ( const (
STATUS_PANIC Status = "EXC" STATUS_PANIC Status = "EXC"
STATUS_ERR_DMG Status = "DMG"
STATUS_ERR_IDX Status = "EIX" STATUS_ERR_IDX Status = "EIX"
STATUS_WARN_OLD Status = "old" STATUS_ERR_DMG Status = "DMG"
STATUS_NEW Status = "new" STATUS_UPDATE_INDEX Status = "iup"
STATUS_UP_WARN_OLD Status = "old"
STATUS_UPDATE Status = "upd" STATUS_UPDATE Status = "upd"
STATUS_NEW Status = "new"
STATUS_OK Status = "ok " STATUS_OK Status = "ok "
STATUS_IGNORE Status = "ign" STATUS_IGNORE Status = "ign"
STATUS_UPDATE_INDEX Status = "iup" STATUS_MISSING Status = "del"
) )
func (s Status) String() string { func (s Status) String() string {
@ -19,7 +20,7 @@ func (s Status) String() string {
} }
func (s Status) IsErrorOrWarning() bool { func (s Status) IsErrorOrWarning() bool {
return s == STATUS_PANIC || s == STATUS_ERR_DMG || s == STATUS_ERR_IDX || s == STATUS_WARN_OLD return s == STATUS_PANIC || s == STATUS_ERR_DMG || s == STATUS_ERR_IDX || s == STATUS_UP_WARN_OLD
} }
func (s Status) IsVerbose() bool { func (s Status) IsVerbose() bool {

View File

@ -3,17 +3,18 @@ package chkbit
type WorkItem struct { type WorkItem struct {
path string path string
filesToIndex []string filesToIndex []string
dirList []string
ignore *Ignore ignore *Ignore
} }
func (context *Context) RunWorker(id int) { func (context *Context) runWorker(id int) {
for { for {
item := <-context.WorkQueue item := <-context.WorkQueue
if item == nil { if item == nil {
break break
} }
index := NewIndex(context, item.path, item.filesToIndex, !context.Update) index := newIndex(context, item.path, item.filesToIndex, item.dirList, !context.UpdateIndex)
err := index.load() err := index.load()
if err != nil { if err != nil {
context.log(STATUS_PANIC, index.getIndexFilepath()+": "+err.Error()) context.log(STATUS_PANIC, index.getIndexFilepath()+": "+err.Error())
@ -23,9 +24,9 @@ func (context *Context) RunWorker(id int) {
index.showIgnoredOnly(item.ignore) index.showIgnoredOnly(item.ignore)
} else { } else {
index.calcHashes(item.ignore) index.calcHashes(item.ignore)
index.checkFix(context.Force) index.checkFix(context.ForceUpdateDmg)
if context.Update { if context.UpdateIndex {
if changed, err := index.save(); err != nil { if changed, err := index.save(); err != nil {
context.logErr(item.path, err) context.logErr(item.path, err)
} else if changed { } else if changed {