Skip to content

Commit

Permalink
Adding walk functionality
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LandonTClipp committed Sep 7, 2020
1 parent 9378098 commit 3c1d997
Show file tree
Hide file tree
Showing 11 changed files with 822 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
quiet: False
all: True
inpackage: True
testonly: True
12 changes: 10 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package pathlib

import "github.com/pkg/errors"
import "fmt"

var (
ErrDoesNotImplement = errors.Errorf("doesn't implement required interface")
// ErrDoesNotImplement indicates that the afero filesystem doesn't
// implement the required interface.
ErrDoesNotImplement = fmt.Errorf("doesn't implement required interface")
// ErrInfoIsNil indicates that a nil os.FileInfo object was provided
ErrInfoIsNil = fmt.Errorf("provided os.Info object was nil")
// ErrInvalidAlgorithm specifies that an unknown algorithm was given for Walk
ErrInvalidAlgorithm = fmt.Errorf("invalid algorithm specified")
// ErrStopWalk indicates to the Walk function that the walk should be aborted
ErrStopWalk = fmt.Errorf("stop filesystem walk")
)
2 changes: 1 addition & 1 deletion file.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package pathlib

import "github.com/spf13/afero"
import "github.com/LandonTClipp/afero"

// File represents a file in the filesystem. It inherits the afero.File interface
// but might also include additional functionality.
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module github.com/chigopher/pathlib
go 1.14

require (
github.com/LandonTClipp/afero v1.3.6-0.20200907052150-97f9d166c7a3
github.com/pkg/errors v0.9.1
github.com/spf13/afero v1.3.2
github.com/rs/zerolog v1.18.0
github.com/stretchr/testify v1.6.1
github.com/vektra/mockery/v2 v2.1.0 // indirect
)
327 changes: 327 additions & 0 deletions go.sum

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions mock_WalkFunc_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions mock_namer_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"time"

"github.com/pkg/errors"
"github.com/spf13/afero"
"github.com/LandonTClipp/afero"
)

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

// Walk walks path, using the given filepath.WalkFunc to handle each
func (p *Path) Walk(walkFn filepath.WalkFunc) error {
return afero.Walk(p.Fs(), p.Path(), walkFn)
}

// WriteFile writes the given data to the path (if possible). If the file exists,
// the file is truncated. If the file is a directory, or the path doesn't exist,
// an error is returned.
Expand Down
2 changes: 1 addition & 1 deletion path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"testing"
"time"

"github.com/spf13/afero"
"github.com/LandonTClipp/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
Expand Down
217 changes: 217 additions & 0 deletions walk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package pathlib

import (
"errors"
"fmt"
"os"
)

// WalkOpts is the struct that defines how a walk should be performed
type WalkOpts struct {
// Depth defines how far down a directory we should recurse. A value of -1 means
// 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
// traverse the directory.
WalkAlgorithm string

// FollowSymlinks defines whether symlinks should be dereferenced or not. If True,
// the symlink itself will never be returned to WalkFunc, but rather whatever it
// points to. Warning!!! You are exposing yourself to substantial risk by setting this
// to True. Here be dragons!
FollowSymlinks bool

// Size of the FIFO queue used when doing a breadth-first search
FIFOQueueSize int
}

// DefaultWalkOpts returns the default WalkOpts struct used when
// walking a directory.
func DefaultWalkOpts() *WalkOpts {
return &WalkOpts{
Depth: -1,
WalkAlgorithm: AlgorithmBasic(),
FollowSymlinks: false,
FIFOQueueSize: 100,
}
}

func AlgorithmDepthFirst() string {
return "depth-first"
}

func AlgorithmBasic() string {
return "basic"
}

// Walk is an object that handles walking through a directory tree
type Walk struct {
Opts *WalkOpts
root *Path
}

// NewWalk returns a new Walk struct with default values applied
func NewWalk(root *Path) (*Walk, error) {
return NewWalkWithOpts(root, DefaultWalkOpts())
}

// NewWalkWithOpts returns a Walk object with the given WalkOpts applied
func NewWalkWithOpts(root *Path, opts *WalkOpts) (*Walk, error) {
if root == nil {
return nil, fmt.Errorf("root path can't be nil")
}
if opts == nil {
return nil, fmt.Errorf("opts can't be nil")
}
return &Walk{
Opts: opts,
root: root,
}, nil
}

func (w *Walk) maxDepthReached(currentDepth int) bool {
if w.Opts.Depth >= 0 && currentDepth > w.Opts.Depth {
return true
}
return false
}

type dfsObjectInfo struct {
path *Path
info os.FileInfo
err error
}

func (w *Walk) walkDFS(walkFn WalkFunc, root *Path, currentDepth int) error {
if w.maxDepthReached(currentDepth) {
return nil
}

var nonDirectories []*dfsObjectInfo

if err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
// Since we are doing depth-first, we have to first recurse through all the directories,
// and save all non-directory objects so we can defer handling at a later time.
if IsDir(info) {
if err := w.walkDFS(walkFn, child, currentDepth+1); err != nil {
return err
}
if err := walkFn(child, info, encounteredErr); err != nil {
return err
}
} else {
nonDirectories = append(nonDirectories, &dfsObjectInfo{
path: child,
info: info,
err: encounteredErr,
})
}
return nil
}); err != nil {
return err
}

// Iterate over all non-directory objects
for _, nonDir := range nonDirectories {
if err := walkFn(nonDir.path, nonDir.info, nonDir.err); err != nil {
return err
}
}
return nil
}

// iterateImmediateChildren is a function that handles discovering root's immediate children,
// and will run the algorithm function for every child. The algorithm function is essentially
// what differentiates how each walk behaves, and determines what actions to take given a
// certain child.
func (w *Walk) iterateImmediateChildren(root *Path, algorithmFunction WalkFunc) error {
children, err := root.ReadDir()
if err != nil {
return err
}

var info os.FileInfo
for _, child := range children {
if child.Path() == root.Path() {
continue
}
if w.Opts.FollowSymlinks {
info, err = child.Stat()
isSymlink, err := IsSymlink(info)
if err != nil {
return err
}
if isSymlink {
child, err = child.ResolveAll()
if err != nil {
return err
}
}

} else {
info, _, err = child.Lstat()
}

if info == nil {
if err != nil {
return err
}
return ErrInfoIsNil
}

if algoErr := algorithmFunction(child, info, err); algoErr != nil {
return algoErr
}
}
return nil
}

func (w *Walk) walkBasic(walkFn WalkFunc, root *Path, currentDepth int) error {
if w.maxDepthReached(currentDepth) {
return nil
}

err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
if IsDir(info) {
if err := w.walkBasic(walkFn, child, currentDepth+1); err != nil {
return err
}
}

if err := walkFn(child, info, encounteredErr); err != nil {
return err
}
return nil
})

return err
}

// WalkFunc is the function provided to the Walk function for each directory.
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():
if err := w.walkBasic(walkFn, w.root, 0); err != nil {
if errors.Is(err, ErrStopWalk) {
return nil
}
return err
}
return nil
case AlgorithmDepthFirst():
if err := w.walkDFS(walkFn, w.root, 0); err != nil {
if errors.Is(err, ErrStopWalk) {
return nil
}
return err
}
return nil
default:
return ErrInvalidAlgorithm
}
}
Loading

0 comments on commit 3c1d997

Please sign in to comment.