Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd76633211 |
@@ -455,11 +455,17 @@ scan and are enumerated concurrently: every operand seeds the shared walk worker
|
|||||||
pool. Overlapping operands are harmless — an operand that duplicates another or
|
pool. Overlapping operands are harmless — an operand that duplicates another or
|
||||||
lies under another is dropped before walking, so every file is reached exactly
|
lies under another is dropped before walking, so every file is reached exactly
|
||||||
once and produces one database record. An operand under another one is kept,
|
once and produces one database record. An operand under another one is kept,
|
||||||
though, and walked on its own when any directory from it (from its directory,
|
though, when the walk of the outer operand does not reach it. It is then walked
|
||||||
for a regular file) up to but not including the operand covering it is a default
|
on its own. The walk of the outer operand does not reach the inner one when it,
|
||||||
exclude, is not on a persistent filesystem (see "Rules for the walk"), or, with
|
or any directory between the two, is:
|
||||||
`-x`, is on a different filesystem than that operand, because that operand's
|
|
||||||
walk does not enter such a directory and so does not reach it.
|
- named `.zfs`;
|
||||||
|
- a symlink;
|
||||||
|
- a default exclude;
|
||||||
|
- not on a persistent filesystem (see "Rules for the walk");
|
||||||
|
- with `-x`, on a different filesystem than the outer operand.
|
||||||
|
|
||||||
|
For a regular-file operand, only the directories between the two count.
|
||||||
|
|
||||||
An operand that is a symlink (never followed, not even as an operand), socket,
|
An operand that is a symlink (never followed, not even as an operand), socket,
|
||||||
FIFO, or device node, or a directory named `.zfs`, is not scanned. `scan` prints
|
FIFO, or device node, or a directory named `.zfs`, is not scanned. `scan` prints
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- an operand under another is walked on its own when a directory between them,
|
||||||
|
the inner operand included, is named `.zfs` or is a symlink. The walk of the
|
||||||
|
outer operand does not reach it then. `scan` now records the files below it
|
||||||
|
and no longer deletes their records (2026-10-26,
|
||||||
|
https://git.eeqj.de/sneak/sfdupes/issues/142)
|
||||||
|
|
||||||
- a file or directory removed while the scan runs is not an error, in the walk
|
- a file or directory removed while the scan runs is not an error, in the walk
|
||||||
or in the hash phase. The scan prints no line for it and does not count it as
|
or in the hash phase. The scan prints no line for it and does not count it as
|
||||||
skipped. When the first of several hard links to a file is gone, the hash
|
skipped. When the first of several hard links to a file is gone, the hash
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ const sampleWindow = 1024 * 1024
|
|||||||
// and hash worker pools.
|
// and hash worker pools.
|
||||||
const workQueueDepth = 1024
|
const workQueueDepth = 1024
|
||||||
|
|
||||||
|
// zfsDirName is the name of the directory in which ZFS shows a
|
||||||
|
// filesystem's snapshots. The walk never enters one, since it would
|
||||||
|
// list every file once per snapshot.
|
||||||
|
const zfsDirName = ".zfs"
|
||||||
|
|
||||||
// errInterrupted reports a scan stopped by SIGINT or SIGTERM. runScan
|
// errInterrupted reports a scan stopped by SIGINT or SIGTERM. runScan
|
||||||
// has already printed its line, so run prints nothing more.
|
// has already printed its line, so run prints nothing more.
|
||||||
var errInterrupted = errors.New("scan interrupted")
|
var errInterrupted = errors.New("scan interrupted")
|
||||||
@@ -272,9 +277,10 @@ func pruneRoots(roots []string, oneFS bool, rules walkRules,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// walkReaches reports whether the walk of operand above reaches r, an
|
// walkReaches reports whether the walk of operand above reaches r, an
|
||||||
// operand under it. That walk does not enter a default exclude in
|
// operand under it. That walk never follows a symlink. It does not
|
||||||
// rules, nor a directory that device puts on a filesystem in rules that
|
// enter a directory named .zfs, nor a default exclude in rules, nor a
|
||||||
// is not persistent, nor, with -x, one that device puts on a different
|
// directory that device puts on a filesystem in rules that is not
|
||||||
|
// persistent, nor, with -x, one that device puts on a different
|
||||||
// filesystem than above, when device knows above's. It reaches every
|
// filesystem than above, when device knows above's. It reaches every
|
||||||
// regular file in a directory it enters, so for a regular-file operand
|
// regular file in a directory it enters, so for a regular-file operand
|
||||||
// the directories checked start at its directory; they end below above.
|
// the directories checked start at its directory; they end below above.
|
||||||
@@ -295,6 +301,15 @@ func walkReaches(above, r string, oneFS bool, rules walkRules,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filepath.Base(dir) == zfsDirName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err = os.Lstat(dir)
|
||||||
|
if err == nil && fi.Mode()&fs.ModeSymlink != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
dev, ok := device(dir)
|
dev, ok := device(dir)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -883,7 +898,7 @@ func operandWarning(root string, fi fs.FileInfo) string {
|
|||||||
case mode.IsRegular():
|
case mode.IsRegular():
|
||||||
return ""
|
return ""
|
||||||
case mode.IsDir():
|
case mode.IsDir():
|
||||||
if filepath.Base(root) != ".zfs" {
|
if filepath.Base(root) != zfsDirName {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,7 +1168,7 @@ func subdirJob(ctx context.Context, p string, e fs.DirEntry,
|
|||||||
parent dirJob, oneFS bool, rules walkRules,
|
parent dirJob, oneFS bool, rules walkRules,
|
||||||
events chan<- walkEvent,
|
events chan<- walkEvent,
|
||||||
) (dirJob, bool) {
|
) (dirJob, bool) {
|
||||||
if e.Name() == ".zfs" {
|
if e.Name() == zfsDirName {
|
||||||
return dirJob{}, false
|
return dirJob{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1672,6 +1672,81 @@ func TestSyncScanOverlappingRoots(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestScanWalksOperandInZFSDirectory checks that an operand inside a
|
||||||
|
// directory named .zfs below another operand is walked on its own,
|
||||||
|
// since the walk of the other operand does not enter .zfs. The file
|
||||||
|
// under it is recorded, and a second scan keeps the record.
|
||||||
|
func TestScanWalksOperandInZFSDirectory(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
db := openTestDB(t)
|
||||||
|
f := writeFile(t, dir, "pool/.zfs/snapshot/daily/f", pattern(1, 10))
|
||||||
|
daily := filepath.Dir(f)
|
||||||
|
|
||||||
|
syncTree(t, db, dir, daily)
|
||||||
|
recordByPath(t, dbRecords(t, db), f)
|
||||||
|
|
||||||
|
syncTree(t, db, dir, daily)
|
||||||
|
recordByPath(t, dbRecords(t, db), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanWalksOperandBehindSymlink checks that an operand reached
|
||||||
|
// through a symlinked directory below another operand is walked on its
|
||||||
|
// own, since the walk of the other operand does not follow the symlink.
|
||||||
|
// The file under it is recorded, and a second scan keeps the record.
|
||||||
|
func TestScanWalksOperandBehindSymlink(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
target := t.TempDir()
|
||||||
|
db := openTestDB(t)
|
||||||
|
writeFile(t, target, "sub/f", pattern(1, 10))
|
||||||
|
|
||||||
|
link := filepath.Join(dir, "link")
|
||||||
|
|
||||||
|
err := os.Symlink(target, link)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := filepath.Join(link, "sub")
|
||||||
|
f := filepath.Join(sub, "f")
|
||||||
|
|
||||||
|
syncTree(t, db, dir, sub)
|
||||||
|
recordByPath(t, dbRecords(t, db), f)
|
||||||
|
|
||||||
|
syncTree(t, db, dir, sub)
|
||||||
|
recordByPath(t, dbRecords(t, db), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanDropsOperandReachedBehindSymlink checks that an operand under
|
||||||
|
// another one is still dropped when both lie behind the same symlinked
|
||||||
|
// directory: the walk of the other operand starts past the symlink and
|
||||||
|
// reaches it, so the file under it is walked once.
|
||||||
|
func TestScanDropsOperandReachedBehindSymlink(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
target := t.TempDir()
|
||||||
|
db := openTestDB(t)
|
||||||
|
writeFile(t, target, "sub/deeper/f", pattern(1, 10))
|
||||||
|
|
||||||
|
link := filepath.Join(dir, "link")
|
||||||
|
|
||||||
|
err := os.Symlink(target, link)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := filepath.Join(link, "sub")
|
||||||
|
|
||||||
|
st := syncTree(t, db, sub, filepath.Join(sub, "deeper"))
|
||||||
|
if st != (scanStats{walked: 1, added: 1}) {
|
||||||
|
t.Fatalf("stats = %+v, want 1 walked and added", st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestScanHashesUniqueSize checks that a file whose size no other file
|
// TestScanHashesUniqueSize checks that a file whose size no other file
|
||||||
// shares is hashed like every other file.
|
// shares is hashed like every other file.
|
||||||
func TestScanHashesUniqueSize(t *testing.T) {
|
func TestScanHashesUniqueSize(t *testing.T) {
|
||||||
@@ -2085,6 +2160,16 @@ func TestPruneRoots(t *testing.T) {
|
|||||||
t.Fatalf("pruneRoots with %s not persistent = %q, want %q",
|
t.Fatalf("pruneRoots with %s not persistent = %q, want %q",
|
||||||
pqRoot, got, want)
|
pqRoot, got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nor does it enter a directory named .zfs, so /p/.zfs/s is kept. The
|
||||||
|
// walk of /p/.zfs/s reaches /p/.zfs/s/t, which is dropped.
|
||||||
|
got = pruneRoots([]string{"/p", "/p/.zfs/s", "/p/.zfs/s/t"}, false,
|
||||||
|
walkRules{}, device)
|
||||||
|
|
||||||
|
want = []string{"/p", "/p/.zfs/s"}
|
||||||
|
if !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("pruneRoots with .zfs = %q, want %q", got, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReportsNeverTouchFilesystem(t *testing.T) {
|
func TestReportsNeverTouchFilesystem(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user