github.com/creachadair/ffs@v0.17.3/fpath/fpath.go (about) 1 // Copyright 2019 Michael J. Fromberger. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package fpath implements path traversal relative to a *file.File. A path is 16 // a slash-separated string, which may optionally begin with "/". 17 package fpath 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "path" 24 "strings" 25 26 "github.com/creachadair/ffs/file" 27 ) 28 29 var ( 30 // ErrEmptyPath is reported by Set when given an empty path. 31 ErrEmptyPath = errors.New("empty path") 32 33 // ErrNilFile is reported by Set when passed a nil file. 34 ErrNilFile = errors.New("nil file") 35 36 // ErrSkipChildren signals to the Walk function that the children of the 37 // current node should not be visited. 38 ErrSkipChildren = errors.New("skip child files") 39 ) 40 41 // Open traverses the given slash-separated path sequentially from root, and 42 // returns the resulting file or file.ErrChildNotFound. An empty path yields 43 // root without error. 44 func Open(ctx context.Context, root *file.File, path string) (*file.File, error) { 45 fp, err := findPath(ctx, query{root: root, path: path}) 46 return fp.target, err 47 } 48 49 // OpenPath traverses the given slash-separated path sequentially from root, 50 // and returns a slice of all the files along the path, not including root 51 // itself. If any element of the path does not exist, OpenPath returns the 52 // prefix that was found along with an file.ErrChildNotFound error. 53 func OpenPath(ctx context.Context, root *file.File, path string) ([]*file.File, error) { 54 var out []*file.File 55 cur := root 56 for _, name := range parsePath(path) { 57 c, err := cur.Open(ctx, name) 58 if err != nil { 59 return out, err 60 } 61 out = append(out, c) 62 cur = c 63 } 64 return out, nil 65 } 66 67 // SetOptions control the behaviour of the Set function. A nil *SetOptions 68 // behaves as a zero-valued options structure. 69 type SetOptions struct { 70 // If true, create any path elements that do not exist along the path. 71 Create bool 72 73 // If not nil, this function is called for any intermediate path elements 74 // created along the path. It is also called for the final element if a new 75 // final element is not provided as File. 76 SetStat func(*file.Stat) 77 78 // If not nil, insert this element at the end of the path. If nil, a new 79 // empty file with default options is created. 80 File *file.File 81 } 82 83 func (s *SetOptions) create() bool { return s != nil && s.Create } 84 85 func (s *SetOptions) target() *file.File { 86 if s == nil { 87 return nil 88 } 89 return s.File 90 } 91 92 func (s *SetOptions) setStat(f *file.File) *file.File { 93 if s != nil && s.SetStat != nil { 94 fs := f.Stat() 95 s.SetStat(&fs) 96 fs.Update() 97 } 98 return f 99 } 100 101 // Set traverses the given slash-separated path sequentially from root and 102 // inserts a file at the end of it. An empty path is an error (ErrEmptyPath). 103 // 104 // If opts.Create is true, any missing path entries are created; otherwise it 105 // is an error (file.ErrChildNotFound) if any path element except the last does 106 // not exist. 107 // 108 // If opts.File != nil, that file is inserted at the end of the path; otherwise 109 // if opts.Create is true, a new empty file is inserted. If neither of these is 110 // true, Set reports ErrNilFile. 111 func Set(ctx context.Context, root *file.File, path string, opts *SetOptions) (*file.File, error) { 112 if opts.target() == nil && !opts.create() { 113 return nil, fmt.Errorf("set %q: %w", path, ErrNilFile) 114 } 115 dir, base := "", path 116 if i := strings.LastIndex(path, "/"); i >= 0 { 117 dir, base = path[:i], path[i+1:] 118 } 119 if base == "" { 120 return nil, fmt.Errorf("set %q: %w", path, ErrEmptyPath) 121 } 122 fp, err := findPath(ctx, query{ 123 root: root, 124 path: dir, 125 ef: func(fp *foundPath, err error) error { 126 if errors.Is(err, file.ErrChildNotFound) && opts.create() { 127 c := opts.setStat(fp.target.New(&file.NewOptions{Name: fp.targetName})) 128 fp.target.Child().Set(fp.targetName, c) 129 fp.parent, fp.target = fp.target, c 130 return nil 131 } 132 return err 133 }, 134 }) 135 if err != nil { 136 return nil, err 137 } 138 if last := opts.target(); last != nil { 139 fp.target.Child().Set(base, last) 140 return last, nil 141 } 142 newf := root.New(nil) 143 fp.target.Child().Set(base, opts.setStat(newf)) 144 return newf, nil 145 } 146 147 // Remove removes the file at the given slash-separated path beneath root. If 148 // any component of the path does not exist, it returns file.ErrChildNotFound. 149 func Remove(ctx context.Context, root *file.File, path string) error { 150 fp, err := findPath(ctx, query{root: root, path: path}) 151 if err != nil { 152 return err 153 } else if fp.parent != nil { 154 fp.parent.Child().Remove(fp.targetName) 155 } 156 return nil 157 } 158 159 // An Entry is the argument to the visit callback for the Walk function. 160 type Entry struct { 161 Path string // the path of this entry relative to the root 162 File *file.File // the file for this entry (nil on error) 163 Err error 164 } 165 166 // Walk walks the file tree rooted at root, depth-first, and calls visit with 167 // an entry for each file in the tree. The entry.Path gives the path of the 168 // file relative to the root. If an error occurred opening the file at that 169 // path, entry.File is nil and entry.Err contains the error; otherwise 170 // entry.File contains the file addressed by the path. 171 // 172 // If visit reports an error other than ErrSkipChildren, traversal stops and 173 // that error is returned to the caller of Walk. If it returns ErrSkipChildren 174 // the walk continues but skips the descendant files of the current entry. 175 func Walk(ctx context.Context, root *file.File, visit func(Entry) error) error { 176 q := []string{""} 177 for ctx.Err() == nil && len(q) != 0 { 178 next := q[len(q)-1] 179 q = q[:len(q)-1] 180 181 f, err := Open(ctx, root, next) 182 err = visit(Entry{ 183 Path: next, 184 File: f, 185 Err: err, 186 }) 187 if err == nil { 188 if f == nil { 189 continue // the error was suppressed 190 } 191 kids := f.Child().Names() 192 for i, name := range kids { 193 kids[i] = path.Join(next, name) 194 } 195 for i, j := 0, len(kids)-1; i < j; i++ { 196 kids[i], kids[j] = kids[j], kids[i] 197 j-- 198 } 199 q = append(q, kids...) 200 } else if err != ErrSkipChildren { 201 return err 202 } 203 } 204 return ctx.Err() 205 } 206 207 type errFilter = func(*foundPath, error) error 208 209 func findPath(ctx context.Context, q query) (foundPath, error) { 210 fp := foundPath{ 211 parent: nil, 212 target: q.root, 213 } 214 for _, name := range parsePath(q.path) { 215 fp.targetName = name 216 c, err := fp.target.Open(ctx, name) 217 if err == nil { 218 fp.parent, fp.target = fp.target, c 219 } else if q.ef == nil { 220 return fp, err 221 } else if ferr := q.ef(&fp, err); ferr != nil { 222 return fp, ferr 223 } 224 } 225 return fp, nil 226 } 227 228 type query struct { 229 root *file.File 230 path string 231 ef errFilter 232 } 233 234 type foundPath struct { 235 parent *file.File 236 target *file.File 237 targetName string 238 } 239 240 func parsePath(path string) []string { 241 clean := strings.TrimPrefix(path, "/") 242 if clean == "" || path == "." { 243 return nil 244 } 245 return strings.Split(clean, "/") 246 }