diff --git a/path.go b/path.go index 18f60a0f..c3e2a8a1 100644 --- a/path.go +++ b/path.go @@ -16,9 +16,12 @@ package afero import ( + iofs "io/fs" "os" "path/filepath" "sort" + + "github.com/spf13/afero/internal/common" ) // readDirNames reads the directory named by dirname and returns @@ -104,3 +107,107 @@ func Walk(fs Fs, root string, walkFn filepath.WalkFunc) error { } return walk(fs, root, info, walkFn) } + +// readDirEntries reads the directory named by dirname and returns +// a sorted list of directory entries. +func readDirEntries(fs Fs, dirname string) ([]iofs.DirEntry, error) { + f, err := fs.Open(dirname) + if err != nil { + return nil, err + } + defer f.Close() + + var entries []iofs.DirEntry + + if rdf, ok := f.(iofs.ReadDirFile); ok { + entries, err = rdf.ReadDir(-1) + if err != nil { + return nil, err + } + } else { + var infos []os.FileInfo + + infos, err = f.Readdir(-1) + if err != nil { + return nil, err + } + + entries = make([]iofs.DirEntry, len(infos)) + + for i, info := range infos { + entries[i] = common.FileInfoDirEntry{FileInfo: info} + } + } + + sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) + + return entries, nil +} + +// walkDir recursively descends path, calling walkDirFn. +// adapted from https://go.dev/src/path/filepath/path.go +func walkDir(fs Fs, path string, d iofs.DirEntry, walkDirFn iofs.WalkDirFunc) error { + if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() { + if err == filepath.SkipDir && d.IsDir() { + err = nil + } + + return err + } + + entries, err := readDirEntries(fs, path) + if err != nil { + err = walkDirFn(path, d, err) + if err != nil { + if err == filepath.SkipDir && d.IsDir() { + err = nil + } + + return err + } + } + + for _, entry := range entries { + name := filepath.Join(path, entry.Name()) + if err := walkDir(fs, name, entry, walkDirFn); err != nil { + if err == filepath.SkipDir { + break + } + + return err + } + } + return nil +} + +// WalkDir walks the file tree rooted at root, calling fn for each file or +// directory in the tree, including root. The fn callback receives an fs.DirEntry +// instead of os.FileInfo, which can be more efficient since it does not require +// a stat call for every visited file. +// +// All errors that arise visiting files and directories are filtered by fn: +// see the fs.WalkDirFunc documentation for details. +// +// The files are walked in lexical order, which makes the output deterministic +// but means that for very large directories WalkDir can be inefficient. +// WalkDir does not follow symbolic links. +func (a Afero) WalkDir(root string, fn iofs.WalkDirFunc) error { + return WalkDir(a.Fs, root, fn) +} + +// WalkDir walks the file tree rooted at root, calling fn for each file or +// directory in the tree, including root. See (Afero).WalkDir for details. +func WalkDir(fs Fs, root string, fn iofs.WalkDirFunc) error { + info, err := lstatIfPossible(fs, root) + if err != nil { + err = fn(root, nil, err) + } else { + err = walkDir(fs, root, common.FileInfoDirEntry{FileInfo: info}, fn) + } + + if err == filepath.SkipDir || err == filepath.SkipAll { + return nil + } + + return err +} diff --git a/path_test.go b/path_test.go index 104a6bcb..8b5ccaac 100644 --- a/path_test.go +++ b/path_test.go @@ -16,7 +16,9 @@ package afero import ( "fmt" + iofs "io/fs" "os" + "path/filepath" "testing" ) @@ -67,3 +69,173 @@ func TestWalk(t *testing.T) { t.Fail() } } + +func TestWalkDir(t *testing.T) { + defer removeAllTestFiles(t) + + var testDir string + + for i, fs := range Fss { + if i == 0 { + testDir = setupTestDirRoot(t, fs) + + continue + } + + setupTestDirReusePath(t, fs, testDir) + } + + outputs := make([]string, len(Fss)) + + for i, fs := range Fss { + walkDirFn := func(path string, d iofs.DirEntry, err error) error { + if err != nil { + t.Error("walkDirFn err:", err) + } + + var size int64 + + if !d.IsDir() { + info, infoErr := d.Info() + if infoErr != nil { + t.Error("d.Info() err:", infoErr) + } + + size = info.Size() + } + + outputs[i] += fmt.Sprintln(path, d.Name(), size, d.IsDir(), err) + + return nil + } + + err := WalkDir(fs, testDir, walkDirFn) + if err != nil { + t.Error(err) + } + } + + fail := false + + for i, o := range outputs { + if i == 0 { + continue + } + + if o != outputs[i-1] { + fail = true + + break + } + } + if fail { + t.Log("WalkDir outputs not equal!") + + for i, o := range outputs { + t.Log(Fss[i].Name() + "\n" + o) + } + + t.Fail() + } +} + +func TestWalkDirSkipDir(t *testing.T) { + defer removeAllTestFiles(t) + + for _, fs := range Fss { + root := testDir(fs) + fs.MkdirAll(filepath.Join(root, "more", "subdirectories"), 0o700) + WriteFile(fs, filepath.Join(root, "more", "subdirectories", "file.txt"), []byte("hello"), 0o644) + WriteFile(fs, filepath.Join(root, "other.txt"), []byte("world"), 0o644) + + var visited []string + + walkDirFn := func(path string, d iofs.DirEntry, err error) error { + if err != nil { + t.Error("walkDirFn err:", err) + } + + rel, _ := filepath.Rel(root, path) + visited = append(visited, rel) + + if d.IsDir() && d.Name() == "more" { + return filepath.SkipDir + } + + return nil + } + err := WalkDir(fs, root, walkDirFn) + if err != nil { + t.Error(fs.Name(), err) + } + + foundMore := false + + for _, v := range visited { + if v == "more" { + foundMore = true + } + + if v == filepath.Join("more", "subdirectories") { + t.Errorf("%s: should not have visited more/subdirectories", fs.Name()) + } + } + + if !foundMore { + t.Errorf("%s: should have visited 'more'", fs.Name()) + } + } +} + +func TestWalkDirSkipAll(t *testing.T) { + defer removeAllTestFiles(t) + + for _, fs := range Fss { + root := testDir(fs) + + WriteFile(fs, filepath.Join(root, "a.txt"), []byte("a"), 0o644) + WriteFile(fs, filepath.Join(root, "b.txt"), []byte("b"), 0o644) + WriteFile(fs, filepath.Join(root, "c.txt"), []byte("c"), 0o644) + + count := 0 + walkDirFn := func(path string, d iofs.DirEntry, err error) error { + count++ + if count >= 2 { + return filepath.SkipAll + } + + return nil + } + + err := WalkDir(fs, root, walkDirFn) + if err != nil { + t.Error(fs.Name(), err) + } + + if count > 2 { + t.Errorf("%s: expected at most 2 entries visited, got %d", fs.Name(), count) + } + } +} + +func TestWalkDirError(t *testing.T) { + defer removeAllTestFiles(t) + + for _, fs := range Fss { + var callbackErr error + + walkDirFn := func(path string, d iofs.DirEntry, err error) error { + callbackErr = err + return err + } + + err := WalkDir(fs, "/nonexistent-path-for-walkdir-test", walkDirFn) + if err == nil { + t.Errorf("%s: expected error for nonexistent root", fs.Name()) + } + + if callbackErr == nil { + t.Errorf("%s: expected callback to receive error", fs.Name()) + } + } +}