All checks were successful
check / check (push) Successful in 3s
scan now synchronizes a database that survives between runs (SFDUPES_DATABASE, default /var/lib/sfdupes/db.sqlite) instead of emitting a stream: operands are resolved to absolute paths, unchanged files (same size, mtime not newer than recorded) are never re-read, new and changed files are hashed, and records under the scanned operands that were not verified this run are deleted; records outside the operands are untouched. All changes commit in a single transaction, and WAL journaling with a busy timeout keeps a report run during a cron scan safe. report and trees read the database (no positional arguments); the NUL-terminated stream format, its parser, and the malformed-record handling are gone. The driver is modernc.org/sqlite (pure Go), so builds keep cgo disabled.
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
// Command sfdupes quickly identifies candidate duplicate files across
|
|
// very large filesystems without reading full file contents. Files are
|
|
// considered duplicates when they have identical size, identical SHA-256
|
|
// of their first 1024 bytes, and identical SHA-256 of their last 1024
|
|
// bytes. scan maintains a persistent SQLite database of file signatures
|
|
// (SFDUPES_DATABASE, default /var/lib/sfdupes/db.sqlite) that the
|
|
// reporting subcommands read.
|
|
//
|
|
// Usage:
|
|
//
|
|
// sfdupes scan [--workers N] [-x] PATH...
|
|
// sfdupes report > dupes.tsv
|
|
// sfdupes trees > dupetrees.tsv
|
|
//
|
|
// See README.md for the complete specification.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Exit codes: 0 is success (even with per-file warnings), exitFatal is
|
|
// a fatal error, exitUsage is a usage error.
|
|
const (
|
|
exitFatal = 1
|
|
exitUsage = 2
|
|
)
|
|
|
|
// Version is the build version, injected at link time via -ldflags
|
|
// (see the Makefile); "dev" for a plain go build.
|
|
//
|
|
//nolint:gochecknoglobals // written only by the linker
|
|
var Version = "dev"
|
|
|
|
func main() {
|
|
root := &cobra.Command{
|
|
Use: "sfdupes",
|
|
Short: "Find candidate duplicate files by size and head/tail SHA-256",
|
|
Version: Version,
|
|
Args: cobra.NoArgs,
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
// A missing subcommand prints usage and exits 2.
|
|
_ = cmd.Usage()
|
|
|
|
os.Exit(exitUsage)
|
|
},
|
|
}
|
|
// Everything on stdout is machine-readable data; all human-facing
|
|
// output (help, usage, errors) goes to stderr.
|
|
root.SetOut(os.Stderr)
|
|
root.SetErr(os.Stderr)
|
|
root.CompletionOptions.DisableDefaultCmd = true
|
|
|
|
var (
|
|
scanWorkers int
|
|
scanOneFS bool
|
|
)
|
|
|
|
scanCmd := &cobra.Command{
|
|
Use: "scan [--workers N] [-x] PATH...",
|
|
Short: "Walk trees and synchronize the scan database",
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
runScan(args, scanWorkers, scanOneFS)
|
|
},
|
|
}
|
|
scanCmd.Flags().IntVar(&scanWorkers, "workers", runtime.NumCPU(),
|
|
"concurrent workers for the stat and hash passes")
|
|
scanCmd.Flags().BoolVarP(&scanOneFS, "one-file-system", "x", false,
|
|
"do not cross filesystem boundaries")
|
|
|
|
reportCmd := &cobra.Command{
|
|
Use: "report",
|
|
Short: "Read the scan database and print the file-level duplicates report",
|
|
Args: cobra.NoArgs,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
runReport()
|
|
},
|
|
}
|
|
|
|
treesCmd := &cobra.Command{
|
|
Use: "trees",
|
|
Short: "Read the scan database and print the duplicate-tree report",
|
|
Args: cobra.NoArgs,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
runTrees()
|
|
},
|
|
}
|
|
|
|
root.AddCommand(scanCmd, reportCmd, treesCmd)
|
|
|
|
err := root.Execute()
|
|
if err != nil {
|
|
// Cobra has already printed the error and usage to stderr;
|
|
// an invalid subcommand or bad arguments is a usage error.
|
|
os.Exit(exitUsage)
|
|
}
|
|
}
|
|
|
|
// fatalf reports a fatal error and exits 1.
|
|
func fatalf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "sfdupes: "+format+"\n", args...)
|
|
os.Exit(exitFatal)
|
|
}
|