1 Commits
Author SHA1 Message Date
sneak 261b13765a Rewrite TODO.md sentence by sentence (closes #135)
check / check (push) Canceled after 0s
TODO.md is the project's own task list, judged by the same rules as
README.md: short sentences, lists as bullets, nothing a newcomer reads
twice. Every completed step is kept with its date and its issue or
branch, and every fact the old entries recorded is kept too, in short
sentences. The 2026-07-22 policy-compliance checklist is folded into
the step that did the work, and the accepted divergence from the
layout policy gets a section of its own. Workflow, Status and Next
Step keep their rules unchanged.

Model: fable-5-1
2026-10-26 11:39:35 +00:00
4 changed files with 88 additions and 237 deletions

No files matched your search

+11 -20
View File
@@ -461,26 +461,17 @@ exclude, is not on a persistent filesystem (see "Rules for the walk"), or, with
`-x`, is on a different filesystem than that operand, because that operand's `-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. walk does not enter such a directory and so does not reach it.
An operand is not scanned when it is: 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
- a symlink, which `scan` never follows, not even as an operand; a one-line warning naming the path and what it is, counts it as skipped, and
- a socket, FIFO or device node; drops it from the scanned operands before reading the database. Another operand
- at or inside a directory named `.zfs`. beneath it is still scanned. The records stored beneath it are not deleted: they
are treated like any other record outside the scanned operands. If it lies under
An operand reached through a symlink into a `.zfs` directory is not scanned another operand, they are under that operand instead, and are treated like any
either. other record there. This is not an error: a scan whose every operand is dropped
walks nothing and exits 0. An operand that is not on a persistent filesystem
This holds even when the operand lies under another operand. `scan` prints a (see "Rules for the walk") is dropped the same way. The line `scan` prints for
one-line warning naming the path and what it is, counts it as skipped, and drops it is not a warning, though, and it is not counted as skipped.
it from the scanned operands before reading the database. An operand beneath a
symlink operand is still scanned. The records stored beneath the dropped operand
are not deleted: they are treated like any other record outside the scanned
operands. If it lies under another operand, they are under that operand instead,
and are treated like any other record there. This is not an error: a scan whose
every operand is dropped walks nothing and exits 0. An operand that is not on a
persistent filesystem (see "Rules for the walk") is dropped the same way. The
line `scan` prints for it is not a warning, though, and it is not counted as
skipped.
`scan` synchronizes the database with the filesystem state under the scanned `scan` synchronizes the database with the filesystem state under the scanned
operands: operands:
+54 -66
View File
@@ -38,14 +38,8 @@ are known.
it. It leads with what the program is and how to use it. A reference section it. It leads with what the program is and how to use it. A reference section
covers every setting and every subcommand. Development and testing come last. covers every setting and every subcommand. Development and testing come last.
Every statement was checked against the code on `next`. Five comments in Every statement was checked against the code on `next`. Five comments in
`main_test.go` cite "Settings", where their rules now live. One comment each `main_test.go` cite "Settings", where their rules now live (2026-10-26,
in `scan_test.go`, `trees_test.go` and `exclude_test.go` cites what the README https://git.eeqj.de/sneak/sfdupes/issues/135)
now says (2026-10-26, https://git.eeqj.de/sneak/sfdupes/issues/135)
- an operand at or inside a directory named `.zfs` is never scanned, whether
given alone or under another operand. `scan` warns about it, counts it as
skipped and does not walk it (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
@@ -79,8 +73,8 @@ are known.
with a content hash, since every record has all three (2026-10-25, with a content hash, since every record has all three (2026-10-25,
https://git.eeqj.de/sneak/sfdupes/issues/132) https://git.eeqj.de/sneak/sfdupes/issues/132)
- `REPO_POLICIES.md` and the four scripts that pass `CACHEBUST` were copied - `REPO_POLICIES.md` and the four scripts that pass `CACHEBUST` copied again
again from `sneak/prompts` `next` at `0653579`. Two steps now come before from `sneak/prompts` `next` at `0653579`. Two steps now come before
`ARG CACHEBUST`: `ARG CACHEBUST`:
- the module download in the `lint`, `test` and `builder` stages of the - the module download in the `lint`, `test` and `builder` stages of the
`Dockerfile`; `Dockerfile`;
@@ -219,16 +213,11 @@ are known.
opening the database, also while it waits on a busy one. `read` counts the opening the database, also while it waits on a busy one. `read` counts the
records, then reads them against that count. `group` counts the directories it records, then reads them against that count. `group` counts the directories it
groups, and `write` the groups it writes. A display's rate names what its groups, and `write` the groups it writes. A display's rate names what its
phase counts: phase counts, such as records or groups. stdout and the summary line are
- files; unchanged. When stdout and stderr are the same file, as one terminal is, no
- records; line of stdout shares a line with the display. When they are not, the display
- directories; keeps being drawn while stdout waits on a slow reader (2026-10-16,
- groups. https://git.eeqj.de/sneak/sfdupes/issues/113)
stdout and the summary line are unchanged. When stdout and stderr are the
same file, as one terminal is, no line of stdout shares a line with the
display. When they are not, the display keeps being drawn while stdout waits
on a slow reader (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/113)
- Each `scan` phase's progress display names the phase and its place among the - Each `scan` phase's progress display names the phase and its place among the
five, as `[2/5] walk`. It runs from the moment the phase starts and shows: five, as `[2/5] walk`. It runs from the moment the phase starts and shows:
@@ -256,8 +245,8 @@ are known.
"Database" says when `scan` writes each file (2026-10-16, "Database" says when `scan` writes each file (2026-10-16,
https://git.eeqj.de/sneak/sfdupes/issues/111) https://git.eeqj.de/sneak/sfdupes/issues/111)
- The canonical files and model scripts were copied again from `sneak/prompts` - The canonical files and model scripts copied again from `sneak/prompts` `next`
`next` at `e3f79c4`: at `e3f79c4`:
- `golangci-lint` v2.14.0; - `golangci-lint` v2.14.0;
- lint and test are phases of the `Dockerfile` that write no image, and - lint and test are phases of the `Dockerfile` that write no image, and
`make test` runs the suite under the race detector. Gone are `make test` runs the suite under the race detector. Gone are
@@ -282,10 +271,11 @@ are known.
code still runs on every build (2026-10-15, code still runs on every build (2026-10-15,
https://git.eeqj.de/sneak/sfdupes/issues/95) https://git.eeqj.de/sneak/sfdupes/issues/95)
- With `-x`, an operand under another operand is no longer dropped when a - With `-x`, an operand under another operand is no longer dropped when the
directory between the two, the inner one included, is on a different outer operand's walk cannot reach it. That is the case when a directory
filesystem than the outer one. The inner operand is then walked on its own. between the two operands, the inner one included, is on a different filesystem
For a regular-file operand the directories checked start at its directory. The than the outer one. The inner operand is then walked on its own. For a
regular-file operand the directories checked start at its directory. The
records of files gone from below it are deleted (2026-10-14, records of files gone from below it are deleted (2026-10-14,
https://git.eeqj.de/sneak/sfdupes/issues/106) https://git.eeqj.de/sneak/sfdupes/issues/106)
@@ -348,7 +338,7 @@ are known.
(2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/16) (2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/16)
- `.golangci.yml` was replaced with the current canonical copy, which uses - `.golangci.yml` replaced with the current canonical copy, which uses
`gomodguard_v2`, so lint no longer prints a deprecation warning (2026-10-04, `gomodguard_v2`, so lint no longer prints a deprecation warning (2026-10-04,
https://git.eeqj.de/sneak/sfdupes/issues/26) https://git.eeqj.de/sneak/sfdupes/issues/26)
@@ -367,8 +357,8 @@ are known.
- A test checks that `scan` refuses a database with another schema version - A test checks that `scan` refuses a database with another schema version
(2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/64) (2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/64)
- Four inaccurate comments in `cancel_test.go` were corrected, and - Four inaccurate comments in `cancel_test.go` corrected, and
`walkCancelInFlightDirs` was renamed to `walkCancelInFlightFiles` (2026-10-04, `walkCancelInFlightDirs` renamed to `walkCancelInFlightFiles` (2026-10-04,
https://git.eeqj.de/sneak/sfdupes/issues/33) https://git.eeqj.de/sneak/sfdupes/issues/33)
- The `-x` filesystem-boundary rules in `subdirJob` are tested (2026-10-04, - The `-x` filesystem-boundary rules in `subdirJob` are tested (2026-10-04,
@@ -455,12 +445,12 @@ are known.
schema. Superseded by https://git.eeqj.de/sneak/sfdupes/issues/132 schema. Superseded by https://git.eeqj.de/sneak/sfdupes/issues/132
(2026-09-22, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/61) (2026-09-22, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/61)
- The dead `files.dat` references were removed from `Makefile`, `.gitignore` and - The dead `files.dat` references removed from `Makefile`, `.gitignore` and
`.dockerignore` (2026-09-21, branch `next`, `.dockerignore` (2026-09-21, branch `next`,
https://git.eeqj.de/sneak/sfdupes/issues/22) https://git.eeqj.de/sneak/sfdupes/issues/22)
- The lint-image pin comments and `FROM` form in `Dockerfile` and - The lint-image pin comments and `FROM` form in `Dockerfile` and
`Dockerfile.lint` were fixed. Both pins became the policy `Dockerfile.lint` fixed. Both pins became the policy
`# image:vX.Y.Z, YYYY-MM-DD` comment over a bare `FROM image@sha256:...`, `# image:vX.Y.Z, YYYY-MM-DD` comment over a bare `FROM image@sha256:...`,
without the false `(Debian-based)` note or the tag. Since without the false `(Debian-based)` note or the tag. Since
https://git.eeqj.de/sneak/sfdupes/issues/95 the `Dockerfile`'s `lint` phase https://git.eeqj.de/sneak/sfdupes/issues/95 the `Dockerfile`'s `lint` phase
@@ -585,7 +575,7 @@ are known.
(2026-08-09, branch `db-close-on-fatal`, (2026-08-09, branch `db-close-on-fatal`,
https://git.eeqj.de/sneak/sfdupes/issues/4) https://git.eeqj.de/sneak/sfdupes/issues/4)
- `golangci-lint` was updated to v2.12.2 with the canonical config, merged as - `golangci-lint` updated to v2.12.2 with the canonical config, merged as
`38a01bd`. The pinned linter in the `Dockerfile` lint stage and `38a01bd`. The pinned linter in the `Dockerfile` lint stage and
`script/bootstrap` went from v2.12.1 to v2.12.2, and `.golangci.yml` was `script/bootstrap` went from v2.12.1 to v2.12.2, and `.golangci.yml` was
replaced with the canonical file. Four thresholds now live under replaced with the canonical file. Four thresholds now live under
@@ -598,7 +588,7 @@ are known.
No new lint findings surfaced (2026-08-09, branch `golangci-v2.12.2`, No new lint findings surfaced (2026-08-09, branch `golangci-v2.12.2`,
https://git.eeqj.de/sneak/sfdupes/issues/3) https://git.eeqj.de/sneak/sfdupes/issues/3)
- `Makefile` targets were converted to scripts-to-rule-them-all `script/` - `Makefile` targets converted to scripts-to-rule-them-all `script/`
entrypoints, like the other managed repos. All 12 entrypoints exist: entrypoints, like the other managed repos. All 12 entrypoints exist:
- `bootstrap`, `setup` and `projectname`; - `bootstrap`, `setup` and `projectname`;
- `test`, `lint`, `fmt`, `fmt-check` and `check`; - `test`, `lint`, `fmt`, `fmt-check` and `check`;
@@ -611,15 +601,15 @@ are known.
it used to run `check` plus `build`. `make build` remains as an alias it used to run `check` plus `build`. `make build` remains as an alias
(2026-07-24, branch `make-default-target`) (2026-07-24, branch `make-default-target`)
- The phases became scan-wide, with concurrent operands and batched updates. All - Scan-wide phases, concurrent operands and batched updates. All operands seed
operands seed the shared walk pool, and every pass runs once over the whole the shared walk pool, and every pass runs once over the whole scan. So the
scan. So the totals and ETAs cover the whole scan. The per-operand walk, hash totals and ETAs cover the whole scan. The per-operand walk, hash and update
and update cycles and their stderr announcements are gone. The update pass cycles and their stderr announcements are gone. The update pass writes in
writes in batches, each in its own transaction. The filesystem is batches, each in its own transaction. The filesystem is authoritative and the
authoritative and the database follows it, so one scan's changes need not land database follows it, so one scan's changes need not land all at once
all at once (2026-07-24, branch `scan-wide-phases`) (2026-07-24, branch `scan-wide-phases`)
- The stat pass was split back out of the walk. Phases are strictly sequential - The stat pass split back out of the walk. Phases are strictly sequential
again, per operand, with parallelism only inside each phase: again, per operand, with parallelism only inside each phase:
- walk; - walk;
- stat; - stat;
@@ -635,32 +625,31 @@ are known.
totals that looked like the whole run's (2026-07-24, branch totals that looked like the whole run's (2026-07-24, branch
`scan-operand-progress`) `scan-operand-progress`)
- The walk became parallel. The walk pass was a single goroutine and took hours - Parallel walk. The walk pass was a single goroutine and took hours at about 20
at about 20 million files on a busy pool. One ZFS server took 4 hours for 22 million files on a busy pool. One ZFS server took 4 hours for 22 million
million files. It is now a walk by a pool of per-directory workers that files. It is now a walk by a pool of per-directory workers that records size
records size and mtime as it goes. That folds away the separate stat pass and and mtime as it goes. That folds away the separate stat pass and halves the
halves the metadata reads. Each `PATH` operand commits in its own transaction, metadata reads. Each `PATH` operand commits in its own transaction, so an
so an interrupted scan keeps completed operands (2026-07-24, branch interrupted scan keeps completed operands (2026-07-24, branch `parallel-walk`)
`parallel-walk`)
- `scan` gained a persistent database. It maintains an SQLite database that - Persistent scan database. `scan` maintains an SQLite database that survives
survives between runs, with the absolute path as each record's key. The driver between runs, with the absolute path as each record's key. The driver is
is `modernc.org/sqlite`, pure Go, so cgo stays disabled. A rescan hashes only `modernc.org/sqlite`, pure Go, so cgo stays disabled. A rescan hashes only new
new or changed files, by mtime and size. It deletes the records of files or changed files, by mtime and size. It deletes the records of files vanished
vanished from under the scanned operands and leaves records outside them from under the scanned operands and leaves records outside them untouched. So
untouched. So `scan` can run daily from `cron`. `report` and `trees` read the `scan` can run daily from `cron`. `report` and `trees` read the database, with
database, with no positional arguments, instead of a scan stream. The database no positional arguments, instead of a scan stream. The database is at
is at `/var/lib/sfdupes/db.sqlite`, and `SFDUPES_DATABASE` overrides that. WAL `/var/lib/sfdupes/db.sqlite`, and `SFDUPES_DATABASE` overrides that. WAL
journaling plus a single-transaction update keep a report run during a scan journaling plus a single-transaction update keep a report run during a scan
safe (2026-07-24, branch `persistent-database`) safe (2026-07-24, branch `persistent-database`)
- The `origin` remote, `git@git.eeqj.de:sneak/sfdupes.git`, was added. `v0.0.1` - The `origin` remote, `git@git.eeqj.de:sneak/sfdupes.git`, added. `v0.0.1`
was tagged, and `main` and the tags were pushed (2026-07-23) tagged, and `main` and the tags pushed (2026-07-23)
- The `scan` command line was reworked. Required `PATH...` operands, parsed by - `scan` command-line rework. Required `PATH...` operands, parsed by cobra,
cobra, replace the `/srv` `-root` default. The new `-x`, `--one-file-system` replace the `/srv` `-root` default. The new `-x`, `--one-file-system` flag
flag follows the GNU convention and stops at filesystem boundaries, which the follows the GNU convention and stops at filesystem boundaries, which the walk
walk crosses by default (2026-07-23, branch `scan-required-paths`) crosses by default (2026-07-23, branch `scan-required-paths`)
- The repo was brought into compliance with `REPO_POLICIES.md` (the version of - The repo was brought into compliance with `REPO_POLICIES.md` (the version of
2026-07-06), the existing repo checklist and the Go styleguide. The audit of 2026-07-06), the existing repo checklist and the Go styleguide. The audit of
@@ -703,11 +692,10 @@ are known.
The code was already `gofmt`-clean, so no standalone formatting commit was The code was already `gofmt`-clean, so no standalone formatting commit was
needed (2026-07-23, branch `repo-policy-compliance`) needed (2026-07-23, branch `repo-policy-compliance`)
- `git init` was run, with a README-only first commit. The code baseline was - `git init` with a README-only first commit; the code baseline committed on
committed on `main` (2026-07-22) `main` (2026-07-22)
- The `scan`, `report` and `trees` subcommands were implemented (pre-git - `scan`, `report` and `trees` subcommands implemented (pre-git history)
history)
# Future Steps # Future Steps
+11 -123
View File
@@ -928,21 +928,24 @@ func TestRunScanWalksOperandUnderSymlinkOperand(t *testing.T) {
} }
func TestRunScanSkipsZFSOperand(t *testing.T) { func TestRunScanSkipsZFSOperand(t *testing.T) {
zfs := filepath.Join(t.TempDir(), ".zfs") path := testDBPath(t)
f := writeFile(t, zfs, "snapshot/hourly/f", pattern(1, 10))
// No scan records a file inside a .zfs directory, so the record
// beneath the operand is written straight into the database.
path := seedDatabase(t, []scanRec{{size: 10, path: f}})
t.Setenv(databaseEnv, path) t.Setenv(databaseEnv, path)
zfs := filepath.Join(t.TempDir(), ".zfs")
snapshot := filepath.Join(zfs, "snapshot", "hourly")
f := writeFile(t, snapshot, "f", pattern(1, 10))
// An operand beneath a .zfs directory is walked, because it is not
// itself named .zfs.
scanOK(t, snapshot)
assertOperandSkipped(t, path, zfs, ".zfs directory", f) assertOperandSkipped(t, path, zfs, ".zfs directory", f)
} }
// assertOperandSkipped scans operand alone and checks that it is skipped // assertOperandSkipped scans operand alone and checks that it is skipped
// as kind: a warning naming it, one skip in the summary, exit 0, and the // as kind: a warning naming it, one skip in the summary, exit 0, and the
// record for kept, stored beneath operand before this scan, still in the // record for kept, which an earlier scan stored beneath operand, still
// database at dbPath. // in the database at dbPath.
func assertOperandSkipped(t *testing.T, dbPath, operand, kind, func assertOperandSkipped(t *testing.T, dbPath, operand, kind,
kept string, kept string,
) { ) {
@@ -971,121 +974,6 @@ func assertOperandSkipped(t *testing.T, dbPath, operand, kind,
recordByPath(t, dbRecords(t, db), kept) recordByPath(t, dbRecords(t, db), kept)
} }
// TestRunScanSkipsOperandInsideZFS scans a directory inside a .zfs
// directory, first alone and then under another operand. Each time it
// is warned about and counted as skipped, and nothing in it is walked.
// The other operand is scanned as usual.
func TestRunScanSkipsOperandInsideZFS(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
dir := t.TempDir()
hourly := filepath.Join(dir, ".zfs", "snapshot", "hourly")
writeFile(t, hourly, "f", pattern(1, 10))
f := writeFile(t, dir, "f", pattern(2, 10))
warning := "walk " + hourly + ": skipping operand inside a .zfs directory\n"
for _, tc := range []struct {
operands []string
summary string
}{
{
[]string{hourly},
"scan: 0 files seen (0 added, 0 updated, 0 removed, " +
"0 unchanged), 1 skipped\n",
},
{
[]string{dir, hourly},
"scan: 1 files seen (1 added, 0 updated, 0 removed, " +
"0 unchanged), 1 skipped\n",
},
} {
stderr := scanOK(t, tc.operands...)
if !strings.Contains(stderr, warning) ||
!strings.Contains(stderr, tc.summary) {
t.Errorf("scan %q: stderr = %q, want %q and %q", tc.operands,
stderr, warning, tc.summary)
}
}
db, err := openDB(path, reportParams)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
if got := recordPaths(dbRecords(t, db)); !slices.Equal(got, []string{f}) {
t.Errorf("records = %q, want %q", got, []string{f})
}
}
// TestRunScanSkipsOperandThroughSymlinkIntoZFS scans snaps/daily, where
// snaps is a symlink to pool/.zfs/snapshot. The operand is warned about
// by the path given, counted as skipped, and not walked.
func TestRunScanSkipsOperandThroughSymlinkIntoZFS(t *testing.T) {
t.Setenv(databaseEnv, testDBPath(t))
dir := t.TempDir()
writeFile(t, dir, "pool/.zfs/snapshot/daily/f", pattern(1, 10))
snaps := filepath.Join(dir, "snaps")
err := os.Symlink(filepath.Join(dir, "pool", ".zfs", "snapshot"), snaps)
if err != nil {
t.Fatal(err)
}
daily := filepath.Join(snaps, "daily")
stderr := scanOK(t, daily)
warning := "walk " + daily + ": skipping operand inside a .zfs directory\n"
summary := "scan: 0 files seen (0 added, 0 updated, 0 removed, " +
"0 unchanged), 1 skipped\n"
if !strings.Contains(stderr, warning) ||
!strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q and %q", stderr, warning, summary)
}
}
// TestRunScanWalksOperandThroughSymlink scans data/daily, where data is
// a symlink to a directory outside any .zfs directory. The operand is
// scanned as usual, and its file is recorded by the path given.
func TestRunScanWalksOperandThroughSymlink(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
dir := t.TempDir()
writeFile(t, dir, "pool/data/daily/f", pattern(1, 10))
data := filepath.Join(dir, "data")
err := os.Symlink(filepath.Join(dir, "pool", "data"), data)
if err != nil {
t.Fatal(err)
}
daily := filepath.Join(data, "daily")
stderr := scanOK(t, daily)
summary := "scan: 1 files seen (1 added, 0 updated, 0 removed, " +
"0 unchanged), 0 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
db, err := openDB(path, reportParams)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
recordByPath(t, dbRecords(t, db), filepath.Join(daily, "f"))
}
func TestRunReportSucceeds(t *testing.T) { func TestRunReportSucceeds(t *testing.T) {
path := testDBPath(t) path := testDBPath(t)
t.Setenv(databaseEnv, path) t.Setenv(databaseEnv, path)
+12 -28
View File
@@ -383,8 +383,7 @@ func (s *scanState) runPhases(ctx context.Context, roots []string,
) error { ) error {
// Types and filesystems are checked before pruning so that an // Types and filesystems are checked before pruning so that an
// operand under a dropped one is still scanned, not dropped as lying // operand under a dropped one is still scanned, not dropped as lying
// under it, and so that an operand inside a .zfs directory gets its // under it.
// warning even when it lies under another operand.
roots = pruneRoots(s.walkableRoots(roots, rules), oneFS, rules, roots = pruneRoots(s.walkableRoots(roots, rules), oneFS, rules,
deviceOfPath) deviceOfPath)
@@ -412,15 +411,15 @@ func (s *scanState) runPhases(ctx context.Context, roots []string,
} }
// walkableRoots returns the operands the walk can start from: regular // walkableRoots returns the operands the walk can start from: regular
// files, and directories not named .zfs, none of them inside a // files, and directories not named .zfs. Every other operand is warned
// directory named .zfs. Every other operand is warned about, counted as // about, counted as skipped, and dropped. A dropped operand is no
// skipped, and dropped. A dropped operand is no longer a root, so the // longer a root, so the records stored beneath it are treated as if it
// records stored beneath it are treated as if it had not been given: // had not been given: those under no other root count as outside the
// those under no other root count as outside the roots and are not // roots and are not deleted as unverified. An operand on a filesystem
// deleted as unverified. An operand on a filesystem in rules that is not // in rules that is not persistent is dropped the same way, with a line
// persistent is dropped the same way, with a line saying so that is not // saying so that is not a warning, and is not counted as skipped. An
// a warning, and is not counted as skipped. An operand that fails lstat // operand that fails lstat here is kept, and the walk warns about it
// here is kept, and the walk warns about it unless it no longer exists. // unless it no longer exists.
func (s *scanState) walkableRoots(roots []string, rules walkRules) []string { func (s *scanState) walkableRoots(roots []string, rules walkRules) []string {
kept := make([]string, 0, len(roots)) kept := make([]string, 0, len(roots))
@@ -875,24 +874,9 @@ func sendEvent(ctx context.Context, events chan<- walkEvent,
// operandWarning returns the one-line warning for an operand the walk // operandWarning returns the one-line warning for an operand the walk
// does not start from, naming the path and what it is, or "" for one it // does not start from, naming the path and what it is, or "" for one it
// does: a regular file, or a directory not named .zfs, that is not // does: a regular file, or a directory not named .zfs. Symlinks are
// inside a directory named .zfs, neither as given nor with its symlinks // never followed, including as operands.
// resolved. Symlinks are never followed, including as operands.
func operandWarning(root string, fi fs.FileInfo) string { func operandWarning(root string, fi fs.FileInfo) string {
// The walk never enters a .zfs directory, so it starts from nothing
// inside one either, not even through a symlink. An operand whose
// symlinks cannot be resolved is checked as given.
resolved, err := filepath.EvalSymlinks(root)
if err != nil {
resolved = root
}
if slices.Contains(strings.Split(filepath.Dir(root), "/"), ".zfs") ||
slices.Contains(strings.Split(filepath.Dir(resolved), "/"), ".zfs") {
return fmt.Sprintf("walk %s: skipping operand inside a .zfs directory",
root)
}
var kind string var kind string
switch mode := fi.Mode(); { switch mode := fi.Mode(); {