Skip to content

Commit

Permalink
Merge pull request #16 from LandonTClipp/walk
Browse files Browse the repository at this point in the history
Adding walk functionality
  • Loading branch information
LandonTClipp authored Sep 7, 2020
2 parents 9378098 + 3c1d997 commit 6ce73bf
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 6ce73bf

Please sign in to comment.