Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
172 changes: 172 additions & 0 deletions path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package afero

import (
"fmt"
iofs "io/fs"
"os"
"path/filepath"
"testing"
)

Expand Down Expand Up @@ -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())
}
}
}