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 }