1
0
Fork 0
collect/collector/collector.go
Andrew Tomaka 64a2bbace4 Add --verbose flag for real-time file discovery feedback
Implement verbose mode that shows files as they are discovered during
collection rather than just a summary. This provides better user feedback
during the typically slow filesystem traversal phase.

Features:
- Real-time "found: filename" output as files are discovered
- Verbose feedback during collection phase (not archiving phase)
- Simple, focused output that shows filtering effects
- Helps users track progress during potentially slow operations

Changes:
- Add --verbose boolean flag to CLI
- Pass verbose flag to collector constructors
- Display "found: filename" output during file collection
- Add test case to verify verbose output contains expected messages
- Update test suite with helper function to verify output contents
2025-06-12 22:39:32 -04:00

143 lines
3.3 KiB
Go

package collector
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
// FileEntry represents a file to be archived
type FileEntry struct {
Path string // Relative path from sourceDir
FullPath string // Absolute path for reading
}
// Collector handles file collection based on a matcher and optional directory filter
type Collector struct {
matcher Matcher
dirFilter DirectoryFilter
verbose bool
}
// New creates a new collector with the specified matcher
func New(matcher Matcher, verbose bool) *Collector {
return &Collector{
matcher: matcher,
dirFilter: nil,
verbose: verbose,
}
}
// NewWithDirectoryFilter creates a new collector with the specified matcher and directory filter
func NewWithDirectoryFilter(matcher Matcher, dirFilter DirectoryFilter, verbose bool) *Collector {
return &Collector{
matcher: matcher,
dirFilter: dirFilter,
verbose: verbose,
}
}
// Collect walks the source directory and collects matching files
func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) {
// Clean and convert to absolute path
absSourceDir, err := filepath.Abs(sourceDir)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path: %w", err)
}
// Check if source directory exists
info, err := os.Stat(absSourceDir)
if err != nil {
return nil, fmt.Errorf("source directory error: %w", err)
}
if !info.IsDir() {
return nil, fmt.Errorf("source path is not a directory: %s", sourceDir)
}
var files []FileEntry
err = filepath.WalkDir(absSourceDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// Log permission errors but continue walking
if os.IsPermission(err) {
fmt.Fprintf(os.Stderr, "Warning: Permission denied: %s\n", path)
return nil
}
return err
}
// Get file info
info, err := d.Info()
if err != nil {
if os.IsPermission(err) {
fmt.Fprintf(os.Stderr, "Warning: Cannot stat file: %s\n", path)
return nil
}
return err
}
// Skip symlinks
if info.Mode()&os.ModeSymlink != 0 {
return nil
}
// Check directory filter for directories
if info.IsDir() && c.dirFilter != nil {
if !c.dirFilter.ShouldTraverse(path, absSourceDir) {
return filepath.SkipDir
}
}
// Check if this file should be included
if c.matcher.ShouldInclude(path, info) {
// Calculate relative path from source directory
relPath, err := filepath.Rel(absSourceDir, path)
if err != nil {
return fmt.Errorf("failed to get relative path: %w", err)
}
// Clean the relative path to ensure consistent formatting
relPath = filepath.ToSlash(relPath)
// Show verbose output when file is found
if c.verbose {
fmt.Printf("found: %s\n", relPath)
}
files = append(files, FileEntry{
Path: relPath,
FullPath: path,
})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("walk error: %w", err)
}
// Check if any files were found
if len(files) == 0 {
return nil, fmt.Errorf("no files found matching criteria")
}
return files, nil
}
// GetArchiveFormat determines the archive format from the filename
func GetArchiveFormat(filename string) string {
lower := strings.ToLower(filename)
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
return "tar.gz"
}
if strings.HasSuffix(lower, ".zip") {
return "zip"
}
return ""
}