All checks were successful
check / check (push) Successful in 4s
scan now takes one or more PATH operands (directories or regular files) via cobra flags instead of the -root flag with its /srv default; invoking scan with no operand is a usage error and a nonexistent operand is fatal. Filesystem boundaries are crossed by default; the new -x/--one-file-system flag (GNU du/rsync convention) stops the walk at each operand's filesystem, implemented by comparing lstat device IDs with build-tagged helpers for darwin's int32 Dev. Verified against a real mounted disk image: default crosses, -x does not, --one-file-system is identical to -x.
107 lines
2.9 KiB
Go
107 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.
|
|
//
|
|
// Usage:
|
|
//
|
|
// sfdupes scan [--workers N] [-x] PATH... > files.dat
|
|
// sfdupes report [files.dat|-] > dupes.tsv
|
|
// sfdupes trees [files.dat|-] > 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 emit one record per regular file on stdout",
|
|
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 [files.dat|-]",
|
|
Short: "Read a scan stream and print the file-level duplicates report",
|
|
Args: cobra.MaximumNArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
runReport(args)
|
|
},
|
|
}
|
|
|
|
treesCmd := &cobra.Command{
|
|
Use: "trees [files.dat|-]",
|
|
Short: "Read a scan stream and print the duplicate-tree report",
|
|
Args: cobra.MaximumNArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
runTrees(args)
|
|
},
|
|
}
|
|
|
|
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)
|
|
}
|