package cli import ( "fmt" "path/filepath" "time" "github.com/spf13/afero" "github.com/urfave/cli/v2" "sneak.berlin/go/mfer/internal/checker" "sneak.berlin/go/mfer/internal/log" ) // findManifest looks for a manifest file in the given directory. // It checks for index.mf and .index.mf, returning the first one found. func findManifest(fs afero.Fs, dir string) (string, error) { candidates := []string{"index.mf", ".index.mf"} for _, name := range candidates { path := filepath.Join(dir, name) exists, err := afero.Exists(fs, path) if err != nil { return "", err } if exists { return path, nil } } return "", fmt.Errorf("no manifest found in %s (looked for index.mf and .index.mf)", dir) } func (mfa *CLIApp) checkManifestOperation(ctx *cli.Context) error { log.Debug("checkManifestOperation()") var manifestPath string var err error if ctx.Args().Len() > 0 { arg := ctx.Args().Get(0) // Check if arg is a directory or a file info, statErr := mfa.Fs.Stat(arg) if statErr == nil && info.IsDir() { // It's a directory, look for manifest inside manifestPath, err = findManifest(mfa.Fs, arg) if err != nil { return err } } else { // Treat as a file path manifestPath = arg } } else { // No argument, look in current directory manifestPath, err = findManifest(mfa.Fs, ".") if err != nil { return err } } basePath := ctx.String("base") showProgress := ctx.Bool("progress") log.Infof("checking manifest %s with base %s", manifestPath, basePath) // Create checker chk, err := checker.NewChecker(manifestPath, basePath, mfa.Fs) if err != nil { return fmt.Errorf("failed to load manifest: %w", err) } log.Infof("manifest contains %d files, %d bytes", chk.FileCount(), chk.TotalBytes()) // Set up results channel results := make(chan checker.Result, 1) // Set up progress channel var progress chan checker.CheckStatus if showProgress { progress = make(chan checker.CheckStatus, 1) go func() { for status := range progress { if status.ETA > 0 { log.Progressf("Checking: %d/%d files, %.1f MB/s, ETA %s, %d failures", status.CheckedFiles, status.TotalFiles, status.BytesPerSec/1e6, status.ETA.Round(time.Second), status.Failures) } else { log.Progressf("Checking: %d/%d files, %.1f MB/s, %d failures", status.CheckedFiles, status.TotalFiles, status.BytesPerSec/1e6, status.Failures) } } log.ProgressDone() }() } // Process results in a goroutine var failures int64 done := make(chan struct{}) go func() { for result := range results { if result.Status != checker.StatusOK { failures++ log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message) } else { log.Debugf("%s: %s", result.Status, result.Path) } } close(done) }() // Run check err = chk.Check(ctx.Context, results, progress) if err != nil { return fmt.Errorf("check failed: %w", err) } // Wait for results processing to complete <-done // Check for extra files if requested if ctx.Bool("no-extra-files") { extraResults := make(chan checker.Result, 1) extraDone := make(chan struct{}) go func() { for result := range extraResults { failures++ log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message) } close(extraDone) }() err = chk.FindExtraFiles(ctx.Context, extraResults) if err != nil { return fmt.Errorf("failed to check for extra files: %w", err) } <-extraDone } elapsed := time.Since(mfa.startupTime).Seconds() rate := float64(chk.TotalBytes()) / elapsed / 1e6 if failures == 0 { log.Infof("checked %d files (%.1f MB) in %.1fs (%.1f MB/s): all OK", chk.FileCount(), float64(chk.TotalBytes())/1e6, elapsed, rate) } else { log.Infof("checked %d files (%.1f MB) in %.1fs (%.1f MB/s): %d failed", chk.FileCount(), float64(chk.TotalBytes())/1e6, elapsed, rate, failures) } if failures > 0 { mfa.exitCode = 1 } return nil }