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  }