github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/pkg/filepathutil/list.go (about) 1 package filepathutil 2 3 import ( 4 "fmt" 5 "io/fs" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/benchkram/bob/pkg/filepathxx" 11 "github.com/logrusorgru/aurora" 12 ) 13 14 // DefaultIgnores 15 var ( 16 DefaultIgnores = map[string]bool{ 17 "node_modules": true, 18 ".git": true, 19 } 20 ) 21 22 //var listRecursiveCache = make(map[string][]string, 1024) 23 24 func ClearListRecursiveCache() { 25 // listRecursiveCache = make(map[string][]string, 1024) 26 } 27 28 // ListRecursive lists all files relative to input. It ignores symbolic links 29 // which are not inside the projectRoot. 30 func ListRecursive(inp string, projectRoot string) (all []string, err error) { 31 // if result, ok := listRecursiveCache[inp]; ok { 32 // return result, nil 33 // } 34 35 // FIXME: when "*" is passed as input it's likely to hit the cache 36 // as there is no further information. Think how to handle the cache correctly 37 // in those cases. For now the cache is disabled! 38 39 // FIXME: new list recursive 40 // * does input contain a glob? see https://pkg.go.dev/path/filepath#Match => read with filepathx.Glob 41 // * check if input is a file => add file 42 // * check if input is a dir => add files in dir recursively 43 // 44 // More input: 45 // https://github.com/iriri/minimal/blob/9b2348d09c1ab2c25505f9933a3591ef9db6522a/gitignore/gitignore.go#L245 46 // https://github.com/zabawaba99/go-gitignore/ 47 // https://github.com/gobwas/glob 48 // 49 // Thoughts: Is it possible to compile a ignoreList upfront? 50 // Then check if the accessed file || dir can be skipped. 51 // Maybe it's even possible to call skipdir on a walk func. 52 53 // symLinkError are gathered here and printed at the end of 54 // the function to stdout. 55 symlinkErrors := []error{} 56 57 // FIXME: possibly ignore here too, before calling listDir 58 if s, err := os.Lstat(inp); err != nil || !s.IsDir() { 59 // File 60 61 // Use glob for unknowns (wildcard-paths) and existing files (non-dirs) 62 matches, err := filepathxx.Glob(inp) 63 if err != nil { 64 return nil, fmt.Errorf("failed to glob %q: %w", inp, err) 65 } 66 67 for _, m := range matches { 68 s, err := os.Lstat(m) 69 if err == nil && !s.IsDir() { 70 isValid, err := isValidFile(m, s, projectRoot) 71 if err != nil { 72 symlinkErrors = append(symlinkErrors, err) 73 } 74 75 if !isValid { 76 continue 77 } 78 79 // Existing file 80 all = append(all, m) 81 } else { 82 // Directory 83 files, symErrors, err := listDir(m, projectRoot) 84 if err != nil { 85 return nil, fmt.Errorf("failed to list dir: %w", err) 86 } 87 symlinkErrors = append(symlinkErrors, symErrors...) 88 all = append(all, files...) 89 } 90 } 91 } else { 92 // Directory 93 files, symErrors, err := listDir(inp, projectRoot) 94 if err != nil { 95 return nil, fmt.Errorf("failed to list dir: %w", err) 96 } 97 symlinkErrors = append(symlinkErrors, symErrors...) 98 all = append(all, files...) 99 } 100 101 for i, sErr := range symlinkErrors { 102 fmt.Println(fmt.Sprintf("%s", aurora.Red("Warning: ")) + sErr.Error()) 103 if i > 10 { 104 break 105 } 106 } 107 108 // listRecursiveMap[inp] = all 109 return all, nil 110 } 111 112 func listDir(path string, projectRoot string) (all []string, symlinkErrors []error, _ error) { 113 114 symlinkErrors = []error{} 115 all = []string{} 116 if err := filepath.WalkDir(path, func(p string, fi fs.DirEntry, err error) error { 117 if err != nil { 118 return err 119 } 120 121 // Skip default ignored 122 if fi.IsDir() && ignored(fi.Name()) { 123 return fs.SkipDir 124 } 125 126 // Append file 127 if fi.IsDir() { 128 return nil 129 } 130 131 fileInfo, err := fi.Info() 132 if err != nil { 133 return err 134 } 135 136 isValid, err := isValidFile(p, fileInfo, projectRoot) 137 if err != nil { 138 symlinkErrors = append(symlinkErrors, err) 139 } 140 141 if isValid { 142 all = append(all, p) 143 } 144 145 return nil 146 }); err != nil { 147 return nil, nil, fmt.Errorf("failed to walk dir %q: %w", path, err) 148 } 149 150 return all, symlinkErrors, nil 151 } 152 153 // isValidFile returns true if a symlink resolves succesfully into a path relative to projectRoot. 154 // It also returns true if the file is a regular file or directory. 155 // 156 // The returned error contains a "failed to follow symlink" hint and should 157 // be presented to the user. 158 func isValidFile(path string, info fs.FileInfo, projectRoot string) (bool, error) { 159 if info.Mode()&os.ModeSymlink != 0 { 160 sym, err := filepath.EvalSymlinks(path) 161 if err != nil { 162 return false, fmt.Errorf("failed to follow symlink %q: %w", path, err) 163 } 164 165 if strings.HasPrefix(sym, "/") { 166 return false, fmt.Errorf("symbolic link [%s] points to a location [%s] outside of the project [%s]", path, sym, projectRoot) 167 } 168 169 absSym, err := filepath.Abs(sym) 170 if err != nil { 171 return false, err 172 } 173 if !strings.HasPrefix(absSym, projectRoot) { 174 return false, fmt.Errorf("symbolic link [%s] points to a location [%s] outside of the project [%s]", path, sym, projectRoot) 175 } 176 177 // the symlink itself should not appear in the input list 178 // only the resolved path should be added. 179 return false, nil 180 } 181 182 return true, nil 183 } 184 185 func ignored(fileName string) bool { 186 return DefaultIgnores[fileName] 187 }