Path: blob/main/vendor/github.com/sagikazarmark/locafero/finder.go
2875 views
// Package locafero looks for files and directories in an {fs.Fs} filesystem.1package locafero23import (4"errors"5"io/fs"6"path/filepath"7"strings"89"github.com/spf13/afero"1011"github.com/sagikazarmark/locafero/internal/queue"12)1314// Finder looks for files and directories in an [afero.Fs] filesystem.15type 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/user22// - etc23Paths []string2425// 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 [filepath.Match]), offering greater flexibility in search patterns.31//32// Examples:33// - config.yaml34// - home/*/config.yaml35// - home/*/config.*36Names []string3738// 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.41Type FileType42}4344// Find looks for files and directories in an [afero.Fs] filesystem.45func (f Finder) Find(fsys afero.Fs) ([]string, error) {46q := queue.NewEager[[]searchResult]()4748for _, searchPath := range f.Paths {49for _, searchName := range f.Names {50q.Add(func() ([]searchResult, error) {51// If the name contains any glob character, perform a glob match52if strings.ContainsAny(searchName, globMatch) {53return globWalkSearch(fsys, searchPath, searchName, f.Type)54}5556return statSearch(fsys, searchPath, searchName, f.Type)57})58}59}6061searchResults, err := flatten(q.Wait())62if err != nil {63return nil, err64}6566// Return early if no results were found67if len(searchResults) == 0 {68return nil, nil69}7071results := make([]string, 0, len(searchResults))7273for _, searchResult := range searchResults {74results = append(results, searchResult.path)75}7677return results, nil78}7980type searchResult struct {81path string82info fs.FileInfo83}8485func flatten[T any](results [][]T, err error) ([]T, error) {86if err != nil {87return nil, err88}8990var flattened []T9192for _, r := range results {93flattened = append(flattened, r...)94}9596return flattened, nil97}9899func globWalkSearch(100fsys afero.Fs,101searchPath string,102searchName string,103searchType FileType,104) ([]searchResult, error) {105var results []searchResult106107err := afero.Walk(fsys, searchPath, func(p string, fileInfo fs.FileInfo, err error) error {108if err != nil {109return err110}111112// Skip the root path113if p == searchPath {114return nil115}116117var result error118119// Stop reading subdirectories120// TODO: add depth detection here121if fileInfo.IsDir() && filepath.Dir(p) == searchPath {122result = fs.SkipDir123}124125// Skip unmatching type126if !searchType.match(fileInfo) {127return result128}129130match, err := filepath.Match(searchName, fileInfo.Name())131if err != nil {132return err133}134135if match {136results = append(results, searchResult{p, fileInfo})137}138139return result140})141if err != nil {142return results, err143}144145return results, nil146}147148func statSearch(149fsys afero.Fs,150searchPath string,151searchName string,152searchType FileType,153) ([]searchResult, error) {154filePath := filepath.Join(searchPath, searchName)155156fileInfo, err := fsys.Stat(filePath)157if errors.Is(err, fs.ErrNotExist) {158return nil, nil159}160if err != nil {161return nil, err162}163164// Skip unmatching type165if !searchType.match(fileInfo) {166return nil, nil167}168169return []searchResult{{filePath, fileInfo}}, nil170}171172173