Implement comprehensive directory filtering functionality that allows users to control which directories are traversed during file collection. Features: - --include-dir: Only traverse directories matching specified patterns - --exclude-dir: Skip directories matching specified patterns - Support for glob patterns (e.g., 'temp-*', 'project-*') - Multiple filters with OR logic (like existing --name/--match flags) - Include filters take precedence over exclude filters when both specified - Seamless integration with existing file matching functionality Implementation: - Add DirectoryFilter interface with Include/Exclude/Composite implementations - Update Collector to accept optional DirectoryFilter and use filepath.SkipDir - Add CLI flags and argument parsing for new directory filtering options - Comprehensive test suite with 7 new test cases covering all scenarios
135 lines
3.1 KiB
Go
135 lines
3.1 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
|
|
}
|
|
|
|
// New creates a new collector with the specified matcher
|
|
func New(matcher Matcher) *Collector {
|
|
return &Collector{
|
|
matcher: matcher,
|
|
dirFilter: nil,
|
|
}
|
|
}
|
|
|
|
// NewWithDirectoryFilter creates a new collector with the specified matcher and directory filter
|
|
func NewWithDirectoryFilter(matcher Matcher, dirFilter DirectoryFilter) *Collector {
|
|
return &Collector{
|
|
matcher: matcher,
|
|
dirFilter: dirFilter,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
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 ""
|
|
}
|