Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a1398812a |
@@ -17,10 +17,11 @@ never read in full. A match between two large files is a strong candidate, not
|
||||
proof.
|
||||
|
||||
`sfdupes` is meant for multi-terabyte servers, where reading every byte of every
|
||||
file takes too long. `scan` keeps its results between runs in an SQLite
|
||||
database, a database held in one file. A rescan reads only new and changed
|
||||
files, so `scan` can run from cron every day. The reports read the database
|
||||
alone and can run at any time.
|
||||
file takes too long. It has four subcommands. `scan` walks the filesystem and
|
||||
keeps its results between runs in an SQLite database, a database held in one
|
||||
file. A rescan reads only new and changed files, so `scan` can run every day
|
||||
from cron, the system's job scheduler. `report`, `trees` and `info` read the
|
||||
database alone and can run at any time.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -33,15 +34,18 @@ export SFDUPES_DATABASE="$HOME/.local/share/sfdupes/db.sqlite"
|
||||
./sfdupes info
|
||||
```
|
||||
|
||||
`scan` walks the trees you name and keeps one database record per regular file.
|
||||
`report` prints the file-level duplicates. `trees` prints the duplicate
|
||||
directory trees. `info` prints the database's path, size and record count.
|
||||
`scan` walks the trees named by its operands, the paths given after `scan` on
|
||||
the command line. It keeps one database record per regular file: an ordinary
|
||||
file, as opposed to a directory, a symlink or a device. `report` prints the
|
||||
file-level duplicates. `trees` prints the duplicate directory trees. `info`
|
||||
prints the database's path, size and record count.
|
||||
|
||||
The database is `/var/lib/sfdupes/db.sqlite` unless `SFDUPES_DATABASE` names
|
||||
another file. The first `scan` creates it.
|
||||
|
||||
Every command writes its data to stdout and everything else to stderr: progress,
|
||||
warnings, summaries and help text.
|
||||
warnings, summaries and help text. stdout and stderr are a program's two output
|
||||
streams. The shell shows both on the terminal, and `>` redirects stdout alone.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -60,11 +64,12 @@ go install sneak.berlin/go/sfdupes@main
|
||||
The binary goes to `$(go env GOPATH)/bin`, or to `$GOBIN` when that is set.
|
||||
|
||||
`sfdupes --version` prints `sfdupes VERSION` and exits. For a binary from
|
||||
`go install`, `VERSION` is the version Go recorded in it: a release tag such as
|
||||
`v0.0.3`, or for `@main` a pseudo-version ending in the commit's hash. A binary
|
||||
built from a clone carries the git tag or commit it was built from. `VERSION` is
|
||||
`dev` only when the binary carries no version at all. To see everything Go
|
||||
recorded in an installed binary:
|
||||
`go install`, `VERSION` is the version Go recorded in it. For `@latest` that is
|
||||
a release tag such as `v0.0.3`. For `@main` it is a pseudo-version, Go's version
|
||||
string for an untagged commit, which ends in the commit's hash. A binary built
|
||||
from a clone carries the git tag or commit it was built from. `VERSION` is `dev`
|
||||
only when the binary carries no version at all. To see everything Go recorded in
|
||||
an installed binary:
|
||||
|
||||
```sh
|
||||
go version -m "$(command -v sfdupes)"
|
||||
@@ -142,8 +147,8 @@ A row is a candidate, not proof:
|
||||
changed or gone since.
|
||||
- A file of 100 MiB or more is compared only on samples of its content (see
|
||||
"Duplicate detection").
|
||||
- Two paths that are hard links to one file are listed as duplicates. They share
|
||||
their data, so removing one frees nothing.
|
||||
- Two paths that are hard links, two names for one file, are listed as
|
||||
duplicates. They share their data, so removing one frees nothing.
|
||||
|
||||
Compare a pair byte for byte before removing either copy. For the row
|
||||
`/srv/a/big.iso`, `/srv/b/big-copy.iso`, `4294967296`:
|
||||
@@ -176,7 +181,7 @@ symlink, which `trees` does not see.
|
||||
|
||||
Duplicate finders that hash every file in full do not scale to the target
|
||||
environment. That is about ten million files and 150 TB. The disks may be slow
|
||||
or busy, as a ZFS pool is while it rebuilds a disk.
|
||||
or busy, as a ZFS storage pool is while it rebuilds a disk.
|
||||
|
||||
`sfdupes` reads a file under 100 MiB in full. It reads a larger file only at its
|
||||
two ends and at 1 MiB per gigabyte. A full read of a small file is cheap, and it
|
||||
@@ -212,25 +217,11 @@ In order:
|
||||
3. **Scan incrementally, analyze offline.** The scan keeps a persistent
|
||||
database, and an unchanged file is never read again. `report`, `trees` and
|
||||
`info` work from the database alone and never touch the scanned filesystem.
|
||||
`scan` is built to run from cron; the reports run at any time against the
|
||||
last completed scan.
|
||||
`scan` is built to run from cron; the reports run at any time, against what
|
||||
the database holds.
|
||||
4. **Keep the streams apart.** Everything on stdout is machine-readable data.
|
||||
Progress, warnings, summaries, help and usage text go to stderr.
|
||||
|
||||
### Constraints
|
||||
|
||||
- Language: Go, module `sneak.berlin/go/sfdupes`, binary `sfdupes`.
|
||||
- Dependencies beyond the standard library:
|
||||
- `github.com/spf13/cobra` for the command line;
|
||||
- `github.com/schollz/progressbar/v3` for the progress display;
|
||||
- `golang.org/x/term`, to tell whether stderr is a terminal;
|
||||
- `modernc.org/sqlite`, the SQLite driver;
|
||||
- `golang.org/x/sys`, for the scan lock and the list of mounted filesystems.
|
||||
- `modernc.org/sqlite` is pure Go, so no build needs cgo, Go's bridge to C code.
|
||||
The `Makefile` exports `CGO_ENABLED=0`.
|
||||
- `report` and `trees` are deterministic: the same database contents give the
|
||||
same output, whatever order the records were inserted in.
|
||||
|
||||
### Subcommands
|
||||
|
||||
- `scan` walks the filesystem and synchronizes the database: one record per
|
||||
@@ -252,6 +243,13 @@ A missing or unknown subcommand prints the usage text on stderr and exits 2.
|
||||
|
||||
The reference below is also the program's specification.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No byte-for-byte compare, and no deleting or linking of duplicates. The
|
||||
reports are advisory; acting on them is your job.
|
||||
- No persistence beyond the SQLite database; no export or import formats.
|
||||
- No daemon or filesystem watcher; scheduling rescans is cron's job.
|
||||
|
||||
## Reference
|
||||
|
||||
### Settings
|
||||
@@ -260,15 +258,16 @@ The reference below is also the program's specification.
|
||||
database is `/var/lib/sfdupes/db.sqlite`. There is no command-line flag for
|
||||
it. The path names the file exactly, whatever characters it holds, `?`, `#`
|
||||
and `%` included. A relative path is relative to the working directory.
|
||||
- `--workers N` (`scan`) sets the number of workers in the walk and hash phases.
|
||||
The default is the number of CPUs. `N` must be at least 1; a smaller value is
|
||||
a usage error, reported in one line on stderr with exit 2 before anything is
|
||||
scanned.
|
||||
- `--workers N` (`scan`) sets how many directories the walk reads at once, and
|
||||
how many files the hash phase reads at once. The default is the number of
|
||||
CPUs. `N` must be at least 1; a smaller value is a usage error, reported in
|
||||
one line on stderr with exit 2 before anything is scanned.
|
||||
- `-x`, `--one-file-system` (`scan`) keeps the walk of each operand on that
|
||||
operand's filesystem, as the same flag does for `du` and `rsync`. By default
|
||||
the walk crosses filesystem boundaries.
|
||||
- `--no-default-excludes` (`scan`) lets the walk enter the default excludes (see
|
||||
"Rules for the walk").
|
||||
- `--no-default-excludes` (`scan`) lets the walk enter the default excludes: the
|
||||
system directories the walk otherwise leaves out, such as `/proc` and `/tmp`
|
||||
on Linux (see "Rules for the walk").
|
||||
- `--version`, `-v` prints `sfdupes VERSION` (see "Install") to stdout and exits
|
||||
0, writing nothing to stderr.
|
||||
- `--help`, `-h`, alone or after a subcommand, prints the help text to stderr
|
||||
@@ -278,8 +277,9 @@ The reference below is also the program's specification.
|
||||
### Duplicate detection
|
||||
|
||||
Two files are duplicates when they have the same size and the same three hashes.
|
||||
`scan` computes the hashes and stores them in the database. `report` and `trees`
|
||||
group the files whose size and hashes all match, whichever scan recorded them.
|
||||
A file's size and its three hashes together are its signature. `scan` computes
|
||||
the hashes and stores them in the database. `report` and `trees` group the files
|
||||
with the same signature, whichever scan recorded them.
|
||||
|
||||
Every record holds three SHA-256 hashes:
|
||||
|
||||
@@ -287,10 +287,6 @@ Every record holds three SHA-256 hashes:
|
||||
- `tail`, the hash of the file's last 64 KiB;
|
||||
- `content`, the content hash described below.
|
||||
|
||||
In a file of 64 KiB or less, each window is the whole file, so all three hashes
|
||||
are the hash of the whole file. An empty file has fixed hashes and is never
|
||||
opened.
|
||||
|
||||
The content hash depends on the file's size:
|
||||
|
||||
- Under 100 MiB, it is the SHA-256 of the whole file. A match proves identical
|
||||
@@ -300,10 +296,14 @@ The content hash depends on the file's size:
|
||||
samples go into one hash in order. A sample that would run past the end of the
|
||||
file stops there.
|
||||
|
||||
The sampled hash is probabilistic by design. Bytes outside the samples and
|
||||
outside the first and last 64 KiB are never read. Two large files that differ
|
||||
only in such bytes are reported as duplicates. That is the price of never
|
||||
reading a 150 GiB file end to end.
|
||||
In a file of 64 KiB or less, each 64 KiB stretch is the whole file, so all three
|
||||
hashes are the hash of the whole file. An empty file has fixed hashes and is
|
||||
never opened.
|
||||
|
||||
The sampled hash is not exact. Bytes outside the samples and outside the first
|
||||
and last 64 KiB are never read. Two large files that differ only in such bytes
|
||||
are reported as duplicates. That is the price of never reading a 150 GiB file
|
||||
end to end.
|
||||
|
||||
Only files of the same size are compared, so their samples lie at the same
|
||||
offsets. A file under 100 MiB and one of 100 MiB or more never share a size, so
|
||||
@@ -322,25 +322,27 @@ default. An operand may be a directory or a regular file.
|
||||
- `scan` with no operand is a usage error: the usage text goes to stderr, and
|
||||
the exit code is 2.
|
||||
- An operand that does not exist is a fatal error, exit 1.
|
||||
- Each operand is made absolute and cleaned before the walk, without resolving
|
||||
symlinks. Records are keyed by that absolute path, so results do not depend on
|
||||
the working directory.
|
||||
- All operands belong to one scan, and every operand seeds the same pool of walk
|
||||
workers.
|
||||
- Each operand is made absolute before the walk. A `.` or `..` in it is resolved
|
||||
as text, and symlinks are not followed. Records are keyed by that absolute
|
||||
path, so results do not depend on the working directory.
|
||||
- All operands belong to one scan, and all are walked at once by the same
|
||||
workers (see "Concurrency").
|
||||
- An operand that repeats another, or lies under another, is dropped before the
|
||||
walk, so every file is reached once. An operand under another is kept when the
|
||||
outer operand's walk would not reach it. That happens when the inner operand,
|
||||
or a directory between the two, is one of these:
|
||||
- a default exclude;
|
||||
- a directory that is not on a persistent filesystem;
|
||||
walk, without a line, so every file is reached once. An operand under another
|
||||
is kept when the outer operand's walk would not reach it. That happens when
|
||||
the inner operand, or a directory between the two, is one of these:
|
||||
- a default exclude (see "Rules for the walk");
|
||||
- a directory on a filesystem that is not persistent: one the kernel makes
|
||||
up or keeps in memory (see "Rules for the walk");
|
||||
- with `-x`, a directory on another filesystem than the outer operand.
|
||||
- An operand that is a symlink, socket, FIFO or device node is not scanned, and
|
||||
neither is a directory named `.zfs`. `scan` prints a warning naming the path
|
||||
and what it is, counts it as skipped, and drops the operand. Another operand
|
||||
beneath it is still scanned. The records beneath a dropped operand count as
|
||||
outside the scanned operands, unless another operand covers them.
|
||||
- An operand that is not on a persistent filesystem is dropped the same way,
|
||||
with a line that is not a warning, and is not counted as skipped.
|
||||
beneath it is still scanned. The scan keeps the records beneath a dropped
|
||||
operand, unless another operand covers them.
|
||||
- An operand on a filesystem that is not persistent is dropped too. `scan`
|
||||
prints a line naming it and its filesystem type, and does not count it as
|
||||
skipped.
|
||||
- A scan whose every operand is dropped walks nothing and exits 0.
|
||||
|
||||
#### What a scan does to the database
|
||||
@@ -356,19 +358,24 @@ operands:
|
||||
is compared to the nanosecond, as finely as the filesystem records it. A
|
||||
same-size rewrite counts as a change whenever the filesystem gives it a later
|
||||
mtime, even within the same second.
|
||||
- A record under a scanned operand whose file the scan did not verify is
|
||||
deleted. That covers deleted files, and also paths that failed to stat or hash
|
||||
this run. The database only ever holds signatures that the latest scan
|
||||
covering them could verify. A later successful scan adds such files again.
|
||||
- The scan deletes each record under a scanned operand whose file it could not
|
||||
verify. That covers deleted files, and also paths whose details or contents
|
||||
the scan could not read this run. So the database holds only what the latest
|
||||
scan over each path could verify. A later successful scan adds such files
|
||||
again.
|
||||
- Records outside the scanned operands are left alone, so disjoint trees can be
|
||||
scanned on different schedules into one database.
|
||||
- A record at or below a path the walk left out on purpose counts as outside the
|
||||
scanned operands, so it is kept. The walk leaves out a default exclude, a path
|
||||
that is not on a persistent filesystem, and, with `-x`, a directory on another
|
||||
filesystem than its operand. A filesystem mounted under an operand can
|
||||
therefore be scanned on its own schedule into the same database. Records below
|
||||
an operand that is itself at or inside such a path are verified by that
|
||||
operand's own walk, so they are deleted when their files are gone.
|
||||
- The scan keeps every record at or below a path that the walk leaves out on
|
||||
purpose. Such records count as outside the scanned operands. The walk leaves
|
||||
out:
|
||||
- a default exclude;
|
||||
- a path that is not on a persistent filesystem;
|
||||
- with `-x`, a directory on another filesystem than its operand.
|
||||
|
||||
A filesystem mounted under an operand can therefore be scanned on its own
|
||||
schedule into the same database. An operand at or inside such a path is
|
||||
walked on its own. Its walk verifies the records below it, so they are
|
||||
deleted when their files are gone.
|
||||
|
||||
#### Phases
|
||||
|
||||
@@ -377,23 +384,27 @@ phases.
|
||||
|
||||
1. `load` reads the existing records. Each record under the operands is kept in
|
||||
memory by path, with its size and mtime, for the walk to compare against.
|
||||
2. `walk` lists the trees under all operands with the walk worker pool. Each
|
||||
worker reads one directory at a time, hands subdirectories back to the queue,
|
||||
and stats each regular file as it finds it. The walk compares each file with
|
||||
its record. An unchanged file needs nothing more. Every new or changed file
|
||||
is passed to the hash phase.
|
||||
2. `walk` lists the trees under all operands. Several workers (see
|
||||
"Concurrency") share one queue of directories to read. Each worker reads one
|
||||
directory at a time, hands the subdirectories it finds back to the queue, and
|
||||
stats each regular file as it finds it. To stat a file is to read its size,
|
||||
type and times without opening it. The walk compares each file with its
|
||||
record. An unchanged file needs nothing more. Every new or changed file is
|
||||
passed to the hash phase.
|
||||
3. `hash` reads every new or changed file and computes its three hashes. Files
|
||||
are hashed in inode order, which keeps a spinning disk from seeking back and
|
||||
forth. Paths that are hard links to one file are read once and share the
|
||||
result, so a farm of hard-linked backups costs one read per file. Completed
|
||||
records are committed to the database 10,000 at a time while hashing runs. A
|
||||
scan interrupted after hours therefore keeps everything hashed so far.
|
||||
are read in inode order. An inode is the filesystem's own record of a file,
|
||||
and files with nearby inode numbers usually lie near each other on disk, so
|
||||
this order keeps a spinning disk from seeking back and forth. Paths that are
|
||||
hard links to one file are read once and share the result, so a backup tree
|
||||
made of hard links costs one read per file. Completed records are committed
|
||||
to the database 10,000 at a time while hashing runs. A scan interrupted after
|
||||
hours therefore keeps everything hashed so far.
|
||||
4. `update` commits the last partial batch of hashed records, then deletes the
|
||||
records the scan did not verify.
|
||||
|
||||
Reading a tree one directory at a time waits on the disk at every step and takes
|
||||
hours at millions of files. Reading many directories at once is what makes the
|
||||
walk practical on large or busy pools.
|
||||
walk practical on large or busy storage pools.
|
||||
|
||||
#### Rules for the walk
|
||||
|
||||
@@ -401,8 +412,9 @@ walk practical on large or busy pools.
|
||||
passed over, and symlinks are never followed.
|
||||
- The walk never enters a directory named `.zfs`. Such a directory holds ZFS
|
||||
snapshots, and walking it would list every file once per snapshot.
|
||||
- The walk never records the scan's own database files: the database, its `-wal`
|
||||
and `-shm` files and its `.lock` file. It knows them by device and inode, so
|
||||
- The walk never records the scan's own database files. Those are the database,
|
||||
its `.lock` file, and the `-wal` and `-shm` files SQLite keeps beside it while
|
||||
the scan runs (see "Concurrent reads"). It knows them by device and inode, so
|
||||
it passes over them however it reaches them, through a symlinked directory
|
||||
included. It prints no line for them and does not count them as skipped. A
|
||||
record an earlier scan left for one of them is deleted.
|
||||
@@ -422,25 +434,27 @@ walk practical on large or busy pools.
|
||||
```
|
||||
|
||||
`devfs` is the macOS filesystem of `/dev`; the others are Linux names. As it
|
||||
starts, `scan` lists the mounted filesystems and notes the device of each
|
||||
one of those types. On Linux the list comes from `/proc/self/mountinfo`,
|
||||
which gives each filesystem's own device. On macOS it comes from the
|
||||
system's mount list, and the device is read from the mount point. Where two
|
||||
filesystems share a mount point on macOS, the one mounted last counts. On
|
||||
any other system the list is empty. If `scan` cannot read the list, it fails
|
||||
with exit
|
||||
1. No flag turns this rule off. The walk never enters a directory, and never
|
||||
records a file, on one of those devices, however it reaches it, through a
|
||||
bind mount included. For each path it leaves out this way, `scan` prints
|
||||
a line on stderr naming the path and its filesystem type:
|
||||
starts, `scan` lists the mounted filesystems and notes the device ID of each
|
||||
one of those types. The device ID is the number by which the kernel tells
|
||||
filesystems apart. On Linux the list comes from `/proc/self/mountinfo`,
|
||||
which gives each filesystem's own device ID. On macOS it comes from the
|
||||
system's mount list, and the device ID is read from the mount point. Where
|
||||
two filesystems share a mount point on macOS, the one mounted last counts.
|
||||
On any other system the list is empty. A list that `scan` cannot read is a
|
||||
fatal error, and nothing is scanned. No flag turns this rule off.
|
||||
|
||||
The walk never enters a directory on one of those devices, and never records
|
||||
a file on one. That holds however the walk reaches the path, through a bind
|
||||
mount included. A bind mount shows a directory at a second path, and the
|
||||
files under it keep their device ID. For each path it leaves out this way,
|
||||
`scan` prints a line on stderr naming the path and its filesystem type:
|
||||
|
||||
```
|
||||
scan: skipped /dev/shm (tmpfs, not a persistent filesystem)
|
||||
```
|
||||
|
||||
The line is not a warning, and the path is not counted as skipped. The
|
||||
records below the path are kept, as described under "What a scan does to the
|
||||
database".
|
||||
The path is not counted as skipped. The records below it are kept, as
|
||||
described under "What a scan does to the database".
|
||||
|
||||
- Below an operand, the walk does not enter a default exclude. The default
|
||||
excludes hold the kernel's view of the running system and temporary files, so
|
||||
@@ -449,8 +463,9 @@ walk practical on large or busy pools.
|
||||
- On Linux: `/proc`, `/sys`, `/dev`, `/run`, `/tmp` and `/var/tmp`.
|
||||
- On macOS: `/dev`, `/private/tmp`, `/private/var/tmp`, `/private/var/vm`,
|
||||
`/System/Volumes/VM` and `/System/Volumes/Data`. A walk of `/` reaches
|
||||
everything in `/System/Volumes/Data` through firmlinks already; walking it
|
||||
as well would record each of those files twice.
|
||||
everything in `/System/Volumes/Data` already, through firmlinks: the links
|
||||
macOS uses to show the data volume inside the system volume. Walking it as
|
||||
well would record each of those files twice.
|
||||
- On any other system: none.
|
||||
|
||||
`--no-default-excludes` turns the default excludes off. What is not on a
|
||||
@@ -463,12 +478,11 @@ walk practical on large or busy pools.
|
||||
scan: skipped /var/tmp (default exclude)
|
||||
```
|
||||
|
||||
The line is not a warning, and the directory is not counted as skipped. A
|
||||
default exclude that is not on a persistent filesystem, as `/proc` is, gets
|
||||
the line for its filesystem instead. One that is itself an operand gets no
|
||||
line, because it is walked as that operand. The records below a default
|
||||
exclude the walk did not enter are kept, as described under "What a scan
|
||||
does to the database".
|
||||
The directory is not counted as skipped. A default exclude that is not on a
|
||||
persistent filesystem, as `/proc` is, gets the line for its filesystem
|
||||
instead. One that is itself an operand gets no line, because it is walked as
|
||||
that operand. The scan keeps the records below a default exclude that the
|
||||
walk did not enter, as described under "What a scan does to the database".
|
||||
|
||||
- A path that cannot be read, for lack of permission or any other error, gets a
|
||||
one-line warning on stderr and is skipped. The scan continues, and the summary
|
||||
@@ -482,19 +496,22 @@ walk practical on large or busy pools.
|
||||
|
||||
#### Concurrency
|
||||
|
||||
The walk phase and the hash phase each use a pool of `--workers` workers. The
|
||||
walk works on several directories at once, and the hash phase on several files
|
||||
at once. On spinning disks both phases wait mostly on seeks, so a `--workers`
|
||||
value well above the CPU count can help on pools with many spindles. The main
|
||||
thread of the program owns the database writes and the progress counts. Each
|
||||
progress display draws from a thread of its own and never blocks the workers.
|
||||
The walk phase and the hash phase each use a pool of workers. A worker is one
|
||||
thread of the program, and each pool has `--workers` of them. The walk works on
|
||||
several directories at once, and the hash phase on several files at once. On
|
||||
spinning disks both phases wait mostly on seeks, the time the disk head takes to
|
||||
move. A `--workers` value well above the CPU count can therefore help on storage
|
||||
pools with many disks. The main thread of the program owns the database writes
|
||||
and the progress counts. Each progress display draws from a thread of its own
|
||||
and never blocks the workers.
|
||||
|
||||
#### What `scan` prints
|
||||
|
||||
`scan` writes nothing to stdout. On stderr it prints, in this order:
|
||||
|
||||
- one line naming the database (see "Database");
|
||||
- a line for each operand it drops;
|
||||
- for each dropped operand, in operand order, the warning or the line described
|
||||
under "Operands". A repeated operand, or one under another, gets neither;
|
||||
- the progress display of each phase (see "Progress"), with a warning for each
|
||||
path it skips and a line for each path the walk leaves out on purpose, as they
|
||||
happen;
|
||||
@@ -527,8 +544,9 @@ The next scan skips the records already written and converges as usual.
|
||||
|
||||
After the first signal `scan` stops catching signals. A second one ends it at
|
||||
once, as any uncaught signal does: the records not yet committed are lost, and
|
||||
the database stays valid. `SIGTERM` is always caught. A `SIGINT` that `scan`
|
||||
inherits as ignored, as a shell's background job does, stays ignored.
|
||||
the database stays valid. `scan` catches `SIGTERM` even when it inherits it as
|
||||
ignored. A `SIGINT` that `scan` inherits as ignored, as a background job started
|
||||
from a script does, stays ignored.
|
||||
|
||||
### `report` mode
|
||||
|
||||
@@ -539,7 +557,7 @@ Its output is the same whether or not the scanned filesystem is still mounted.
|
||||
|
||||
Processing:
|
||||
|
||||
- Records are grouped by the key `(size, head, tail, content)`.
|
||||
- Records are grouped by signature: `(size, head, tail, content)`.
|
||||
- Every group with two or more paths is a duplicate group.
|
||||
- Within a group, paths are sorted in byte order. The first is `first`, and
|
||||
every other path is a `dupe`.
|
||||
@@ -557,8 +575,8 @@ smaller size.
|
||||
|
||||
#### Report output format
|
||||
|
||||
TSV on stdout: a header line, then one row per duplicate file. A group of N
|
||||
paths gives N-1 rows.
|
||||
TSV (tab-separated values) on stdout: a header line, then one row per duplicate
|
||||
file. A group of N paths gives N-1 rows.
|
||||
|
||||
```
|
||||
first dupe size
|
||||
@@ -566,16 +584,20 @@ first dupe size
|
||||
/srv/a/big.iso /srv/c/big-copy2.iso 4294967296
|
||||
```
|
||||
|
||||
Paths are raw bytes and may hold any byte except NUL. The path columns are
|
||||
escaped, so that every row stays one line of tab-separated fields. A backslash
|
||||
is written `\\`, a tab `\t`, a newline `\n` and a carriage return `\r`. Every
|
||||
other byte is written unchanged, including bytes that are not valid UTF-8.
|
||||
Undoing the four escapes gives back the stored path. Grouping and ordering use
|
||||
the stored path. The warnings `scan` prints are escaped the same way, so each
|
||||
warning stays one line.
|
||||
Paths are raw bytes and may hold any byte except NUL, the zero byte. The path
|
||||
columns are escaped, so that every row stays one line of tab-separated fields. A
|
||||
backslash is written `\\`, a tab `\t`, a newline `\n` and a carriage return
|
||||
`\r`. Every other byte is written unchanged, including bytes that are not valid
|
||||
UTF-8. Undoing the four escapes gives back the stored path. Grouping and
|
||||
ordering use the stored path. The warnings `scan` prints are escaped the same
|
||||
way, so each warning stays one line.
|
||||
|
||||
The summary line on stderr gives the records read, the duplicate groups, the
|
||||
dupe files and the reclaimable bytes, which are the sum of `size` over all rows:
|
||||
The summary line on stderr gives:
|
||||
|
||||
- the records read;
|
||||
- the duplicate groups;
|
||||
- the dupe files, which is the number of rows;
|
||||
- the reclaimable bytes: the sum of `size` over all rows.
|
||||
|
||||
```
|
||||
report: 14 records read, 5 duplicate groups, 8 dupe files, 10.0 KiB reclaimable
|
||||
@@ -584,15 +606,15 @@ report: 14 records read, 5 duplicate groups, 8 dupe files, 10.0 KiB reclaimable
|
||||
### `trees` mode
|
||||
|
||||
`trees` reads the same database as `report` and takes no positional arguments.
|
||||
It reports whole duplicate directory trees: directories under which the same
|
||||
relative paths hold files with the same signatures. Like `report`, it never
|
||||
touches the scanned filesystem. It rebuilds the directory hierarchy from the
|
||||
paths in the records, split on `/`.
|
||||
It reports whole duplicate directory trees. Two directories are duplicate trees
|
||||
when they hold the same relative paths, and each path holds a file with the same
|
||||
signature in both. Like `report`, it never touches the scanned filesystem. It
|
||||
rebuilds the directory hierarchy from the paths in the records, split on `/`.
|
||||
|
||||
Definitions:
|
||||
|
||||
- A file's **signature** is `(size, head, tail, content)`. The mtime is not part
|
||||
of it.
|
||||
- A file's **signature** is `(size, head, tail, content)`, as for `report`. The
|
||||
mtime is not part of it.
|
||||
- A directory's **digest** is a SHA-256 hash computed from the bottom up. Each
|
||||
child contributes one entry. A file's entry is its name and signature; a
|
||||
subdirectory's entry is its name and digest. The entries are sorted in byte
|
||||
@@ -606,10 +628,11 @@ Processing:
|
||||
- Build the hierarchy, compute every directory's digest, and group the
|
||||
directories by digest. Every group with two or more directories is a
|
||||
duplicate-tree group.
|
||||
- Report only the largest trees. A group is left out when its members' parents
|
||||
are distinct directories that all share one digest: the parents' group, or an
|
||||
ancestor's, already implies it. A group holding sibling directories, or
|
||||
members whose parents differ, is always reported.
|
||||
- Only the outermost duplicate trees are reported. A group is left out when its
|
||||
members' parents are distinct directories that all have the same digest: the
|
||||
parents' group, or an ancestor's, already implies it. A group holding sibling
|
||||
directories, or members whose parents have different digests, is always
|
||||
reported.
|
||||
- Within a group, paths are sorted in byte order. The first is `first`, and
|
||||
every other path is a `dupe`.
|
||||
- Groups are ordered by total tree size, largest first, then by `first` path.
|
||||
@@ -644,9 +667,12 @@ first dupe files size
|
||||
The paths are escaped as described under "Report output format". The root
|
||||
directory's path is `/`.
|
||||
|
||||
The summary line on stderr gives the records read, the duplicate-tree groups,
|
||||
the dupe trees and the reclaimable bytes, which are the sum of `size` over all
|
||||
rows:
|
||||
The summary line on stderr gives:
|
||||
|
||||
- the records read;
|
||||
- the duplicate-tree groups;
|
||||
- the dupe trees, which is the number of rows;
|
||||
- the reclaimable bytes: the sum of `size` over all rows.
|
||||
|
||||
```
|
||||
trees: 14 records read, 1 duplicate tree groups, 1 dupe trees, 3.0 KiB reclaimable
|
||||
@@ -674,9 +700,9 @@ size of recorded files 150000000000000 (136.4 TiB)
|
||||
- `database file size` and `-wal file size` are in bytes, then in human units.
|
||||
The `-wal` line appears only while a `-wal` file is beside the database, as
|
||||
during a scan.
|
||||
- `modified` is when the database file was last changed, in UTC. While the
|
||||
database is in WAL mode (see "Database"), the newest changes may be in the
|
||||
`-wal` file alone.
|
||||
- `modified` is when the database file was last changed, in UTC. While a scan
|
||||
runs, the newest changes may be in the `-wal` file alone (see "Concurrent
|
||||
reads").
|
||||
- `schema version` is the database's `PRAGMA user_version`. A database with
|
||||
another version than this build's is refused, as by `report`.
|
||||
- `records` is the number of records. `size of recorded files` adds up the sizes
|
||||
@@ -722,17 +748,18 @@ the error for a missing database comes after it.
|
||||
a fatal error, exit 1, telling you to run `scan` first.
|
||||
- `report`, `trees` and `info` open the database read-only. They need read
|
||||
access to the database file and no write access to its directory. While the
|
||||
database is in WAL mode, they also read the `-wal` and `-shm` files beside it,
|
||||
which SQLite creates with the database file's permissions.
|
||||
database has `-wal` and `-shm` files beside it (see "Concurrent reads"), they
|
||||
read those too. SQLite creates them with the database file's permissions.
|
||||
|
||||
#### The scan lock
|
||||
|
||||
Only one `scan` runs against a database at a time.
|
||||
|
||||
- For its whole run, `scan` holds an exclusive `flock(2)` lock on a lock file
|
||||
beside the database. The lock file's name is the database's path, with
|
||||
symlinks resolved, plus `.lock`: `/var/lib/sfdupes/db.sqlite.lock` by default.
|
||||
Every path to one database therefore names the same lock file.
|
||||
- For its whole run, `scan` holds an exclusive lock, taken with the `flock(2)`
|
||||
system call, on a lock file beside the database. The lock file's name is the
|
||||
database's path, with symlinks resolved, plus `.lock`:
|
||||
`/var/lib/sfdupes/db.sqlite.lock` by default. Every path to one database
|
||||
therefore names the same lock file.
|
||||
- `scan` takes the lock before it walks the filesystem or opens the database.
|
||||
Just before that, it creates the database file, empty, if it is missing, so
|
||||
that the lock file is named the same way before the first scan as after. A
|
||||
@@ -748,29 +775,34 @@ Only one `scan` runs against a database at a time.
|
||||
|
||||
#### Concurrent reads
|
||||
|
||||
- While `scan` runs, the database is in write-ahead log (WAL) mode, with a
|
||||
10-second busy timeout. In WAL mode SQLite appends changes to a `-wal` file
|
||||
beside the database, and readers do not block the writer. Running a report
|
||||
during a scan is therefore safe.
|
||||
- While `scan` runs, the database is in write-ahead log (WAL) mode. In WAL mode
|
||||
SQLite appends changes to a `-wal` file beside the database, and keeps an
|
||||
index of that file in a `-shm` file. Readers do not block the writer, so
|
||||
running a report during a scan is safe.
|
||||
- Every connection has a busy timeout of 10 seconds: how long it waits for a
|
||||
database another connection has locked before it gives up.
|
||||
- The filesystem is authoritative, and the database follows it. Hashed records
|
||||
are committed 10,000 at a time while the scan runs, so a report may see a
|
||||
scan's changes partly applied. A scan that dies partway leaves a valid
|
||||
database holding every batch committed so far, and the next scan carries on
|
||||
from there.
|
||||
- When `scan` closes the database, it switches it back to rollback-journal mode,
|
||||
so that between scans the database file alone holds everything. Each switch
|
||||
SQLite's default mode, in which the database file alone holds everything.
|
||||
Between scans a reader therefore needs no `-wal` or `-shm` file. Each switch
|
||||
needs the database to itself. A `scan` that starts while a report is still
|
||||
reading waits for it up to the busy timeout, then fails. A `scan` that ends
|
||||
while a report has the database open prints a warning and leaves the database
|
||||
in WAL mode until the next scan. `report` writes its rows while it reads, so
|
||||
it is still reading while a pager or a stalled pipe holds up its output.
|
||||
it is still reading while a pager such as `less`, or a stalled pipe, holds up
|
||||
its output.
|
||||
|
||||
#### What `scan` writes, and when
|
||||
|
||||
- As it starts, before the walk: the directory, an empty database file and the
|
||||
`.lock` file, each only if missing.
|
||||
- Then, still before the walk: the schema in a new database, and the `-wal` and
|
||||
`-shm` files, which stay beside the database while the scan runs.
|
||||
- Then, still before the walk: the schema, the table and index described under
|
||||
"Schema", in a new database, and the `-wal` and `-shm` files, which stay
|
||||
beside the database while the scan runs.
|
||||
- During the `load` and `walk` phases: no records. A first scan's database holds
|
||||
no records until the hash phase, however long its walk runs.
|
||||
- During the `hash` phase: records, committed 10,000 at a time.
|
||||
@@ -805,7 +837,7 @@ CREATE INDEX files_signature ON files (size, head, tail, content);
|
||||
`mtime_nsec` the nanoseconds past that second. Split this way, they hold any
|
||||
mtime a filesystem can record, before 1678 or after 2262 included, which one
|
||||
64-bit count of nanoseconds cannot. They serve change detection only and are
|
||||
not part of the duplicate key.
|
||||
not part of the signature.
|
||||
- `head`, `tail` and `content` each hold the raw 32 bytes of a SHA-256 (see
|
||||
"Duplicate detection"). A record whose hash is not 32 bytes is an error.
|
||||
- `files_signature` lets `report` read the records in signature order without
|
||||
@@ -824,9 +856,10 @@ The phases, and what each display counts:
|
||||
- `load` counts the records first, then reads them against that count. While
|
||||
it counts, the display says `counting the records`.
|
||||
- `walk` has no exact total. Its estimate is the number of records the last
|
||||
scan left under the operands, shown with a `~`. A first scan has no
|
||||
estimate. The time remaining is `unknown` when there is no estimate, and
|
||||
once the walk passes the estimate.
|
||||
scan left under the operands, shown with a `~`. A scan with no records
|
||||
under its operands, such as a first scan, has no estimate. The time
|
||||
remaining is `unknown` when there is no estimate, and once the walk passes
|
||||
the estimate.
|
||||
- `hash` and `update` have exact totals. Only the files that need hashing
|
||||
count toward the hash total, so its time remaining is meaningful.
|
||||
- `report`: `[1/2] open`, `[2/2] read`.
|
||||
@@ -852,7 +885,8 @@ Each display is one line holding:
|
||||
- the current rate, in the items the phase counts: files, records, directories
|
||||
or groups. It covers the items completed over the last 5 seconds, or, when
|
||||
stderr is not a terminal, since the line before. It falls to 0 while nothing
|
||||
completes, and a burst after a wait shows as a burst;
|
||||
completes. A burst of items after a wait shows as a high rate for the next 5
|
||||
seconds, then the rate falls again;
|
||||
- the elapsed time, labelled `elapsed`;
|
||||
- the time remaining, labelled `remain`: a duration such as `12m9s`, or
|
||||
`unknown`. It comes from the phase's average rate since it started counting,
|
||||
@@ -882,15 +916,18 @@ wait in two places names both, as `disk/db`:
|
||||
|
||||
On a terminal:
|
||||
|
||||
- Every display starts with a spinner, followed by its line. The display is
|
||||
drawn again several times a second, so the spinner turns and the elapsed time
|
||||
keeps moving while a phase waits for its next item.
|
||||
- Every display starts with a spinner, a turning character, followed by its
|
||||
line. The display is drawn again several times a second, so the spinner turns
|
||||
and the elapsed time keeps moving while a phase waits for its next item.
|
||||
- A phase with an exact total ends its line with its percent and a bar 10
|
||||
columns wide, at the terminal's right edge. The percent after the count is
|
||||
left out then. A phase with nothing to do shows no bar. Every phase has an
|
||||
exact total once it has counted its work, except `scan`'s `walk`, the `open`
|
||||
phase of `report` and `trees`, and both phases of `info`. On a terminal 100
|
||||
columns wide:
|
||||
exact total once it has counted its work, except these:
|
||||
- `scan`'s `walk`;
|
||||
- the `open` phase of `report` and `trees`;
|
||||
- both phases of `info`.
|
||||
|
||||
On a terminal 100 columns wide:
|
||||
|
||||
```
|
||||
⠹ [3/4] hash: [12345/98765], 92 files/s, elapsed 2m32s, remain 17m44s 12% |█ |
|
||||
@@ -900,10 +937,16 @@ On a terminal:
|
||||
line too wide for the terminal first leaves out its rate, or the words of a
|
||||
stalled phase, or what the phase is waiting for. It leaves out its bar next.
|
||||
It is cut short at the edge only when the four remaining parts do not fit on
|
||||
their own. Those are the phase and its place, the progress, the elapsed time
|
||||
and the time remaining. That happens hours into a scan of millions of files,
|
||||
on a terminal narrower than about 75 columns. A terminal made narrower while a
|
||||
phase runs is fitted at the next draw.
|
||||
their own. Those are:
|
||||
- the phase and its place;
|
||||
- the progress;
|
||||
- the elapsed time;
|
||||
- the time remaining.
|
||||
|
||||
That happens hours into a scan of millions of files, on a terminal narrower
|
||||
than about 75 columns. A terminal made narrower while a phase runs is fitted
|
||||
at the next draw.
|
||||
|
||||
- A warning printed during a phase lands on a line of its own, never inside the
|
||||
display. So does every line written to stdout when stdout and stderr are the
|
||||
same file: one terminal, or one pipe or file that both are sent to. When they
|
||||
@@ -920,17 +963,25 @@ the count it stopped at.
|
||||
### Error handling and exit codes
|
||||
|
||||
- `0`: success, even when files were skipped with warnings.
|
||||
- `1`: a fatal error, or a `scan` stopped by `SIGINT` or `SIGTERM`. The fatal
|
||||
errors are:
|
||||
- an operand that does not exist;
|
||||
- `1`: a fatal error, or a `scan` stopped by `SIGINT` or `SIGTERM`. A fatal
|
||||
error is one of these:
|
||||
- an operand that does not exist, or whose details `scan` cannot read;
|
||||
- a list of mounted filesystems that `scan` cannot read;
|
||||
- another `scan` already running against the same database;
|
||||
- a database that cannot be created, opened, read or written;
|
||||
- a database that cannot be created, opened, read or written, its directory
|
||||
and its lock file included;
|
||||
- a relative database path when the working directory cannot be read;
|
||||
- a database with another schema version, or one at version 0 that already
|
||||
has a `files` table;
|
||||
- a record whose `head`, `tail` or `content` is not 32 bytes;
|
||||
- a missing database for `report`, `trees` or `info`;
|
||||
- a failed write to stdout.
|
||||
- `2`: a usage error:
|
||||
- `2`: a usage error, which is one of these:
|
||||
- a missing or unknown subcommand;
|
||||
- an unknown flag;
|
||||
- `scan` with no operand;
|
||||
- `scan` with `--workers` below 1;
|
||||
- `--workers` without a value, or with a value that is not a whole number or
|
||||
is below 1;
|
||||
- `report`, `trees` or `info` with a positional argument.
|
||||
|
||||
A fatal error is reported in one line on stderr that starts with `sfdupes:`. A
|
||||
@@ -938,9 +989,9 @@ failed write to stdout, such as a full disk, is reported the same way. Two cases
|
||||
never reach `sfdupes` as a failed write:
|
||||
|
||||
- When the reader of a stdout pipe exits early, as in `sfdupes report | head`,
|
||||
the next write ends `sfdupes` with `SIGPIPE`. It ends quietly and without a
|
||||
summary, as `cat` or `sort` would. The shell reports the signal, status 141 in
|
||||
most shells, not exit 1.
|
||||
the next write ends `sfdupes` with `SIGPIPE`, the signal for a write to a
|
||||
closed pipe. It ends quietly and without a summary, as `cat` or `sort` would.
|
||||
The shell reports the signal, status 141 in most shells, not exit 1.
|
||||
- When stdout is closed outright, as in `sfdupes report >&-`, the Go runtime
|
||||
opens `/dev/null` in its place before `sfdupes` starts. The output is
|
||||
discarded, and the run succeeds, as with `> /dev/null`.
|
||||
@@ -955,43 +1006,52 @@ is POSIX `sh`, finds the repository root itself, and can be run from any
|
||||
directory.
|
||||
|
||||
- `script/bootstrap` installs what building and developing need. It can be run
|
||||
again at any time; installed tools are kept. `git`, `make` and `go` come from
|
||||
the first of nix, apt, brew or apk found on the host, and are only checked for
|
||||
presence. An installed node is used when its major version is 22. Otherwise
|
||||
node 22.17.0 is installed through nvm, from a release archive whose SHA-256
|
||||
the script checks, and yarn 1.22.22 is activated under it through corepack.
|
||||
With an installed node 22, a yarn already on `PATH` is used as it is.
|
||||
Otherwise yarn 1.22.22 is activated through corepack, or installed with `npm`
|
||||
when there is no corepack. `yarn install --frozen-lockfile` then installs the
|
||||
prettier that `package.json` and `yarn.lock` pin. The linter is never
|
||||
installed; it runs in Docker (see `script/lint`). `docker` is not installed
|
||||
either, and testing, linting and the image build need it. The script ends with
|
||||
`go mod download`.
|
||||
again at any time; installed tools are kept. `git`, `make` and `go` are
|
||||
checked for presence only. A missing one is installed from the first of `nix`,
|
||||
`apt`, `brew` or `apk` found on the host. An installed `node` is used when its
|
||||
major version is 22. Otherwise the script downloads `nvm`, checks the
|
||||
archive's SHA-256, and installs `node` 22.17.0 through it. It then activates
|
||||
`yarn` 1.22.22 under that `node` through `corepack`. With an installed `node`
|
||||
22, a `yarn` already on `PATH` is used as it is. Otherwise `yarn` 1.22.22 is
|
||||
activated through `corepack`, or installed with `npm` when there is no
|
||||
`corepack`. `yarn install --frozen-lockfile` then installs the `prettier` that
|
||||
`package.json` and `yarn.lock` pin. The linter is never installed; it runs in
|
||||
Docker (see `script/lint`). `docker` is not installed either, and testing,
|
||||
linting and the image build need it. The script ends with `go mod download`.
|
||||
- `script/setup` makes a fresh clone ready for development: `script/bootstrap`,
|
||||
then `script/install-precommit`.
|
||||
- `script/projectname` prints `sfdupes`. Scripts that need the name call it, so
|
||||
they stay identical across repositories.
|
||||
- `script/test` runs the test suite under the race detector, with a 90-second
|
||||
timeout and coverage enabled. On failure it reruns the suite verbosely, so
|
||||
that the log names the failing test. It builds the `Dockerfile`'s `test` phase
|
||||
alone and writes no image. The race detector needs cgo and a C compiler, so
|
||||
the phase starts from a digest-pinned Debian `golang` image, which has `gcc`.
|
||||
The tests run as `nobody`, because several of them make a file unreadable, and
|
||||
root reads it anyway.
|
||||
- `script/test` runs the test suite under the race detector, Go's check for
|
||||
unsynchronized access to memory from several threads. The suite runs with a
|
||||
90-second timeout and coverage enabled. On failure it reruns the suite
|
||||
verbosely, so that the log names the failing test. It builds the
|
||||
`Dockerfile`'s `test` phase alone and writes no image. The race detector needs
|
||||
cgo, Go's bridge to C code, and a C compiler. The phase therefore starts from
|
||||
a Debian `golang` image, which has `gcc`. The image is pinned by its digest,
|
||||
its content hash. The tests run as `nobody`, because several of them make a
|
||||
file unreadable, and root reads it anyway.
|
||||
- `script/lint` runs the linter. It builds the `Dockerfile`'s `lint` phase alone
|
||||
and writes no image. In the digest-pinned `golangci/golangci-lint` image, the
|
||||
gofmt check, `golangci-lint config verify` and `golangci-lint run` run as
|
||||
build steps, so a successful build is a clean lint. The linter never runs on
|
||||
the host, so a working `docker` is the one thing linting needs.
|
||||
and writes no image. The phase starts from the `golangci/golangci-lint` image,
|
||||
pinned by its digest, and runs three build steps:
|
||||
- the `gofmt` check;
|
||||
- `golangci-lint config verify`;
|
||||
- `golangci-lint run`.
|
||||
|
||||
A successful build is a clean lint. The linter never runs on the host, so a
|
||||
working `docker` is the one thing linting needs.
|
||||
|
||||
- `script/fmt` formats in place: the Go sources with `gofmt -s -w`, and every
|
||||
Markdown file with prettier. Prettier uses 4-space indents and wraps prose at
|
||||
80 columns, as `.prettierrc` sets. Both run on the host; prettier runs through
|
||||
yarn at the version `yarn.lock` pins. When the node on `PATH` is not node 22,
|
||||
the script loads the node 22.17.0 that `script/bootstrap` installed.
|
||||
Markdown file with `prettier`. `prettier` indents by 4 spaces and wraps prose,
|
||||
as `.prettierrc` sets and as the script also passes on the command line. It
|
||||
wraps at 80 columns, its default width. Both run on the host; `prettier` runs
|
||||
through `yarn` at the version `yarn.lock` pins. When the `node` on `PATH` is
|
||||
not `node` 22, the script loads the `node` 22.17.0 that `script/bootstrap`
|
||||
installed.
|
||||
- `script/fmt-check` is the read-only form of `script/fmt`: it names any
|
||||
unformatted file and exits non-zero instead of writing. gofmt and prettier
|
||||
unformatted file and exits non-zero instead of writing. `gofmt` and `prettier`
|
||||
both run every time, and each names itself when it fails. The `Dockerfile`'s
|
||||
`lint` phase runs the same gofmt check.
|
||||
`lint` phase runs the same `gofmt` check.
|
||||
- `script/check` runs `script/test`, `script/lint` and `script/fmt-check`, in
|
||||
that order, and changes nothing. The first two need `docker`; the third needs
|
||||
what `script/bootstrap` installs.
|
||||
@@ -1011,15 +1071,15 @@ directory.
|
||||
|
||||
Every `docker build` in `script/` passes a new `CACHEBUST` build argument. In
|
||||
every stage of the `Dockerfile`, `ARG CACHEBUST` comes after the steps that
|
||||
install system packages and download the Go modules, and before every step that
|
||||
lints, tests or compiles. Every `RUN` step after it runs again on each build.
|
||||
Without that, Docker would serve the test and lint steps from its cache on an
|
||||
unchanged tree, and the build would pass having run neither. The layers above
|
||||
the line stay cached until the base image, their own lines, `go.mod` or `go.sum`
|
||||
change. Go's build cache is kept between builds, so only what changed is
|
||||
compiled again. The tests run with `-count=1`, so no test reports a stored
|
||||
result. `golangci-lint run` keeps golangci-lint's cache as well, so an unchanged
|
||||
package is not linted again.
|
||||
install system packages and download the Go modules. Every step that lints,
|
||||
tests or compiles comes after it. Every `RUN` step after it runs again on each
|
||||
build. Without that, Docker would serve the test and lint steps from its cache
|
||||
on an unchanged tree, and the build would pass having run neither. The layers
|
||||
above the line stay cached until the base image, their own lines, `go.mod` or
|
||||
`go.sum` change. Go's build cache is kept between builds, so only what changed
|
||||
is compiled again. The tests run with `-count=1`, so no test reports a stored
|
||||
result. `golangci-lint run` keeps the linter's own cache as well, so an
|
||||
unchanged package is not linted again.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -1032,7 +1092,7 @@ are shims onto them, except `build`, which carries the compile recipe:
|
||||
- `make setup` prepares a fresh clone: `bootstrap`, then the pre-commit hook.
|
||||
- `make test` runs the test suite under the race detector, in Docker (see
|
||||
`script/test`). It needs `docker`.
|
||||
- `make lint` runs the linter and the gofmt check, in Docker (see
|
||||
- `make lint` runs the linter and the `gofmt` check, in Docker (see
|
||||
`script/lint`). It needs `docker`.
|
||||
- `make fmt` formats the Go sources and the Markdown on the host, and
|
||||
`make fmt-check` checks them without writing. Both need `go` and what
|
||||
@@ -1044,11 +1104,27 @@ are shims onto them, except `build`, which carries the compile recipe:
|
||||
- `make hooks` installs the pre-commit hook.
|
||||
- `make clean` removes the binary.
|
||||
|
||||
### Constraints
|
||||
|
||||
- The language is Go. The module is `sneak.berlin/go/sfdupes`, and the binary is
|
||||
`sfdupes`.
|
||||
- The dependencies beyond the standard library are:
|
||||
- `github.com/spf13/cobra`, for the command line;
|
||||
- `github.com/schollz/progressbar/v3`, for the progress display;
|
||||
- `golang.org/x/term`, to tell whether stderr is a terminal;
|
||||
- `modernc.org/sqlite`, the SQLite driver;
|
||||
- `golang.org/x/sys`, for the scan lock and the list of mounted filesystems.
|
||||
- `modernc.org/sqlite` is pure Go, so the binary builds without cgo. The
|
||||
`Makefile` exports `CGO_ENABLED=0`. The test build is the one exception: the
|
||||
race detector needs cgo (see `script/test`).
|
||||
- `report` and `trees` are deterministic: the same database contents give the
|
||||
same output, whatever order the records were inserted in.
|
||||
|
||||
### Definition of done
|
||||
|
||||
All of the following, run in this directory, must pass:
|
||||
|
||||
1. `make check` passes: the tests, the linter, `gofmt` and prettier.
|
||||
1. `make check` passes: the tests, the linter, `gofmt` and `prettier`.
|
||||
2. `make docker` succeeds.
|
||||
3. The smoke test below behaves as described. It builds a throwaway tree in a
|
||||
temporary directory; never test against real data.
|
||||
@@ -1108,13 +1184,6 @@ All of the following, run in this directory, must pass:
|
||||
|
||||
Tracked in [TODO.md](TODO.md).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No byte-for-byte compare, and no deleting or linking of duplicates. The
|
||||
reports are advisory; acting on them is your job.
|
||||
- No persistence beyond the SQLite database; no export or import formats.
|
||||
- No daemon or filesystem watcher; scheduling rescans is cron's job.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
|
||||
Reference in New Issue
Block a user