Skip to content

Commit 3c1d997

Browse files
committed
Adding walk functionality
Adding tests Move over to LandonTClipp afero branch until issue is fixed Loop over every algorithm There are a certain class of tests that should behave the same for each type of algorithm, so create a loop that simply passes in a different algorithm each time and run the same tests for each.
1 parent 9378098 commit 3c1d997

11 files changed

+822
-11
lines changed

.mockery.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
quiet: False
2+
all: True
3+
inpackage: True
4+
testonly: True

errors.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package pathlib
22

3-
import "github.com/pkg/errors"
3+
import "fmt"
44

55
var (
6-
ErrDoesNotImplement = errors.Errorf("doesn't implement required interface")
6+
// ErrDoesNotImplement indicates that the afero filesystem doesn't
7+
// implement the required interface.
8+
ErrDoesNotImplement = fmt.Errorf("doesn't implement required interface")
9+
// ErrInfoIsNil indicates that a nil os.FileInfo object was provided
10+
ErrInfoIsNil = fmt.Errorf("provided os.Info object was nil")
11+
// ErrInvalidAlgorithm specifies that an unknown algorithm was given for Walk
12+
ErrInvalidAlgorithm = fmt.Errorf("invalid algorithm specified")
13+
// ErrStopWalk indicates to the Walk function that the walk should be aborted
14+
ErrStopWalk = fmt.Errorf("stop filesystem walk")
715
)

file.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package pathlib
22

3-
import "github.com/spf13/afero"
3+
import "github.com/LandonTClipp/afero"
44

55
// File represents a file in the filesystem. It inherits the afero.File interface
66
// but might also include additional functionality.

go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ module github.com/chigopher/pathlib
33
go 1.14
44

55
require (
6+
github.com/LandonTClipp/afero v1.3.6-0.20200907052150-97f9d166c7a3
67
github.com/pkg/errors v0.9.1
7-
github.com/spf13/afero v1.3.2
8+
github.com/rs/zerolog v1.18.0
89
github.com/stretchr/testify v1.6.1
10+
github.com/vektra/mockery/v2 v2.1.0 // indirect
911
)

go.sum

+327
Large diffs are not rendered by default.

mock_WalkFunc_test.go

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mock_namer_test.go

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

path.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"time"
99

1010
"github.com/pkg/errors"
11-
"github.com/spf13/afero"
11+
"github.com/LandonTClipp/afero"
1212
)
1313

1414
// Path is an object that represents a path
@@ -221,11 +221,6 @@ func (p *Path) SafeWriteReader(r io.Reader) error {
221221
return afero.SafeWriteReader(p.Fs(), p.Path(), r)
222222
}
223223

224-
// Walk walks path, using the given filepath.WalkFunc to handle each
225-
func (p *Path) Walk(walkFn filepath.WalkFunc) error {
226-
return afero.Walk(p.Fs(), p.Path(), walkFn)
227-
}
228-
229224
// WriteFile writes the given data to the path (if possible). If the file exists,
230225
// the file is truncated. If the file is a directory, or the path doesn't exist,
231226
// an error is returned.

path_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"testing"
1313
"time"
1414

15-
"github.com/spf13/afero"
15+
"github.com/LandonTClipp/afero"
1616
"github.com/stretchr/testify/assert"
1717
"github.com/stretchr/testify/require"
1818
"github.com/stretchr/testify/suite"

walk.go

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package pathlib
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// WalkOpts is the struct that defines how a walk should be performed
10+
type WalkOpts struct {
11+
// Depth defines how far down a directory we should recurse. A value of -1 means
12+
// infinite depth. 0 means only the direct children of root will be returned, etc.
13+
Depth int
14+
15+
// WalkAlgorithm specifies the algoritm that the Walk() function should use to
16+
// traverse the directory.
17+
WalkAlgorithm string
18+
19+
// FollowSymlinks defines whether symlinks should be dereferenced or not. If True,
20+
// the symlink itself will never be returned to WalkFunc, but rather whatever it
21+
// points to. Warning!!! You are exposing yourself to substantial risk by setting this
22+
// to True. Here be dragons!
23+
FollowSymlinks bool
24+
25+
// Size of the FIFO queue used when doing a breadth-first search
26+
FIFOQueueSize int
27+
}
28+
29+
// DefaultWalkOpts returns the default WalkOpts struct used when
30+
// walking a directory.
31+
func DefaultWalkOpts() *WalkOpts {
32+
return &WalkOpts{
33+
Depth: -1,
34+
WalkAlgorithm: AlgorithmBasic(),
35+
FollowSymlinks: false,
36+
FIFOQueueSize: 100,
37+
}
38+
}
39+
40+
func AlgorithmDepthFirst() string {
41+
return "depth-first"
42+
}
43+
44+
func AlgorithmBasic() string {
45+
return "basic"
46+
}
47+
48+
// Walk is an object that handles walking through a directory tree
49+
type Walk struct {
50+
Opts *WalkOpts
51+
root *Path
52+
}
53+
54+
// NewWalk returns a new Walk struct with default values applied
55+
func NewWalk(root *Path) (*Walk, error) {
56+
return NewWalkWithOpts(root, DefaultWalkOpts())
57+
}
58+
59+
// NewWalkWithOpts returns a Walk object with the given WalkOpts applied
60+
func NewWalkWithOpts(root *Path, opts *WalkOpts) (*Walk, error) {
61+
if root == nil {
62+
return nil, fmt.Errorf("root path can't be nil")
63+
}
64+
if opts == nil {
65+
return nil, fmt.Errorf("opts can't be nil")
66+
}
67+
return &Walk{
68+
Opts: opts,
69+
root: root,
70+
}, nil
71+
}
72+
73+
func (w *Walk) maxDepthReached(currentDepth int) bool {
74+
if w.Opts.Depth >= 0 && currentDepth > w.Opts.Depth {
75+
return true
76+
}
77+
return false
78+
}
79+
80+
type dfsObjectInfo struct {
81+
path *Path
82+
info os.FileInfo
83+
err error
84+
}
85+
86+
func (w *Walk) walkDFS(walkFn WalkFunc, root *Path, currentDepth int) error {
87+
if w.maxDepthReached(currentDepth) {
88+
return nil
89+
}
90+
91+
var nonDirectories []*dfsObjectInfo
92+
93+
if err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
94+
// Since we are doing depth-first, we have to first recurse through all the directories,
95+
// and save all non-directory objects so we can defer handling at a later time.
96+
if IsDir(info) {
97+
if err := w.walkDFS(walkFn, child, currentDepth+1); err != nil {
98+
return err
99+
}
100+
if err := walkFn(child, info, encounteredErr); err != nil {
101+
return err
102+
}
103+
} else {
104+
nonDirectories = append(nonDirectories, &dfsObjectInfo{
105+
path: child,
106+
info: info,
107+
err: encounteredErr,
108+
})
109+
}
110+
return nil
111+
}); err != nil {
112+
return err
113+
}
114+
115+
// Iterate over all non-directory objects
116+
for _, nonDir := range nonDirectories {
117+
if err := walkFn(nonDir.path, nonDir.info, nonDir.err); err != nil {
118+
return err
119+
}
120+
}
121+
return nil
122+
}
123+
124+
// iterateImmediateChildren is a function that handles discovering root's immediate children,
125+
// and will run the algorithm function for every child. The algorithm function is essentially
126+
// what differentiates how each walk behaves, and determines what actions to take given a
127+
// certain child.
128+
func (w *Walk) iterateImmediateChildren(root *Path, algorithmFunction WalkFunc) error {
129+
children, err := root.ReadDir()
130+
if err != nil {
131+
return err
132+
}
133+
134+
var info os.FileInfo
135+
for _, child := range children {
136+
if child.Path() == root.Path() {
137+
continue
138+
}
139+
if w.Opts.FollowSymlinks {
140+
info, err = child.Stat()
141+
isSymlink, err := IsSymlink(info)
142+
if err != nil {
143+
return err
144+
}
145+
if isSymlink {
146+
child, err = child.ResolveAll()
147+
if err != nil {
148+
return err
149+
}
150+
}
151+
152+
} else {
153+
info, _, err = child.Lstat()
154+
}
155+
156+
if info == nil {
157+
if err != nil {
158+
return err
159+
}
160+
return ErrInfoIsNil
161+
}
162+
163+
if algoErr := algorithmFunction(child, info, err); algoErr != nil {
164+
return algoErr
165+
}
166+
}
167+
return nil
168+
}
169+
170+
func (w *Walk) walkBasic(walkFn WalkFunc, root *Path, currentDepth int) error {
171+
if w.maxDepthReached(currentDepth) {
172+
return nil
173+
}
174+
175+
err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
176+
if IsDir(info) {
177+
if err := w.walkBasic(walkFn, child, currentDepth+1); err != nil {
178+
return err
179+
}
180+
}
181+
182+
if err := walkFn(child, info, encounteredErr); err != nil {
183+
return err
184+
}
185+
return nil
186+
})
187+
188+
return err
189+
}
190+
191+
// WalkFunc is the function provided to the Walk function for each directory.
192+
type WalkFunc func(path *Path, info os.FileInfo, err error) error
193+
194+
// Walk walks the directory using the algorithm specified in the configuration.
195+
func (w *Walk) Walk(walkFn WalkFunc) error {
196+
197+
switch w.Opts.WalkAlgorithm {
198+
case AlgorithmBasic():
199+
if err := w.walkBasic(walkFn, w.root, 0); err != nil {
200+
if errors.Is(err, ErrStopWalk) {
201+
return nil
202+
}
203+
return err
204+
}
205+
return nil
206+
case AlgorithmDepthFirst():
207+
if err := w.walkDFS(walkFn, w.root, 0); err != nil {
208+
if errors.Is(err, ErrStopWalk) {
209+
return nil
210+
}
211+
return err
212+
}
213+
return nil
214+
default:
215+
return ErrInvalidAlgorithm
216+
}
217+
}

0 commit comments

Comments
 (0)