ls.go

  1package fsext
  2
  3import (
  4	"bytes"
  5	"io"
  6	"log/slog"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10
 11	"github.com/charlievieth/fastwalk"
 12	ignore "github.com/sabhiram/go-gitignore"
 13)
 14
 15// CommonIgnorePatterns contains commonly ignored files and directories
 16var CommonIgnorePatterns = []string{
 17	// Version control
 18	".git",
 19	".svn",
 20	".hg",
 21	".bzr",
 22
 23	// IDE and editor files
 24	".vscode",
 25	".idea",
 26	"*.swp",
 27	"*.swo",
 28	"*~",
 29	".DS_Store",
 30	"Thumbs.db",
 31
 32	// Build artifacts and dependencies
 33	"node_modules",
 34	"target",
 35	"build",
 36	"dist",
 37	"out",
 38	"bin",
 39	"obj",
 40	"*.o",
 41	"*.so",
 42	"*.dylib",
 43	"*.dll",
 44	"*.exe",
 45
 46	// Logs and temporary files
 47	"*.log",
 48	"*.tmp",
 49	"*.temp",
 50	".cache",
 51	".tmp",
 52
 53	// Language-specific
 54	"__pycache__",
 55	"*.pyc",
 56	"*.pyo",
 57	".pytest_cache",
 58	"vendor",
 59	"Cargo.lock",
 60	"package-lock.json",
 61	"yarn.lock",
 62	"pnpm-lock.yaml",
 63
 64	// OS generated files
 65	".Trash",
 66	".Spotlight-V100",
 67	".fseventsd",
 68
 69	// Crush
 70	".crush",
 71}
 72
 73type DirectoryLister struct {
 74	ignores  *ignore.GitIgnore
 75	rootPath string
 76}
 77
 78func NewDirectoryLister(rootPath string) *DirectoryLister {
 79	dl := &DirectoryLister{
 80		rootPath: rootPath,
 81	}
 82
 83	dl.ignores = ignore.CompileIgnoreLines(append(CommonIgnorePatterns, strings.Split(parseIgnores(rootPath), "\n")...)...)
 84
 85	return dl
 86}
 87
 88func parseIgnores(path string) string {
 89	var b bytes.Buffer
 90	for _, ign := range []string{".crushignore", ".gitignore"} {
 91		p := filepath.Join(path, ign)
 92		if _, err := os.Stat(p); err == nil {
 93			f, err := os.Open(p)
 94			if err != nil {
 95				_ = f.Close()
 96				slog.Error("Failed to open ignore file", "path", p, "error", err)
 97				continue
 98			}
 99			if _, err := io.Copy(&b, f); err != nil {
100				slog.Error("Failed to read ignore file", "path", p, "error", err)
101			}
102			_ = f.Close()
103		}
104	}
105	return b.String()
106}
107
108func (dl *DirectoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
109	relPath, err := filepath.Rel(dl.rootPath, path)
110	if err != nil {
111		relPath = path
112	}
113
114	if dl.ignores.MatchesPath(relPath) {
115		return true
116	}
117
118	base := filepath.Base(path)
119
120	for _, pattern := range ignorePatterns {
121		matched, err := filepath.Match(pattern, base)
122		if err == nil && matched {
123			return true
124		}
125	}
126	return false
127}
128
129// ListDirectory lists files and directories in the specified path,
130func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
131	var results []string
132	truncated := false
133	dl := NewDirectoryLister(initialPath)
134
135	conf := fastwalk.Config{
136		Follow: true,
137		// Use forward slashes when running a Windows binary under WSL or MSYS
138		ToSlash: fastwalk.DefaultToSlash(),
139		Sort:    fastwalk.SortDirsFirst,
140	}
141
142	err := fastwalk.Walk(&conf, initialPath, func(path string, d os.DirEntry, err error) error {
143		if err != nil {
144			return nil // Skip files we don't have permission to access
145		}
146
147		if dl.shouldIgnore(path, ignorePatterns) {
148			if d.IsDir() {
149				return filepath.SkipDir
150			}
151			return nil
152		}
153
154		if path != initialPath {
155			if d.IsDir() {
156				path = path + string(filepath.Separator)
157			}
158			results = append(results, path)
159		}
160
161		if limit > 0 && len(results) >= limit {
162			truncated = true
163			return filepath.SkipAll
164		}
165
166		return nil
167	})
168	if err != nil && len(results) == 0 {
169		return nil, truncated, err
170	}
171
172	return results, truncated, nil
173}