1package fastwalk
2
3import (
4 "io/fs"
5 "os"
6 "path/filepath"
7)
8
9func isDir(path string, d fs.DirEntry) bool {
10 if d.IsDir() {
11 return true
12 }
13 if d.Type()&os.ModeSymlink != 0 {
14 if fi, err := StatDirEntry(path, d); err == nil {
15 return fi.IsDir()
16 }
17 }
18 return false
19}
20
21// IgnoreDuplicateDirs wraps [fs.WalkDirFunc] walkFn to make it follow symbolic
22// links and ignore duplicate directories (if a symlink points to a directory
23// that has already been traversed it is skipped). The walkFn is called for
24// for skipped directories, but the directory is not traversed (this is
25// required for error handling).
26//
27// The Follow [Config] setting has no effect on the behavior of Walk when
28// this wrapper is used.
29//
30// In most use cases, the returned [fs.WalkDirFunc] should not be reused.
31// If it is reused, any previously visited file will be skipped.
32//
33// NOTE: The order of traversal is undefined. Given an "example" directory
34// like the one below where "dir" is a directory and "smydir1" and "smydir2"
35// are links to it, only one of "dir", "smydir1", or "smydir2" will be
36// traversed, but which one is undefined.
37//
38// example
39// ├── dir
40// ├── smydir1 -> dir
41// └── smydir2 -> dir
42func IgnoreDuplicateDirs(walkFn fs.WalkDirFunc) fs.WalkDirFunc {
43 filter := NewEntryFilter()
44 return func(path string, d fs.DirEntry, err error) error {
45 // Call walkFn before checking the entry filter so that we
46 // don't record directories that are skipped with SkipDir.
47 err = walkFn(path, d, err)
48 if err != nil {
49 if err != filepath.SkipDir && isDir(path, d) {
50 filter.Entry(path, d)
51 }
52 return err
53 }
54 if isDir(path, d) {
55 if filter.Entry(path, d) {
56 return filepath.SkipDir
57 }
58 if d.Type() == os.ModeSymlink {
59 return ErrTraverseLink
60 }
61 }
62 return nil
63 }
64}
65
66// IgnoreDuplicateFiles wraps walkFn so that symlinks are followed and duplicate
67// files are ignored. If a symlink resolves to a file that has already been
68// visited it will be skipped.
69//
70// In most use cases, the returned [fs.WalkDirFunc] should not be reused.
71// If it is reused, any previously visited file will be skipped.
72//
73// This can significantly slow Walk as os.Stat() is called for each path
74// (on Windows, os.Stat() is only needed for symlinks).
75func IgnoreDuplicateFiles(walkFn fs.WalkDirFunc) fs.WalkDirFunc {
76 filter := NewEntryFilter()
77 return func(path string, d fs.DirEntry, err error) error {
78 // Skip all duplicate files, directories, and links
79 if filter.Entry(path, d) {
80 if isDir(path, d) {
81 return filepath.SkipDir
82 }
83 return nil
84 }
85 err = walkFn(path, d, err)
86 if err == nil && d.Type() == os.ModeSymlink && isDir(path, d) {
87 err = ErrTraverseLink
88 }
89 return err
90 }
91}
92
93// IgnorePermissionErrors wraps walkFn so that [fs.ErrPermission] permission
94// errors are ignored. The returned [fs.WalkDirFunc] may be reused.
95func IgnorePermissionErrors(walkFn fs.WalkDirFunc) fs.WalkDirFunc {
96 return func(path string, d fs.DirEntry, err error) error {
97 if err != nil && os.IsPermission(err) {
98 return nil
99 }
100 return walkFn(path, d, err)
101 }
102}