Both phases now share a small, dependency-free progress indicator: an in-place bar on a TTY (e.g. `docker compose run`), and a line every 10% when stdout is piped (cron/CI) so logs don't fill with carriage returns. Also fixes the pairs phase, which mislabelled its progress as "hashed" — it now reads "matching". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -85,12 +85,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
if doPairs {
|
if doPairs {
|
||||||
fmt.Printf("rebuilding duplicate pairs (threshold %d)...\n", cfg.DuplicateHashThreshold)
|
fmt.Printf("rebuilding duplicate pairs (threshold %d)...\n", cfg.DuplicateHashThreshold)
|
||||||
|
// total is only known once Rescan has loaded the hashes, so create the bar
|
||||||
|
// lazily on the first progress callback.
|
||||||
|
var prog *progress
|
||||||
if err := dupSvc.Rescan(ctx, func(done, total int) {
|
if err := dupSvc.Rescan(ctx, func(done, total int) {
|
||||||
fmt.Printf("\r hashed %d/%d", done, total)
|
if prog == nil {
|
||||||
|
prog = newProgress("matching", total)
|
||||||
|
}
|
||||||
|
prog.set(done)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fatal("rescan pairs", err)
|
fatal("rescan pairs", err)
|
||||||
}
|
}
|
||||||
fmt.Println("\n done")
|
if prog != nil {
|
||||||
|
prog.finish()
|
||||||
|
}
|
||||||
|
fmt.Println(" done")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +115,7 @@ func backfillHashes(ctx context.Context, files *postgres.FileRepo, store *storag
|
|||||||
fmt.Printf("hashing %d files without a perceptual hash...\n", total)
|
fmt.Printf("hashing %d files without a perceptual hash...\n", total)
|
||||||
|
|
||||||
var hashed, skipped, failed int
|
var hashed, skipped, failed int
|
||||||
|
prog := newProgress("hashing", total)
|
||||||
for i, f := range pending {
|
for i, f := range pending {
|
||||||
ph, err := hashOne(ctx, store, f.ID, f.MIMEType)
|
ph, err := hashOne(ctx, store, f.ID, f.MIMEType)
|
||||||
switch {
|
switch {
|
||||||
@@ -120,11 +130,10 @@ func backfillHashes(ctx context.Context, files *postgres.FileRepo, store *storag
|
|||||||
}
|
}
|
||||||
hashed++
|
hashed++
|
||||||
}
|
}
|
||||||
if (i+1)%200 == 0 || i+1 == total {
|
prog.set(i + 1)
|
||||||
fmt.Printf("\r processed %d/%d", i+1, total)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fmt.Printf("\n hashed %d, skipped %d, failed %d\n", hashed, skipped, failed)
|
prog.finish()
|
||||||
|
fmt.Printf(" hashed %d, skipped %d, failed %d\n", hashed, skipped, failed)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +168,49 @@ func hashOne(ctx context.Context, store *storage.DiskStorage, id uuid.UUID, mime
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// progress renders a dependency-free progress indicator. On a TTY it draws an
|
||||||
|
// in-place bar; otherwise (pipe, cron, CI) it prints a line every 10% so logs
|
||||||
|
// stay readable instead of filling with carriage returns.
|
||||||
|
type progress struct {
|
||||||
|
label string
|
||||||
|
tty bool
|
||||||
|
total int
|
||||||
|
lastDec int // last 10%-decile printed in non-TTY mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgress(label string, total int) *progress {
|
||||||
|
fi, _ := os.Stdout.Stat()
|
||||||
|
tty := fi != nil && fi.Mode()&os.ModeCharDevice != 0
|
||||||
|
return &progress{label: label, tty: tty, total: total, lastDec: -1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progress) set(done int) {
|
||||||
|
if p.total <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pct := done * 100 / p.total
|
||||||
|
if p.tty {
|
||||||
|
const w = 30
|
||||||
|
filled := done * w / p.total
|
||||||
|
fmt.Printf("\r %s [%s%s] %3d%% (%d/%d)",
|
||||||
|
p.label,
|
||||||
|
strings.Repeat("#", filled), strings.Repeat("-", w-filled),
|
||||||
|
pct, done, p.total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dec := pct / 10; dec != p.lastDec {
|
||||||
|
p.lastDec = dec
|
||||||
|
fmt.Printf(" %s %d%% (%d/%d)\n", p.label, pct, done, p.total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish ends the in-place bar with a newline (TTY only).
|
||||||
|
func (p *progress) finish() {
|
||||||
|
if p.tty && p.total > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fatal(what string, err error) {
|
func fatal(what string, err error) {
|
||||||
fmt.Fprintf(os.Stderr, "dedup: %s: %v\n", what, err)
|
fmt.Fprintf(os.Stderr, "dedup: %s: %v\n", what, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user