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 "" }