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