- Add FileCount, FileSize, RelFilePath, AbsFilePath, ModTime, Multihash types - Add UnixSeconds and UnixNanos types for timestamp handling - Add URL types (ManifestURL, FileURL, BaseURL) with safe path joining - Consolidate scanner package into mfer package - Update checker to use custom types in Result and CheckStatus - Add ModTime.Timestamp() method for protobuf conversion - Update all tests to use proper custom types
169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
"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/mfer"
|
|
)
|
|
|
|
func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
|
log.Debug("generateManifestOperation()")
|
|
|
|
opts := &mfer.ScannerOptions{
|
|
IncludeDotfiles: ctx.Bool("IncludeDotfiles"),
|
|
FollowSymLinks: ctx.Bool("FollowSymLinks"),
|
|
Fs: mfa.Fs,
|
|
}
|
|
|
|
s := mfer.NewScannerWithOptions(opts)
|
|
|
|
// Phase 1: Enumeration - collect paths and stat files
|
|
args := ctx.Args()
|
|
showProgress := ctx.Bool("progress")
|
|
|
|
// Set up enumeration progress reporting
|
|
var enumProgress chan mfer.EnumerateStatus
|
|
var enumWg sync.WaitGroup
|
|
if showProgress {
|
|
enumProgress = make(chan mfer.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 and validate all paths first
|
|
paths := make([]string, 0, args.Len())
|
|
for i := 0; i < args.Len(); i++ {
|
|
inputPath := args.Get(i)
|
|
ap, err := filepath.Abs(inputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Validate path exists before adding to list
|
|
if exists, _ := afero.Exists(mfa.Fs, ap); !exists {
|
|
return fmt.Errorf("path does not exist: %s", inputPath)
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Set up signal handler to clean up temp file on Ctrl-C
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
sig, ok := <-sigChan
|
|
if !ok || sig == nil {
|
|
return // Channel closed normally, not a signal
|
|
}
|
|
_ = outFile.Close()
|
|
_ = mfa.Fs.Remove(tmpPath)
|
|
os.Exit(1)
|
|
}()
|
|
|
|
// Clean up temp file on any error or interruption
|
|
success := false
|
|
defer func() {
|
|
signal.Stop(sigChan)
|
|
close(sigChan)
|
|
_ = outFile.Close()
|
|
if !success {
|
|
_ = mfa.Fs.Remove(tmpPath)
|
|
}
|
|
}()
|
|
|
|
// Phase 2: Scan - read file contents and generate manifest
|
|
var scanProgress chan mfer.ScanStatus
|
|
var scanWg sync.WaitGroup
|
|
if showProgress {
|
|
scanProgress = make(chan mfer.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
|
|
}
|