Store mtime to the nanosecond so a same-second rewrite is re-hashed (closes #12)
check / check (push) Waiting to run

scan recorded mtime in whole seconds, so a file rewritten in place at
the same size within the same second as its recorded mtime was classed
unchanged and kept its old hashes. The files table keeps mtime as whole
Unix seconds and gains mtime_nsec, the nanoseconds within that second.
scan holds the mtime as a time.Time and decides "newer" with After, so
any time a filesystem can record, one after 2262 included, compares in
the right order. The walk, a file given as an operand, and the content
phase's recheck all move over. PRAGMA user_version stays 1, per the
owner's ruling. README states what both columns hold.

Model: opus-5-5
This commit is contained in:
2026-10-07 21:46:12 +00:00
parent 0064eba542
commit 8e814bf0ba
8 changed files with 243 additions and 68 deletions
+8 -7
View File
@@ -17,6 +17,7 @@ import (
"strings"
"sync"
"syscall"
"time"
)
// The duplicate ladder (see hashSignature and README "Duplicate
@@ -68,7 +69,7 @@ var errInterrupted = errors.New("scan interrupted")
type fileRec struct {
path string
size int64
mtime int64
mtime time.Time
dev uint64
ino uint64
}
@@ -79,7 +80,7 @@ type fileRec struct {
// they would dominate the scan's memory.
type fileMeta struct {
size int64
mtime int64
mtime time.Time
hashed bool
}
@@ -369,7 +370,7 @@ func (s *scanState) loadIndex(ctx context.Context, roots []string) error {
s.existing = make(map[string]fileMeta)
return loadFileMeta(ctx, s.db,
func(path string, size, mtime int64, hashed bool) {
func(path string, size int64, mtime time.Time, hashed bool) {
prog.increment()
if underAnyRoot(path, roots) {
@@ -415,7 +416,7 @@ func (s *scanState) walkPhase(
old, ok := s.existing[ev.rec.path]
switch {
case !ok || old.size != ev.rec.size || old.mtime < ev.rec.mtime:
case !ok || old.size != ev.rec.size || ev.rec.mtime.After(old.mtime):
changed = append(changed, ev.rec)
case old.hashed:
delete(s.existing, ev.rec.path)
@@ -807,7 +808,7 @@ func unchangedFile(r scanRec) (fileRec, bool, error) {
}
if !fi.Mode().IsRegular() || fi.Size() != r.size ||
fi.ModTime().Unix() > r.mtime {
fi.ModTime().After(r.mtime) {
return fileRec{}, false, nil
}
@@ -966,7 +967,7 @@ func seedRoot(ctx context.Context, root string,
sendEvent(ctx, events, walkEvent{rec: fileRec{
path: root,
size: fi.Size(),
mtime: fi.ModTime().Unix(),
mtime: fi.ModTime(),
dev: dev,
ino: ino,
}})
@@ -1124,7 +1125,7 @@ func emitFile(ctx context.Context, p string, e fs.DirEntry,
sendEvent(ctx, events, walkEvent{rec: fileRec{
path: p,
size: info.Size(),
mtime: info.ModTime().Unix(),
mtime: info.ModTime(),
dev: dev,
ino: ino,
}})