finder.go raw
1 // Package finder looks for files and directories in an {fs.Fs} filesystem.
2 package locafero
3
4 import (
5 "errors"
6 "io/fs"
7 "path/filepath"
8 "strings"
9
10 "github.com/sourcegraph/conc/iter"
11 "github.com/spf13/afero"
12 )
13
14 // Finder looks for files and directories in an [afero.Fs] filesystem.
15 type Finder struct {
16 // Paths represents a list of locations that the [Finder] will search in.
17 //
18 // They are essentially the root directories or starting points for the search.
19 //
20 // Examples:
21 // - home/user
22 // - etc
23 Paths []string
24
25 // Names are specific entries that the [Finder] will look for within the given Paths.
26 //
27 // It provides the capability to search for entries with depth,
28 // meaning it can target deeper locations within the directory structure.
29 //
30 // It also supports glob syntax (as defined by [filepat.Match]), offering greater flexibility in search patterns.
31 //
32 // Examples:
33 // - config.yaml
34 // - home/*/config.yaml
35 // - home/*/config.*
36 Names []string
37
38 // Type restricts the kind of entries returned by the [Finder].
39 //
40 // This parameter helps in differentiating and filtering out files from directories or vice versa.
41 Type FileType
42 }
43
44 // Find looks for files and directories in an [afero.Fs] filesystem.
45 func (f Finder) Find(fsys afero.Fs) ([]string, error) {
46 // Arbitrary go routine limit (TODO: make this a parameter)
47 // pool := pool.NewWithResults[[]string]().WithMaxGoroutines(5).WithErrors().WithFirstError()
48
49 type searchItem struct {
50 path string
51 name string
52 }
53
54 var searchItems []searchItem
55
56 for _, searchPath := range f.Paths {
57 searchPath := searchPath
58
59 for _, searchName := range f.Names {
60 searchName := searchName
61
62 searchItems = append(searchItems, searchItem{searchPath, searchName})
63
64 // pool.Go(func() ([]string, error) {
65 // // If the name contains any glob character, perform a glob match
66 // if strings.ContainsAny(searchName, "*?[]\\^") {
67 // return globWalkSearch(fsys, searchPath, searchName, f.Type)
68 // }
69 //
70 // return statSearch(fsys, searchPath, searchName, f.Type)
71 // })
72 }
73 }
74
75 // allResults, err := pool.Wait()
76 // if err != nil {
77 // return nil, err
78 // }
79
80 allResults, err := iter.MapErr(searchItems, func(item *searchItem) ([]string, error) {
81 // If the name contains any glob character, perform a glob match
82 if strings.ContainsAny(item.name, "*?[]\\^") {
83 return globWalkSearch(fsys, item.path, item.name, f.Type)
84 }
85
86 return statSearch(fsys, item.path, item.name, f.Type)
87 })
88 if err != nil {
89 return nil, err
90 }
91
92 var results []string
93
94 for _, r := range allResults {
95 results = append(results, r...)
96 }
97
98 // Sort results in alphabetical order for now
99 // sort.Strings(results)
100
101 return results, nil
102 }
103
104 func globWalkSearch(fsys afero.Fs, searchPath string, searchName string, searchType FileType) ([]string, error) {
105 var results []string
106
107 err := afero.Walk(fsys, searchPath, func(p string, fileInfo fs.FileInfo, err error) error {
108 if err != nil {
109 return err
110 }
111
112 // Skip the root path
113 if p == searchPath {
114 return nil
115 }
116
117 var result error
118
119 // Stop reading subdirectories
120 // TODO: add depth detection here
121 if fileInfo.IsDir() && filepath.Dir(p) == searchPath {
122 result = fs.SkipDir
123 }
124
125 // Skip unmatching type
126 if !searchType.matchFileInfo(fileInfo) {
127 return result
128 }
129
130 match, err := filepath.Match(searchName, fileInfo.Name())
131 if err != nil {
132 return err
133 }
134
135 if match {
136 results = append(results, p)
137 }
138
139 return result
140 })
141 if err != nil {
142 return results, err
143 }
144
145 return results, nil
146 }
147
148 func statSearch(fsys afero.Fs, searchPath string, searchName string, searchType FileType) ([]string, error) {
149 filePath := filepath.Join(searchPath, searchName)
150
151 fileInfo, err := fsys.Stat(filePath)
152 if errors.Is(err, fs.ErrNotExist) {
153 return nil, nil
154 }
155 if err != nil {
156 return nil, err
157 }
158
159 // Skip unmatching type
160 if !searchType.matchFileInfo(fileInfo) {
161 return nil, nil
162 }
163
164 return []string{filePath}, nil
165 }
166