Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4aeec355a |
@@ -633,10 +633,10 @@ Rules for the walk:
|
|||||||
is accepted: the database mirrors what the latest scan could actually verify.
|
is accepted: the database mirrors what the latest scan could actually verify.
|
||||||
The exception is an operand dropped before the database was read, whose record
|
The exception is an operand dropped before the database was read, whose record
|
||||||
counts as outside the scanned operands.
|
counts as outside the scanned operands.
|
||||||
- A file or directory removed while the scan runs is not an error. The scan
|
- A file or directory removed before the scan reaches it is not an error. The
|
||||||
prints no line for it and does not count it as skipped. A path gone when the
|
scan prints no line for it and does not count it as skipped. That holds in the
|
||||||
walk reaches it gets no record. A file gone when the hash phase opens it gets
|
walk, and in the hash phase for a file removed since the walk. Such a path
|
||||||
none either. The records an earlier scan left at or below such a path are
|
gets no record, and the records an earlier scan left at or below it are
|
||||||
deleted.
|
deleted.
|
||||||
|
|
||||||
Concurrency: the walk phase, which also stats files, and the hash phase each use
|
Concurrency: the walk phase, which also stats files, and the hash phase each use
|
||||||
@@ -938,9 +938,11 @@ Additional requirements:
|
|||||||
too wide, and then the end of the time remaining is lost first. That happens
|
too wide, and then the end of the time remaining is lost first. That happens
|
||||||
hours into a scan of millions of files, on a terminal narrower than about 75
|
hours into a scan of millions of files, on a terminal narrower than about 75
|
||||||
columns. On an 80-column terminal, the stalled line of the example above
|
columns. On an 80-column terminal, the stalled line of the example above
|
||||||
keeps its words. The next draw fits the line to a terminal made narrower
|
keeps its words. Where the words and the time remaining do not both fit, as
|
||||||
while a phase runs. It erases the old line within the new width, and the
|
at millions of files and times in hours, the words give way. A terminal made
|
||||||
display goes on redrawing in place.
|
narrower while a phase runs gets the same: the next draw erases the line
|
||||||
|
within the new width and fits the line to it, and the display goes on
|
||||||
|
redrawing in place.
|
||||||
|
|
||||||
- When stderr is not a terminal (a pipe, a file, `/dev/null`), do not emit ANSI
|
- When stderr is not a terminal (a pipe, a file, `/dev/null`), do not emit ANSI
|
||||||
redraws: print the line the moment each phase starts, every 5 seconds while it
|
redraws: print the line the moment each phase starts, every 5 seconds while it
|
||||||
|
|||||||
@@ -29,14 +29,14 @@
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- a file or directory removed while the scan runs is not an error, in the walk
|
- a file or directory removed before the scan reaches it is not an error, in the
|
||||||
or in the hash phase. The scan prints no line for it and does not count it as
|
walk or in the hash phase. The scan prints no line for it, does not count it
|
||||||
skipped. When the first of several hard links to a file is gone, the hash
|
as skipped, and gives it no record. When the first of several hard links to a
|
||||||
phase reads the next one instead. On a terminal every progress display starts
|
file is gone, the hash phase reads the next one instead. On a terminal every
|
||||||
with the spinner, the hash phase's included. A phase with an exact total ends
|
progress display starts with the spinner, the hash phase's included. A phase
|
||||||
its line with its percent and a 10-column bar at the terminal's right edge,
|
with an exact total ends its line with its percent and a 10-column bar at the
|
||||||
which no longer moves as the line's length changes. The time remaining is
|
terminal's right edge, which no longer moves as the line's length changes. The
|
||||||
labelled `remain`, not `eta` (2026-10-26,
|
time remaining is labelled `remain`, not `eta` (2026-10-26,
|
||||||
https://git.eeqj.de/sneak/sfdupes/issues/133)
|
https://git.eeqj.de/sneak/sfdupes/issues/133)
|
||||||
|
|
||||||
- the `head`, `tail` and `content` columns are BLOBs, each holding the raw 32
|
- the `head`, `tail` and `content` columns are BLOBs, each holding the raw 32
|
||||||
|
|||||||
@@ -726,65 +726,6 @@ func TestWalkPassesOverRemovedPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestScanWarnsAboutUnreadablePaths checks that a permission error is
|
|
||||||
// still warned about and counted as skipped, unlike a removed path. The
|
|
||||||
// walk reaches an unreadable directory, and a file in a directory that
|
|
||||||
// can be read but not searched. Another such file is an operand.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
|
||||||
func TestScanWarnsAboutUnreadablePaths(t *testing.T) {
|
|
||||||
if os.Geteuid() == 0 {
|
|
||||||
t.Skip("root ignores file permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr := captureStderr(t)
|
|
||||||
dir := t.TempDir()
|
|
||||||
tree := filepath.Join(dir, "tree")
|
|
||||||
walked := writeFile(t, tree, "unsearchable/f", []byte("x"))
|
|
||||||
operand := writeFile(t, dir, "unsearchable/f", []byte("y"))
|
|
||||||
unreadable := filepath.Join(tree, "unreadable")
|
|
||||||
|
|
||||||
err := os.Mkdir(unreadable, 0o750)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
modes := map[string]fs.FileMode{
|
|
||||||
unreadable: 0,
|
|
||||||
filepath.Dir(walked): 0o400,
|
|
||||||
filepath.Dir(operand): 0o400,
|
|
||||||
}
|
|
||||||
|
|
||||||
for p, mode := range modes {
|
|
||||||
err = os.Chmod(p, mode)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs before t.TempDir's own cleanup, which must delete the files.
|
|
||||||
t.Cleanup(func() {
|
|
||||||
//nolint:gosec // removing the files needs the search bit back
|
|
||||||
_ = os.Chmod(p, 0o700)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
st := syncTree(t, openTestDB(t), tree, operand)
|
|
||||||
if st != (scanStats{skipped: 3}) {
|
|
||||||
t.Errorf("stats = %+v, want 3 skipped and nothing else", st)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := stderr()
|
|
||||||
for _, warning := range []string{
|
|
||||||
"walk " + unreadable + ": open " + unreadable + ": permission denied\n",
|
|
||||||
"stat " + walked + ": lstat " + walked + ": permission denied\n",
|
|
||||||
"walk " + operand + ": lstat " + operand + ": permission denied\n",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(got, warning) {
|
|
||||||
t.Errorf("stderr = %q, want %q", got, warning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncOneFilesystem scans roots into db with -x and returns the scan
|
// syncOneFilesystem scans roots into db with -x and returns the scan
|
||||||
// stats. Every device pruneRoots looks up, and the device each
|
// stats. Every device pruneRoots looks up, and the device each
|
||||||
// operand's walk compares its subdirectories with, comes from devices
|
// operand's walk compares its subdirectories with, comes from devices
|
||||||
|
|||||||
Reference in New Issue
Block a user