Add testable CLI with dependency injection and new scanner/checker packages
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
This commit is contained in:
@@ -1,54 +1,100 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"sneak.berlin/go/mfer/internal/log"
|
||||
"sneak.berlin/go/mfer/mfer"
|
||||
"sneak.berlin/go/mfer/internal/scanner"
|
||||
)
|
||||
|
||||
func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
||||
log.Debug("generateManifestOperation()")
|
||||
myArgs := ctx.Args()
|
||||
log.Dump(myArgs)
|
||||
|
||||
opts := &mfer.ManifestScanOptions{
|
||||
opts := &scanner.Options{
|
||||
IgnoreDotfiles: ctx.Bool("IgnoreDotfiles"),
|
||||
FollowSymLinks: ctx.Bool("FollowSymLinks"),
|
||||
Fs: mfa.Fs,
|
||||
}
|
||||
paths := make([]string, ctx.Args().Len()-1)
|
||||
for i := 0; i < ctx.Args().Len(); i++ {
|
||||
ap, err := filepath.Abs(ctx.Args().Get(i))
|
||||
if err != nil {
|
||||
|
||||
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.Dump(ap)
|
||||
paths = append(paths, ap)
|
||||
}
|
||||
mf, err := mfer.NewFromPaths(opts, paths...)
|
||||
|
||||
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 {
|
||||
panic(err)
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
mf.WithContext(ctx.Context)
|
||||
defer outFile.Close()
|
||||
|
||||
log.Dump(mf)
|
||||
// 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 = mf.Scan()
|
||||
err = s.ToManifest(ctx.Context, outFile, scanProgress)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to generate manifest: %w", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err = mf.WriteTo(buf)
|
||||
if err != nil {
|
||||
return 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)
|
||||
}
|
||||
|
||||
dat := buf.Bytes()
|
||||
|
||||
log.Dump(dat)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user