144 lines
2.7 KiB
Go
144 lines
2.7 KiB
Go
package traversal
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Walker streams filesystem entries relative to a working directory.
|
|
type Walker struct{}
|
|
|
|
// NewWalker constructs a new Walker with default behavior.
|
|
func NewWalker() *Walker {
|
|
return &Walker{}
|
|
}
|
|
|
|
// Walk traverses starting at root and invokes fn for each matching entry.
|
|
//
|
|
// The callback receives the relative path, os.DirEntry metadata, and depth.
|
|
// Directories that are symbolic links are not descended into when recursive is true.
|
|
func (w *Walker) Walk(
|
|
root string,
|
|
recursive bool,
|
|
includeDirs bool,
|
|
includeHidden bool,
|
|
maxDepth int,
|
|
fn func(relPath string, entry fs.DirEntry, depth int) error,
|
|
) error {
|
|
if root == "" {
|
|
return errors.New("walk root cannot be empty")
|
|
}
|
|
|
|
info, err := os.Stat(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
return errors.New("walk root must be a directory")
|
|
}
|
|
|
|
rootAbs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
walker := func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
// propagate traversal errors to caller for logging/handling
|
|
return err
|
|
}
|
|
|
|
rel, relErr := filepath.Rel(rootAbs, path)
|
|
if relErr != nil {
|
|
return relErr
|
|
}
|
|
|
|
rel = filepath.Clean(rel)
|
|
depth := depthFor(rel)
|
|
|
|
if rel == "." {
|
|
// Skip emitting the root directory unless explicitly requested.
|
|
if includeDirs {
|
|
return fn(rel, d, depth)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if maxDepth > 0 && depth > maxDepth {
|
|
if d.IsDir() {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if !includeHidden && isHidden(rel) {
|
|
if d.IsDir() && recursive {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if d.IsDir() {
|
|
if !recursive && depth > 0 {
|
|
return fs.SkipDir
|
|
}
|
|
if !includeDirs {
|
|
// continue traversal but don't emit directory
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if d.Type()&os.ModeSymlink != 0 && d.IsDir() {
|
|
// emit symlink entry but do not traverse into it
|
|
if err := fn(rel, d, depth); err != nil {
|
|
return err
|
|
}
|
|
if recursive {
|
|
return nil
|
|
}
|
|
return fs.SkipDir
|
|
}
|
|
|
|
return fn(rel, d, depth)
|
|
}
|
|
|
|
if recursive {
|
|
return filepath.WalkDir(rootAbs, walker)
|
|
}
|
|
|
|
entries, err := os.ReadDir(rootAbs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
path := filepath.Join(rootAbs, entry.Name())
|
|
if err := walker(path, entry, nil); err != nil {
|
|
if errors.Is(err, fs.SkipDir) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func depthFor(rel string) int {
|
|
if rel == "." || rel == "" {
|
|
return 0
|
|
}
|
|
return strings.Count(rel, string(filepath.Separator))
|
|
}
|
|
|
|
func isHidden(rel string) bool {
|
|
parts := strings.Split(rel, string(filepath.Separator))
|
|
for _, part := range parts {
|
|
if len(part) > 0 && part[0] == '.' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|