github.com/aretext/aretext@v1.3.0/file/listdir.go (about) 1 package file 2 3 import ( 4 "context" 5 "fmt" 6 "io/fs" 7 "log" 8 "os" 9 "path/filepath" 10 "runtime" 11 "sync" 12 ) 13 14 type ListDirOptions struct { 15 DirPatternsToHide []string // Glob patterns for directories to skip. 16 DirectoriesOnly bool // If true, return directories (not files) in results. 17 } 18 19 // ListDir lists every file in a root directory and its subdirectories. 20 // The returned paths are relative to the root directory. 21 // The order of the returned paths is non-deterministic. 22 // Symbolic links are not followed. 23 // If an error occurs while accessing a directory, ListDir will skip that 24 // directory and log the error. 25 func ListDir(ctx context.Context, root string, options ListDirOptions) []string { 26 // Use a semaphore to limit the number of open files. 27 semaphoreChan := make(chan struct{}, runtime.NumCPU()) 28 return listDirRec(ctx, root, options, semaphoreChan) 29 } 30 31 func listDirRec(ctx context.Context, root string, options ListDirOptions, semaphoreChan chan struct{}) []string { 32 select { 33 case <-ctx.Done(): 34 log.Printf("Context done channel closed while listing subdirectories in %q: %s\n", root, ctx.Err()) 35 return nil 36 default: 37 break 38 } 39 40 semaphoreChan <- struct{}{} // Block until open file count decreases. 41 dirEntries, err := listDir(root) 42 <-semaphoreChan // Decrease open file count. 43 44 if err != nil { 45 log.Printf("Error listing subdirectories in %q: %s\n", root, err) 46 return nil 47 } 48 49 var mu sync.Mutex 50 var results []string 51 var wg sync.WaitGroup 52 for _, d := range dirEntries { 53 path := filepath.Join(root, d.Name()) 54 55 if !d.IsDir() { 56 if !options.DirectoriesOnly { 57 mu.Lock() 58 results = append(results, path) 59 mu.Unlock() 60 } 61 continue 62 } 63 64 if shouldSkipDir(path, options.DirPatternsToHide) { 65 continue 66 } 67 68 if options.DirectoriesOnly { 69 mu.Lock() 70 results = append(results, path) 71 mu.Unlock() 72 } 73 74 // Traverse subdirectories concurrently. 75 wg.Add(1) 76 go func(path string) { 77 defer wg.Done() 78 subpaths := listDirRec(ctx, path, options, semaphoreChan) 79 mu.Lock() 80 results = append(results, subpaths...) 81 mu.Unlock() 82 }(path) 83 } 84 wg.Wait() 85 86 return results 87 } 88 89 func listDir(path string) ([]fs.DirEntry, error) { 90 f, err := os.Open(path) 91 if err != nil { 92 return nil, fmt.Errorf("os.Open: %w", err) 93 } 94 dirs, err := f.ReadDir(-1) 95 f.Close() 96 if err != nil { 97 return nil, fmt.Errorf("f.ReadDir: %w", err) 98 } 99 return dirs, nil 100 } 101 102 func shouldSkipDir(path string, dirPatternsToHide []string) bool { 103 for _, pattern := range dirPatternsToHide { 104 if GlobMatch(pattern, path) { 105 return true 106 } 107 } 108 return false 109 }