Major changes: - Refactor CLI to accept injected I/O streams and filesystem (afero.Fs) for testing without touching the real filesystem - Add RunOptions struct and RunWithOptions() for configurable CLI execution - Add internal/scanner package with two-phase manifest generation: - Phase 1 (Enumeration): walk directories, collect metadata - Phase 2 (Scan): read contents, compute hashes, write manifest - Add internal/checker package for manifest verification with progress reporting and channel-based result streaming - Add mfer/builder.go for incremental manifest construction - Add --no-extra-files flag to check command to detect files not in manifest - Add timing summaries showing file count, size, elapsed time, and throughput - Add comprehensive tests using afero.MemMapFs (no real filesystem access) - Add contrib/usage.sh integration test script - Fix banner ASCII art alignment (consistent spacing) - Fix verbosity levels so summaries display at default log level - Update internal/log to support configurable output writers
101 lines
2.5 KiB
Go
101 lines
2.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
"sneak.berlin/go/mfer/internal/log"
|
|
"sneak.berlin/go/mfer/internal/scanner"
|
|
)
|
|
|
|
func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
|
log.Debug("generateManifestOperation()")
|
|
|
|
opts := &scanner.Options{
|
|
IgnoreDotfiles: ctx.Bool("IgnoreDotfiles"),
|
|
FollowSymLinks: ctx.Bool("FollowSymLinks"),
|
|
Fs: mfa.Fs,
|
|
}
|
|
|
|
s := scanner.NewWithOptions(opts)
|
|
|
|
// Phase 1: Enumeration - collect paths and stat files
|
|
args := ctx.Args()
|
|
showProgress := ctx.Bool("progress")
|
|
|
|
// Set up enumeration progress reporting
|
|
var enumProgress chan scanner.EnumerateStatus
|
|
if showProgress {
|
|
enumProgress = make(chan scanner.EnumerateStatus, 1)
|
|
go func() {
|
|
for status := range enumProgress {
|
|
log.Progressf("Enumerating: %d files, %.1f MB",
|
|
status.FilesFound,
|
|
float64(status.BytesFound)/1e6)
|
|
}
|
|
log.ProgressDone()
|
|
}()
|
|
}
|
|
|
|
if args.Len() == 0 {
|
|
// Default to current directory
|
|
if err := s.EnumeratePath(".", enumProgress); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Collect all paths first
|
|
paths := make([]string, 0, args.Len())
|
|
for i := 0; i < args.Len(); i++ {
|
|
ap, err := filepath.Abs(args.Get(i))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Debugf("enumerating path: %s", ap)
|
|
paths = append(paths, ap)
|
|
}
|
|
if err := s.EnumeratePaths(enumProgress, paths...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.Debugf("enumerated %d files, %d bytes total", s.FileCount(), s.TotalBytes())
|
|
|
|
// Open output file
|
|
outputPath := ctx.String("output")
|
|
outFile, err := mfa.Fs.Create(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Phase 2: Scan - read file contents and generate manifest
|
|
var scanProgress chan scanner.ScanStatus
|
|
if showProgress {
|
|
scanProgress = make(chan scanner.ScanStatus, 1)
|
|
go func() {
|
|
for status := range scanProgress {
|
|
log.Progressf("Scanning: %d/%d files, %.1f MB/s",
|
|
status.ScannedFiles,
|
|
status.TotalFiles,
|
|
status.BytesPerSec/1e6)
|
|
}
|
|
log.ProgressDone()
|
|
}()
|
|
}
|
|
|
|
err = s.ToManifest(ctx.Context, outFile, scanProgress)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate manifest: %w", err)
|
|
}
|
|
|
|
if !ctx.Bool("quiet") {
|
|
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)
|
|
}
|
|
|
|
return nil
|
|
}
|