Add sfdupes info: show the database's details (closes #117)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
info prints the database's path and source, the sizes of the database file and its -wal file, when the database file last changed, the schema version and the record counts, one per line as a name, a tab and the value. It opens the database as report does and shows an open and a count phase on stderr. The schema version and the counts come from one read-only transaction, the counts from one query. seedDatabase now closes test databases out of WAL mode, as scan does; otherwise info's own connection creates an empty -wal file. The open phase's name and text became constants for the linter, and the scan command moved to newScanCommand to keep newRootCommand short enough. Judgement call: the lines are written after the last display ends, so they skip the display's stdout writer. Model: opus-5-5
This commit was merged in pull request #123.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// infoPhase names one phase of info with its place among all of them,
|
||||
// as "[2/2] count", for the phase's progress display. The list here is
|
||||
// the one place the phases are counted.
|
||||
func infoPhase(name string) string {
|
||||
return phaseLabel([]string{openPhase, "count"}, name)
|
||||
}
|
||||
|
||||
// runInfo implements the info subcommand: it prints the database's
|
||||
// details on stdout (see writeInfo). It opens the database as report
|
||||
// does, read-only and without the scan lock, and never touches the
|
||||
// scanned filesystem. It names the database on stderr before it opens
|
||||
// it. Any database problem, including a missing database, is fatal.
|
||||
//
|
||||
// The work runs in two phases, each under a progress display of its
|
||||
// own on stderr: open (open the database) and count (count the records,
|
||||
// with one query). Each display ends before the next phase starts, and
|
||||
// before an error is returned to be printed. The details are written
|
||||
// once the last display has ended, so no display is drawn while they
|
||||
// are written.
|
||||
func runInfo(ctx context.Context, stdout io.Writer) error {
|
||||
dbPath, source, err := databasePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "info: database %s (%s)\n",
|
||||
escapePath(dbPath), source)
|
||||
|
||||
// Opening can wait on a busy database for as long as the busy
|
||||
// timeout.
|
||||
prog := newProgress(progressParams{
|
||||
label: infoPhase(openPhase), total: -1,
|
||||
waitingFor: openingDatabase,
|
||||
})
|
||||
db, err := openReportDatabase(ctx, dbPath)
|
||||
|
||||
prog.finish()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
// One query counts everything, so the phase never has a total.
|
||||
prog = newProgress(progressParams{
|
||||
label: infoPhase("count"), total: -1,
|
||||
waitingFor: countingRecords,
|
||||
})
|
||||
counts, err := readDatabaseCounts(ctx, db)
|
||||
|
||||
prog.finish()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("database %s: %w", dbPath, err)
|
||||
}
|
||||
|
||||
return writeInfo(stdout, dbPath, source, counts)
|
||||
}
|
||||
|
||||
// writeInfo writes the details of the database at dbPath to stdout,
|
||||
// one per line, as a name, a tab and the value (README "`info` mode"):
|
||||
// the path and source as databasePath returns them, the sizes of the
|
||||
// database file and of its -wal file when there is one, when the
|
||||
// database file was last changed, and the schema version and record
|
||||
// counts that readDatabaseCounts read.
|
||||
func writeInfo(stdout io.Writer, dbPath, source string,
|
||||
counts databaseCounts,
|
||||
) error {
|
||||
// SQLite keeps the -wal file beside the database file, named from
|
||||
// its path with symlinks resolved.
|
||||
file, err := filepath.EvalSymlinks(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database: %w", err)
|
||||
}
|
||||
|
||||
dbFile, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database: %w", err)
|
||||
}
|
||||
|
||||
walFile, err := os.Stat(file + "-wal")
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("database: %w", err)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "database\t%s (%s)\n", escapePath(dbPath), source)
|
||||
fmt.Fprintf(&b, "database file size\t%d (%s)\n",
|
||||
dbFile.Size(), humanBytes(dbFile.Size()))
|
||||
|
||||
if walFile != nil {
|
||||
fmt.Fprintf(&b, "-wal file size\t%d (%s)\n",
|
||||
walFile.Size(), humanBytes(walFile.Size()))
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "modified\t%s\n",
|
||||
dbFile.ModTime().UTC().Format(time.RFC3339))
|
||||
fmt.Fprintf(&b, "schema version\t%d\n", counts.schemaVersion)
|
||||
fmt.Fprintf(&b, "records\t%d\n", counts.records)
|
||||
fmt.Fprintf(&b, "records with head and tail hashes\t%d\n",
|
||||
counts.headTail)
|
||||
fmt.Fprintf(&b, "records with content hash\t%d\n", counts.content)
|
||||
fmt.Fprintf(&b, "size of recorded files\t%d (%s)\n",
|
||||
counts.size, humanBytes(counts.size))
|
||||
|
||||
_, err = io.WriteString(stdout, b.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("write stdout: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user