`next` changed the old README after this branch was cut, in #146 (the `totals` table, commit98193b2) and #147 (`disk_size`, commit4ed2e37). This commit states the same facts in the rewritten README, in the places #140 (comment) lists, and changes nothing else. Every statement was checked against the code on `next`. Model: fable-5-1
This commit is contained in:
@@ -405,6 +405,11 @@ operands:
|
|||||||
- A file already in the database is not read again when its size and mtime match
|
- A file already in the database is not read again when its size and mtime match
|
||||||
its record. The mtime matches when it is not newer than the recorded one. This
|
its record. The mtime matches when it is not newer than the recorded one. This
|
||||||
is what makes a daily rescan cheap.
|
is what makes a daily rescan cheap.
|
||||||
|
- When such a file's size on disk differs from its recorded `disk_size`, `scan`
|
||||||
|
updates `disk_size` alone. It still does not read the file, and counts it as
|
||||||
|
unchanged. ZFS, for example, counts a new file's blocks only seconds after the
|
||||||
|
write. A block is the unit in which a filesystem allocates disk space. A file
|
||||||
|
scanned within those seconds gets its real `disk_size` at the next scan.
|
||||||
- `scan` hashes a file again when its size differs or its mtime is newer. The
|
- `scan` hashes a file again when its size differs or its mtime is newer. The
|
||||||
mtime is compared to the nanosecond, as finely as the filesystem records it. A
|
mtime 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
|
same-size rewrite counts as a change whenever the filesystem gives it a later
|
||||||
@@ -435,15 +440,17 @@ operands:
|
|||||||
phases.
|
phases.
|
||||||
|
|
||||||
1. `load` reads the existing records. Each record under the operands is kept in
|
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.
|
memory by path, with its size, its size on disk and its mtime, for the walk
|
||||||
|
to compare against.
|
||||||
2. `walk` lists the trees under all operands. Several workers share one queue of
|
2. `walk` lists the trees under all operands. Several workers share one queue of
|
||||||
directories to read. A worker is one thread of the program. A thread is one
|
directories to read. A worker is one thread of the program. A thread is one
|
||||||
of several parts of a program that run at the same time and share its memory.
|
of several parts of a program that run at the same time and share its memory.
|
||||||
"Concurrency" says how many workers there are. Each worker reads one
|
"Concurrency" says how many workers there are. Each worker reads one
|
||||||
directory at a time. It hands the subdirectories it finds back to the queue,
|
directory at a time. It hands the subdirectories it finds back to the queue,
|
||||||
and it stats each regular file as it finds it. The walk compares each file
|
and it 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
|
with its record. An unchanged file is not read again. When its size on disk
|
||||||
file is passed to the hash phase.
|
has changed, the update phase writes its new `disk_size`. 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
|
3. `hash` reads every new or changed file and computes its three hashes. Files
|
||||||
are read in inode order. An inode is the filesystem's own record of a file.
|
are read in inode order. An inode is the filesystem's own record of a file.
|
||||||
Files with nearby inode numbers usually lie near each other on disk. Reading
|
Files with nearby inode numbers usually lie near each other on disk. Reading
|
||||||
@@ -452,8 +459,9 @@ phases.
|
|||||||
tree made of hard links therefore costs one read per file. Completed records
|
tree made of hard links therefore costs one read per file. Completed records
|
||||||
are written to the database 10,000 at a time while hashing runs. So a scan
|
are written to the database 10,000 at a time while hashing runs. So a scan
|
||||||
interrupted after hours keeps everything hashed so far.
|
interrupted after hours keeps everything hashed so far.
|
||||||
4. `update` writes the hashed records not yet written, then deletes the records
|
4. `update` writes the hashed records not yet written, and the new `disk_size`
|
||||||
the scan did not verify.
|
of each unchanged file whose size on disk changed. Then it 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
|
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
|
hours at millions of files. Reading many directories at once is what makes the
|
||||||
@@ -656,14 +664,14 @@ Processing:
|
|||||||
the same for the same database contents.
|
the same for the same database contents.
|
||||||
|
|
||||||
`report` has two phases, each with its own progress display on stderr (see
|
`report` has two phases, each with its own progress display on stderr (see
|
||||||
"Progress"). `open` opens the database. `read` counts the records, then reads
|
"Progress"). `open` opens the database. `read` gets the number of records from
|
||||||
them in signature order, largest size first. The database keeps an index named
|
the `totals` table (see "Schema"), then reads them in signature order, largest
|
||||||
`files_signature`, a sorted list of the records by signature (see "Schema").
|
size first. The database keeps an index named `files_signature`, a sorted list
|
||||||
Reading it backwards gives that order, so SQLite sorts nothing and returns the
|
of the records by signature (see "Schema"). Reading it backwards gives that
|
||||||
first record at once. Records with one signature come together, and so do the
|
order, so SQLite sorts nothing and returns the first record at once. Records
|
||||||
records of one size. So `report` holds only the duplicate groups of the size it
|
with one signature come together, and so do the records of one size. So `report`
|
||||||
is reading. Once it reaches a smaller size, it writes them, ordered by `first`
|
holds only the duplicate groups of the size it is reading. Once it reaches a
|
||||||
path.
|
smaller size, it writes them, ordered by `first` path.
|
||||||
|
|
||||||
#### Report output format
|
#### Report output format
|
||||||
|
|
||||||
@@ -746,22 +754,35 @@ Two limitations are accepted:
|
|||||||
two trees differ.
|
two trees differ.
|
||||||
|
|
||||||
`trees` has four phases, each with its own progress display on stderr (see
|
`trees` has four phases, each with its own progress display on stderr (see
|
||||||
"Progress"). `open` opens the database. `read` counts the records, then reads
|
"Progress"). `open` opens the database. `read` gets the number of records from
|
||||||
them in path order. It builds the directory tree and computes the digests as it
|
the `totals` table, then reads them in path order. It builds the directory tree
|
||||||
goes. `group` groups the directories by digest and picks the groups to report.
|
and computes the digests as it goes. `group` groups the directories by digest
|
||||||
`write` writes them to stdout.
|
and picks the groups to report. `write` writes them to stdout.
|
||||||
|
|
||||||
#### Trees output format
|
#### Trees output format
|
||||||
|
|
||||||
The report is TSV on stdout, with a header line and then one row per duplicate
|
The report is TSV on stdout, with a header line and then one row per duplicate
|
||||||
tree. A group of N trees gives N-1 rows. `files` is the number of regular files
|
tree. A group of N trees gives N-1 rows. After the `first` and `dupe` paths come
|
||||||
in one copy of the tree, and `size` their total size in bytes:
|
three numbers:
|
||||||
|
|
||||||
|
- `files`, the number of regular files in one copy of the tree;
|
||||||
|
- `size`, their total size in bytes;
|
||||||
|
- `disk_size`, the bytes the `dupe` copy takes on disk, its files' `disk_size`
|
||||||
|
added up.
|
||||||
|
|
||||||
```
|
```
|
||||||
first dupe files size
|
first dupe files size disk_size
|
||||||
/srv/a/project /srv/backup/project 3417 104857600
|
/srv/a/project /srv/backup/project 3417 104857600 61865984
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A file's `disk_size` is the space its blocks take on disk. It can be more than
|
||||||
|
the file's size, because space is allocated in whole blocks. It is less than the
|
||||||
|
file's size on a compressed or sparse file. A sparse file has gaps that take no
|
||||||
|
space on disk. So two copies of one tree can differ in `disk_size`. When a file
|
||||||
|
in one copy is a hard link to one in the other, both copies count its blocks in
|
||||||
|
full. Removing it from one copy then frees nothing, as the limitation under
|
||||||
|
"`trees` mode" says.
|
||||||
|
|
||||||
The paths are escaped as described under "Report output format". The root
|
The paths are escaped as described under "Report output format". The root
|
||||||
directory's path is `/`.
|
directory's path is `/`.
|
||||||
|
|
||||||
@@ -808,11 +829,11 @@ size of recorded files 150000000000000 (136.4 TiB)
|
|||||||
of all recorded files, in bytes, then rounded to a unit such as TiB.
|
of all recorded files, in bytes, then rounded to a unit such as TiB.
|
||||||
|
|
||||||
`info` has two phases, each with its own progress display on stderr (see
|
`info` has two phases, each with its own progress display on stderr (see
|
||||||
"Progress"). `open` opens the database. `count` reads the schema version and
|
"Progress"). `open` opens the database. `count` reads the schema version and the
|
||||||
counts the records in one transaction. A transaction is a group of reads or
|
row of `totals` (see "Schema") in one transaction. A transaction is a group of
|
||||||
writes that SQLite carries out as one step, against the database as it stands at
|
reads or writes that SQLite carries out as one step, against the database as it
|
||||||
one moment. So both describe the database at one moment, even while a scan is
|
stands at one moment. So both describe the database at one moment, even while a
|
||||||
writing. `info` writes its lines after the `count` display has ended.
|
scan is writing. `info` writes its lines after the `count` display has ended.
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
@@ -908,33 +929,36 @@ Only one `scan` at a time can hold the lock.
|
|||||||
- Then, before the walk as well, it writes:
|
- Then, before the walk as well, it writes:
|
||||||
- the `-wal` and `-shm` files, which stay beside the database while the scan
|
- the `-wal` and `-shm` files, which stay beside the database while the scan
|
||||||
runs;
|
runs;
|
||||||
- in a new database, the table, the index and the schema version described
|
- in a new database, the tables, the index, the triggers and the schema
|
||||||
under "Schema".
|
version described under "Schema".
|
||||||
- During the `load` and `walk` phases, no records are written. A first scan's
|
- During the `load` and `walk` phases, no records are written. A first scan's
|
||||||
database holds no records until the hash phase, however long its walk runs.
|
database holds no records until the hash phase, however long its walk runs.
|
||||||
- During the `hash` phase, records are written 10,000 at a time.
|
- During the `hash` phase, records are written 10,000 at a time.
|
||||||
- During the `update` phase, the rest of the hashed records are written, and the
|
- During the `update` phase, the rest of the hashed records are written, and so
|
||||||
deletions are made.
|
is the new `disk_size` of each unchanged file whose size on disk changed. Then
|
||||||
|
the deletions are made.
|
||||||
- As `scan` closes the database, the `-wal` and `-shm` files go away, unless a
|
- As `scan` closes the database, the `-wal` and `-shm` files go away, unless a
|
||||||
report still has the database open.
|
report still has the database open.
|
||||||
|
|
||||||
#### Schema
|
#### Schema
|
||||||
|
|
||||||
The schema is the database's layout, its table and its index.
|
The schema is the database's layout: its tables, its index and its triggers. A
|
||||||
|
trigger is a statement SQLite runs by itself when a table changes.
|
||||||
`PRAGMA user_version` holds the schema version, currently 1. An empty database
|
`PRAGMA user_version` holds the schema version, currently 1. An empty database
|
||||||
is at version 0, and `scan` sets it up. Any other version is a fatal error.
|
is at version 0, and `scan` sets it up. Any other version is a fatal error.
|
||||||
`report`, `trees` and `info` set nothing up, so for them version 0 is a fatal
|
`report`, `trees` and `info` set nothing up, so for them version 0 is a fatal
|
||||||
error too. `scan` creates the table and the index and sets the version in one
|
error too. `scan` creates the tables, the index and the triggers and sets the
|
||||||
transaction (see "`info` mode"). So the three land together or not at all. A
|
version in one transaction (see "`info` mode"). So they all land together or not
|
||||||
first scan stopped while doing so leaves an empty database, which the next scan
|
at all. A first scan stopped while doing so leaves an empty database, which the
|
||||||
sets up. A database at version 0 that already has a `files` table was not made
|
next scan sets up. A database at version 0 that already has a `files` table was
|
||||||
by `sfdupes`. Every subcommand refuses it, with an error telling you to remove
|
not made by `sfdupes`. Every subcommand refuses it, with an error telling you to
|
||||||
the file and rescan.
|
remove the file and rescan.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE files (
|
CREATE TABLE files (
|
||||||
path BLOB PRIMARY KEY, -- absolute path, raw bytes
|
path BLOB PRIMARY KEY, -- absolute path, raw bytes
|
||||||
size INTEGER NOT NULL, -- bytes
|
size INTEGER NOT NULL, -- bytes
|
||||||
|
disk_size INTEGER NOT NULL, -- bytes allocated on disk (st_blocks × 512)
|
||||||
mtime INTEGER NOT NULL, -- whole Unix seconds of the mtime
|
mtime INTEGER NOT NULL, -- whole Unix seconds of the mtime
|
||||||
mtime_nsec INTEGER NOT NULL, -- nanoseconds within that second
|
mtime_nsec INTEGER NOT NULL, -- nanoseconds within that second
|
||||||
head BLOB NOT NULL, -- raw 32-byte SHA-256 of the first 64 KiB
|
head BLOB NOT NULL, -- raw 32-byte SHA-256 of the first 64 KiB
|
||||||
@@ -942,10 +966,28 @@ CREATE TABLE files (
|
|||||||
content BLOB NOT NULL -- raw 32-byte SHA-256; whole file under 100 MiB, else samples
|
content BLOB NOT NULL -- raw 32-byte SHA-256; whole file under 100 MiB, else samples
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
CREATE INDEX files_signature ON files (size, head, tail, content);
|
CREATE INDEX files_signature ON files (size, head, tail, content);
|
||||||
|
CREATE TABLE totals (
|
||||||
|
records INTEGER NOT NULL, -- number of records in files
|
||||||
|
size INTEGER NOT NULL -- sizes of all records in files added up
|
||||||
|
);
|
||||||
|
INSERT INTO totals (records, size) VALUES (0, 0);
|
||||||
|
CREATE TRIGGER totals_after_insert AFTER INSERT ON files BEGIN
|
||||||
|
UPDATE totals SET records = records + 1, size = size + NEW.size;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER totals_after_delete AFTER DELETE ON files BEGIN
|
||||||
|
UPDATE totals SET records = records - 1, size = size - OLD.size;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER totals_after_update AFTER UPDATE OF size ON files BEGIN
|
||||||
|
UPDATE totals SET size = size + NEW.size - OLD.size;
|
||||||
|
END;
|
||||||
```
|
```
|
||||||
|
|
||||||
- `path` is a BLOB, a column of raw bytes, because Unix paths are bytes and not
|
- `path` is a BLOB, a column of raw bytes, because Unix paths are bytes and not
|
||||||
necessarily UTF-8 text.
|
necessarily UTF-8 text.
|
||||||
|
- `disk_size` is the space the file's blocks take on disk. `lstat` is the system
|
||||||
|
call that stats a file without following a symlink. Its `st_blocks` field
|
||||||
|
counts that space in units of 512 bytes. `disk_size` is less than `size` on a
|
||||||
|
compressed or sparse file (see "Trees output format").
|
||||||
- `mtime` holds the whole seconds of the file's mtime, rounded down, and
|
- `mtime` holds the whole seconds of the file's mtime, rounded down, and
|
||||||
`mtime_nsec` the nanoseconds past that second. Split this way, they hold any
|
`mtime_nsec` the nanoseconds past that second. Split this way, they hold any
|
||||||
mtime a filesystem can record, even one before 1678 or after 2262. One 64-bit
|
mtime a filesystem can record, even one before 1678 or after 2262. One 64-bit
|
||||||
@@ -956,6 +998,16 @@ CREATE INDEX files_signature ON files (size, head, tail, content);
|
|||||||
not 32 bytes.
|
not 32 bytes.
|
||||||
- `files_signature` lets `report` read the records in signature order without
|
- `files_signature` lets `report` read the records in signature order without
|
||||||
sorting the table.
|
sorting the table.
|
||||||
|
- `totals` holds one row: the number of records in `files` and the sum of their
|
||||||
|
sizes. Three triggers on `files` keep that row current. Each runs in the
|
||||||
|
transaction of the change to `files` that fires it, so the row and the records
|
||||||
|
always agree. `info` reads both numbers from this row. The `load` phase of
|
||||||
|
`scan` and the `read` phase of `report` and `trees` read the number of records
|
||||||
|
from it as their total. Counting the records in `files` would read every
|
||||||
|
record. That takes minutes on a database of tens of millions. The triggers
|
||||||
|
make each insert, each delete and each change of a record's `size` slower, so
|
||||||
|
a first scan of many small files takes about two fifths longer. On large
|
||||||
|
files, reading them takes most of the time, so the difference is small.
|
||||||
|
|
||||||
### Progress
|
### Progress
|
||||||
|
|
||||||
@@ -966,8 +1018,9 @@ of its line are listed below.
|
|||||||
The phases, and what each display counts:
|
The phases, and what each display counts:
|
||||||
|
|
||||||
- `scan` has four phases:
|
- `scan` has four phases:
|
||||||
- `[1/4] load` counts the records first, then reads them against that count.
|
- `[1/4] load` gets the number of records from the `totals` table first,
|
||||||
While it counts, the display says `counting the records`.
|
then reads them against that number. Until it has the number, the display
|
||||||
|
says `counting the records`.
|
||||||
- `[2/4] walk` has no exact total. Its estimate is the number of records
|
- `[2/4] walk` has no exact total. Its estimate is the number of records
|
||||||
under the operands, shown with a `~`. A scan with no records under its
|
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
|
operands, such as a first scan, has no estimate. The time remaining is
|
||||||
@@ -979,8 +1032,9 @@ The phases, and what each display counts:
|
|||||||
- `report` has two phases:
|
- `report` has two phases:
|
||||||
- `[1/2] open` has no total. The display says `opening the database`, also
|
- `[1/2] open` has no total. The display says `opening the database`, also
|
||||||
while it waits on a busy one.
|
while it waits on a busy one.
|
||||||
- `[2/2] read` counts the records first, then reads them against that count.
|
- `[2/2] read` gets the number of records from the `totals` table first,
|
||||||
It writes rows to stdout as it goes.
|
then reads them against that number. Until it has the number, the display
|
||||||
|
says `counting the records`. It writes rows to stdout as it goes.
|
||||||
- `trees` has four phases:
|
- `trees` has four phases:
|
||||||
- `[1/4] open` and `[2/4] read` are as for `report`, except that `read`
|
- `[1/4] open` and `[2/4] read` are as for `report`, except that `read`
|
||||||
writes nothing.
|
writes nothing.
|
||||||
@@ -988,9 +1042,8 @@ The phases, and what each display counts:
|
|||||||
- `[4/4] write` has the duplicate-tree groups as its total.
|
- `[4/4] write` has the duplicate-tree groups as its total.
|
||||||
- `info` has two phases:
|
- `info` has two phases:
|
||||||
- `[1/2] open` is as for `report`.
|
- `[1/2] open` is as for `report`.
|
||||||
- `[2/2] count` has no total either, because one request to the database
|
- `[2/2] count` has no total either, because it reads one row. The display
|
||||||
counts everything. The display says `counting the records` until that
|
says `counting the records` until it has the row.
|
||||||
request returns.
|
|
||||||
|
|
||||||
Each display is one line holding:
|
Each display is one line holding:
|
||||||
|
|
||||||
@@ -1323,9 +1376,10 @@ All of the following, run in this directory, must pass:
|
|||||||
- The groups come in order of size, largest first.
|
- The groups come in order of size, largest first.
|
||||||
|
|
||||||
`trees` prints exactly one row, `$d/t1` as `first` and `$d/t2` as `dupe`.
|
`trees` prints exactly one row, `$d/t1` as `first` and `$d/t2` as `dupe`.
|
||||||
The row shows 2 files and 3100 bytes. The two `sub` directories are left
|
The row shows 2 files, 3100 bytes, and a `disk_size` that depends on the
|
||||||
out, because the `t1` and `t2` group implies them. `t3` appears nowhere,
|
filesystem. The two `sub` directories are left out, because the `t1` and
|
||||||
since one of its file names differs.
|
`t2` group implies them. `t3` appears nowhere, since one of its file names
|
||||||
|
differs.
|
||||||
|
|
||||||
After the rescan, the second `report` shows `one.bin` out of its group,
|
After the rescan, the second `report` shows `one.bin` out of its group,
|
||||||
because its content changed. `copy.bin` and `copy2.bin` remain as one pair.
|
because its content changed. `copy.bin` and `copy2.bin` remain as one pair.
|
||||||
|
|||||||
Reference in New Issue
Block a user