Add atomic writes, humanized sizes, debug logging, and -v/-q per-command
- Atomic writes for mfer gen: writes to temp file, renames on success, cleans up temp on error/interrupt. Prevents empty manifests on Ctrl-C. - Humanized byte sizes using dustin/go-humanize (e.g., "10 MiB" not "10485760") - Progress lines clear when done (using ANSI escape \r\033[K]) - Debug logging when files are added to manifest (mfer gen -vv) - Move -v/-q flags from global to per-command for better UX - Add tests for atomic write behavior with failing filesystem mock
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/urfave/cli/v2"
|
||||
"sneak.berlin/go/mfer/internal/log"
|
||||
@@ -36,9 +37,9 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
||||
go func() {
|
||||
defer enumWg.Done()
|
||||
for status := range enumProgress {
|
||||
log.Progressf("Enumerating: %d files, %.1f MB",
|
||||
log.Progressf("Enumerating: %d files, %s",
|
||||
status.FilesFound,
|
||||
float64(status.BytesFound)/1e6)
|
||||
humanize.IBytes(uint64(status.BytesFound)))
|
||||
}
|
||||
log.ProgressDone()
|
||||
}()
|
||||
@@ -66,7 +67,7 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
||||
}
|
||||
enumWg.Wait()
|
||||
|
||||
log.Infof("enumerated %d files, %d bytes total", s.FileCount(), s.TotalBytes())
|
||||
log.Infof("enumerated %d files, %s total", s.FileCount(), humanize.IBytes(uint64(s.TotalBytes())))
|
||||
|
||||
// Check if output file exists
|
||||
outputPath := ctx.String("output")
|
||||
@@ -76,12 +77,21 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Open output file
|
||||
outFile, err := mfa.Fs.Create(outputPath)
|
||||
// Create temp file for atomic write
|
||||
tmpPath := outputPath + ".tmp"
|
||||
outFile, err := mfa.Fs.Create(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
// Clean up temp file on any error or interruption
|
||||
success := false
|
||||
defer func() {
|
||||
_ = outFile.Close()
|
||||
if !success {
|
||||
_ = mfa.Fs.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
// Phase 2: Scan - read file contents and generate manifest
|
||||
var scanProgress chan scanner.ScanStatus
|
||||
@@ -93,16 +103,16 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
||||
defer scanWg.Done()
|
||||
for status := range scanProgress {
|
||||
if status.ETA > 0 {
|
||||
log.Progressf("Scanning: %d/%d files, %.1f MB/s, ETA %s",
|
||||
log.Progressf("Scanning: %d/%d files, %s/s, ETA %s",
|
||||
status.ScannedFiles,
|
||||
status.TotalFiles,
|
||||
status.BytesPerSec/1e6,
|
||||
humanize.IBytes(uint64(status.BytesPerSec)),
|
||||
status.ETA.Round(time.Second))
|
||||
} else {
|
||||
log.Progressf("Scanning: %d/%d files, %.1f MB/s",
|
||||
log.Progressf("Scanning: %d/%d files, %s/s",
|
||||
status.ScannedFiles,
|
||||
status.TotalFiles,
|
||||
status.BytesPerSec/1e6)
|
||||
humanize.IBytes(uint64(status.BytesPerSec)))
|
||||
}
|
||||
}
|
||||
log.ProgressDone()
|
||||
@@ -115,9 +125,21 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
||||
return fmt.Errorf("failed to generate manifest: %w", err)
|
||||
}
|
||||
|
||||
// Close file before rename to ensure all data is flushed
|
||||
if err := outFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
if err := mfa.Fs.Rename(tmpPath, outputPath); err != nil {
|
||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||
}
|
||||
|
||||
success = true
|
||||
|
||||
elapsed := time.Since(mfa.startupTime).Seconds()
|
||||
rate := float64(s.TotalBytes()) / elapsed / 1e6
|
||||
log.Infof("wrote %d files (%.1f MB) to %s in %.1fs (%.1f MB/s)", s.FileCount(), float64(s.TotalBytes())/1e6, outputPath, elapsed, rate)
|
||||
rate := float64(s.TotalBytes()) / elapsed
|
||||
log.Infof("wrote %d files (%s) to %s in %.1fs (%s/s)", s.FileCount(), humanize.IBytes(uint64(s.TotalBytes())), outputPath, elapsed, humanize.IBytes(uint64(rate)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user