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