package cli import ( "fmt" "path/filepath" "sync" "time" "github.com/dustin/go-humanize" "github.com/spf13/afero" "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{ IncludeDotfiles: ctx.Bool("IncludeDotfiles"), 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 var enumWg sync.WaitGroup if showProgress { enumProgress = make(chan scanner.EnumerateStatus, 1) enumWg.Add(1) go func() { defer enumWg.Done() for status := range enumProgress { log.Progressf("Enumerating: %d files, %s", status.FilesFound, humanize.IBytes(uint64(status.BytesFound))) } 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 } } enumWg.Wait() log.Infof("enumerated %d files, %s total", s.FileCount(), humanize.IBytes(uint64(s.TotalBytes()))) // Check if output file exists outputPath := ctx.String("output") if exists, _ := afero.Exists(mfa.Fs, outputPath); exists { if !ctx.Bool("force") { return fmt.Errorf("output file %s already exists (use --force to overwrite)", 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 temp file: %w", err) } // 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 var scanWg sync.WaitGroup if showProgress { scanProgress = make(chan scanner.ScanStatus, 1) scanWg.Add(1) go func() { defer scanWg.Done() for status := range scanProgress { if status.ETA > 0 { log.Progressf("Scanning: %d/%d files, %s/s, ETA %s", status.ScannedFiles, status.TotalFiles, humanize.IBytes(uint64(status.BytesPerSec)), status.ETA.Round(time.Second)) } else { log.Progressf("Scanning: %d/%d files, %s/s", status.ScannedFiles, status.TotalFiles, humanize.IBytes(uint64(status.BytesPerSec))) } } log.ProgressDone() }() } err = s.ToManifest(ctx.Context, outFile, scanProgress) scanWg.Wait() if err != nil { 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 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 }