diff --git a/path.go b/path.go index 5ae5b18..b89888e 100644 --- a/path.go +++ b/path.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/pkg/errors" "github.com/LandonTClipp/afero" + "github.com/pkg/errors" ) // Path is an object that represents a path @@ -83,8 +83,9 @@ func doesNotImplementErr(interfaceName string, fs afero.Fs) error { // ******************************* // Create creates a file if possible, returning the file and an error, if any happens. -func (p *Path) Create() (afero.File, error) { - return p.Fs().Create(p.Path()) +func (p *Path) Create() (File, error) { + file, err := p.Fs().Create(p.Path()) + return File{file}, err } // Mkdir makes the current dir. If the parents don't exist, an error @@ -200,12 +201,22 @@ func (p *Path) IsEmpty() (bool, error) { } // ReadDir reads the current path and returns a list of the corresponding -// Path objects. +// Path objects. This function differs from os.Readdir in that it does +// not call Stat() on the files. Instead, it calls Readdirnames which +// is less expensive and does not force the caller to make expensive +// Stat calls. func (p *Path) ReadDir() ([]*Path, error) { var paths []*Path - fileInfos, err := afero.ReadDir(p.Fs(), p.Path()) - for _, fileInfo := range fileInfos { - paths = append(paths, p.Join(fileInfo.Name())) + handle, err := p.Open() + if err != nil { + return paths, err + } + children, err := handle.Readdirnames(-1) + if err != nil { + return paths, err + } + for _, child := range children { + paths = append(paths, p.Join(child)) } return paths, err } diff --git a/path_test.go b/path_test.go index 1d3a376..c18b02f 100644 --- a/path_test.go +++ b/path_test.go @@ -301,6 +301,29 @@ func (p *PathSuite) TestEquals() { p.True(hello1.Equals(hello2)) } +func (p *PathSuite) TestReadDir() { + require.NoError(p.T(), TwoFilesAtRootTwoInSubdir(p.tmpdir)) + paths, err := p.tmpdir.ReadDir() + p.NoError(err) + p.Equal(3, len(paths)) +} + +func (p *PathSuite) TestReadDirInvalidPath() { + paths, err := p.tmpdir.Join("i_dont_exist").ReadDir() + p.Error(err) + p.Equal(0, len(paths)) +} + +func (p *PathSuite) TestCreate() { + msg := "hello world" + file, err := p.tmpdir.Join("hello.txt").Create() + p.NoError(err) + defer file.Close() + n, err := file.WriteString(msg) + p.Equal(len(msg), n) + p.NoError(err) +} + func TestPathSuite(t *testing.T) { suite.Run(t, new(PathSuite)) } diff --git a/scenarios_test.go b/scenarios_test.go new file mode 100644 index 0000000..5ce9781 --- /dev/null +++ b/scenarios_test.go @@ -0,0 +1,38 @@ +package pathlib + +import "fmt" + +// The following functions provide different "scenarios" +// that you might encounter in a filesystem tree. + +func HelloWorld(root *Path) error { + hello := root.Join("hello.txt") + return hello.WriteFile([]byte("hello world"), 0o644) +} + +func OneFile(root *Path, name string, content string) error { + file := root.Join(name) + return file.WriteFile([]byte(content), 0o644) +} + +func NFiles(root *Path, n int) error { + for i := 0; i < n; i++ { + if err := OneFile(root, fmt.Sprintf("file%d.txt", i), fmt.Sprintf("file%d contents", i)); err != nil { + return err + } + } + return nil +} + +// TwoFilesAtRootTwoInSubdir creates two files in the root dir, +// a directory, and creates two files inside that new directory. +func TwoFilesAtRootTwoInSubdir(root *Path) error { + if err := NFiles(root, 2); err != nil { + return err + } + subdir := root.Join("subdir") + if err := subdir.Mkdir(0o777); err != nil { + return err + } + return NFiles(subdir, 2) +} diff --git a/walk.go b/walk.go index a702e4c..d2fdb1c 100644 --- a/walk.go +++ b/walk.go @@ -12,9 +12,9 @@ type WalkOpts struct { // infinite depth. 0 means only the direct children of root will be returned, etc. Depth int - // WalkAlgorithm specifies the algoritm that the Walk() function should use to + // Algorithm specifies the algoritm that the Walk() function should use to // traverse the directory. - WalkAlgorithm string + Algorithm Algorithm // FollowSymlinks defines whether symlinks should be dereferenced or not. If True, // the symlink itself will never be returned to WalkFunc, but rather whatever it @@ -37,26 +37,25 @@ type WalkOpts struct { func DefaultWalkOpts() *WalkOpts { return &WalkOpts{ Depth: -1, - WalkAlgorithm: AlgorithmBasic(), + Algorithm: AlgorithmBasic, FollowSymlinks: false, - // VisitFirst: false, } } -// AlgorithmDepthFirst is a walk algorithm. It iterates over a filesystem tree -// by first recursing as far down as it can in one path. Each directory is visited -// only after all of its children directories have been recursed. -func AlgorithmDepthFirst() string { - return "depth-first" -} - -// AlgorithmBasic is a walk algorithm. It iterates over filesystem objects in the -// order in which they are returned by the operating system. It guarantees no -// ordering of any kind. This is the most efficient algorithm and should be used -// in all cases where ordering does not matter. -func AlgorithmBasic() string { - return "basic" -} +// Algorithm represents the walk algorithm that will be performed. +type Algorithm int + +const ( + // AlgorithmBasic is a walk algorithm. It iterates over filesystem objects in the + // order in which they are returned by the operating system. It guarantees no + // ordering of any kind. This is the most efficient algorithm and should be used + // in all cases where ordering does not matter. + AlgorithmBasic Algorithm = iota + // AlgorithmDepthFirst is a walk algorithm. It iterates over a filesystem tree + // by first recursing as far down as it can in one path. Each directory is visited + // only after all of its children directories have been recursed. + AlgorithmDepthFirst +) // Walk is an object that handles walking through a directory tree type Walk struct { @@ -207,8 +206,8 @@ type WalkFunc func(path *Path, info os.FileInfo, err error) error // Walk walks the directory using the algorithm specified in the configuration. func (w *Walk) Walk(walkFn WalkFunc) error { - switch w.Opts.WalkAlgorithm { - case AlgorithmBasic(): + switch w.Opts.Algorithm { + case AlgorithmBasic: if err := w.walkBasic(walkFn, w.root, 0); err != nil { if errors.Is(err, ErrStopWalk) { return nil @@ -216,7 +215,7 @@ func (w *Walk) Walk(walkFn WalkFunc) error { return err } return nil - case AlgorithmDepthFirst(): + case AlgorithmDepthFirst: if err := w.walkDFS(walkFn, w.root, 0); err != nil { if errors.Is(err, ErrStopWalk) { return nil diff --git a/walk_test.go b/walk_test.go index 330ff98..6a0cc63 100644 --- a/walk_test.go +++ b/walk_test.go @@ -11,44 +11,6 @@ import ( "github.com/stretchr/testify/suite" ) -// *********************** -// * FILESYSTEM FIXTURES * -// *********************** -// The following functions provide different "scenarios" -// that you might encounter in a filesystem tree. - -func HelloWorld(root *Path) error { - hello := root.Join("hello.txt") - return hello.WriteFile([]byte("hello world"), 0o644) -} - -func OneFile(root *Path, name string, content string) error { - file := root.Join(name) - return file.WriteFile([]byte(content), 0o644) -} - -func NFiles(root *Path, n int) error { - for i := 0; i < n; i++ { - if err := OneFile(root, fmt.Sprintf("file%d.txt", i), fmt.Sprintf("file%d contents", i)); err != nil { - return err - } - } - return nil -} - -// TwoFilesAtRootTwoInSubdir creates two files in the root dir, -// a directory, and creates two files inside that new directory. -func TwoFilesAtRootTwoInSubdir(root *Path) error { - if err := NFiles(root, 2); err != nil { - return err - } - subdir := root.Join("subdir") - if err := subdir.Mkdir(0o777); err != nil { - return err - } - return NFiles(subdir, 2) -} - // ********* // * TESTS * // ********* @@ -60,7 +22,7 @@ type WalkSuiteAll struct { suite.Suite walk *Walk root *Path - algorithm string + algorithm Algorithm Fs afero.Fs } @@ -71,7 +33,7 @@ func (w *WalkSuiteAll) SetupTest() { w.root = NewPathAfero("/", w.Fs) w.walk, err = NewWalk(w.root) require.NoError(w.T(), err) - w.walk.Opts.WalkAlgorithm = w.algorithm + w.walk.Opts.Algorithm = w.algorithm } func (w *WalkSuiteAll) TeardownTest() { @@ -144,9 +106,9 @@ func (w *WalkSuiteAll) TestWalkFuncErr() { } func TestWalkSuite(t *testing.T) { - for _, algorithm := range []string{ - AlgorithmBasic(), - AlgorithmDepthFirst(), + for _, algorithm := range []Algorithm{ + AlgorithmBasic, + AlgorithmDepthFirst, } { walkSuite := new(WalkSuiteAll) walkSuite.algorithm = algorithm @@ -159,7 +121,7 @@ func TestDefaultWalkOpts(t *testing.T) { name string want *WalkOpts }{ - {"assert defaults", &WalkOpts{-1, AlgorithmBasic(), false, 100}}, + {"assert defaults", &WalkOpts{-1, AlgorithmBasic, false}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -170,6 +132,8 @@ func TestDefaultWalkOpts(t *testing.T) { } } +var ConfusedWandering Algorithm = 0xBADC0DE + func TestWalk_Walk(t *testing.T) { type fields struct { Opts *WalkOpts @@ -187,7 +151,7 @@ func TestWalk_Walk(t *testing.T) { { name: "Bad algoritm", fields: fields{ - Opts: &WalkOpts{WalkAlgorithm: "confused wandering"}, + Opts: &WalkOpts{Algorithm: ConfusedWandering}, }, wantErr: true, },