golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/webdav/file.go (about) 1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package webdav 6 7 import ( 8 "context" 9 "encoding/xml" 10 "io" 11 "net/http" 12 "os" 13 "path" 14 "path/filepath" 15 "runtime" 16 "strings" 17 "sync" 18 "time" 19 ) 20 21 // slashClean is equivalent to but slightly more efficient than 22 // path.Clean("/" + name). 23 func slashClean(name string) string { 24 if name == "" || name[0] != '/' { 25 name = "/" + name 26 } 27 return path.Clean(name) 28 } 29 30 // A FileSystem implements access to a collection of named files. The elements 31 // in a file path are separated by slash ('/', U+002F) characters, regardless 32 // of host operating system convention. 33 // 34 // Each method has the same semantics as the os package's function of the same 35 // name. 36 // 37 // Note that the os.Rename documentation says that "OS-specific restrictions 38 // might apply". In particular, whether or not renaming a file or directory 39 // overwriting another existing file or directory is an error is OS-dependent. 40 type FileSystem interface { 41 Mkdir(ctx context.Context, name string, perm os.FileMode) error 42 OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) 43 RemoveAll(ctx context.Context, name string) error 44 Rename(ctx context.Context, oldName, newName string) error 45 Stat(ctx context.Context, name string) (os.FileInfo, error) 46 } 47 48 // A File is returned by a FileSystem's OpenFile method and can be served by a 49 // Handler. 50 // 51 // A File may optionally implement the DeadPropsHolder interface, if it can 52 // load and save dead properties. 53 type File interface { 54 http.File 55 io.Writer 56 } 57 58 // A Dir implements FileSystem using the native file system restricted to a 59 // specific directory tree. 60 // 61 // While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's 62 // string value is a filename on the native file system, not a URL, so it is 63 // separated by filepath.Separator, which isn't necessarily '/'. 64 // 65 // An empty Dir is treated as ".". 66 type Dir string 67 68 func (d Dir) resolve(name string) string { 69 // This implementation is based on Dir.Open's code in the standard net/http package. 70 if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 || 71 strings.Contains(name, "\x00") { 72 return "" 73 } 74 dir := string(d) 75 if dir == "" { 76 dir = "." 77 } 78 return filepath.Join(dir, filepath.FromSlash(slashClean(name))) 79 } 80 81 func (d Dir) Mkdir(ctx context.Context, name string, perm os.FileMode) error { 82 if name = d.resolve(name); name == "" { 83 return os.ErrNotExist 84 } 85 return os.Mkdir(name, perm) 86 } 87 88 func (d Dir) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) { 89 if name = d.resolve(name); name == "" { 90 return nil, os.ErrNotExist 91 } 92 f, err := os.OpenFile(name, flag, perm) 93 if err != nil { 94 return nil, err 95 } 96 return f, nil 97 } 98 99 func (d Dir) RemoveAll(ctx context.Context, name string) error { 100 if name = d.resolve(name); name == "" { 101 return os.ErrNotExist 102 } 103 if name == filepath.Clean(string(d)) { 104 // Prohibit removing the virtual root directory. 105 return os.ErrInvalid 106 } 107 return os.RemoveAll(name) 108 } 109 110 func (d Dir) Rename(ctx context.Context, oldName, newName string) error { 111 if oldName = d.resolve(oldName); oldName == "" { 112 return os.ErrNotExist 113 } 114 if newName = d.resolve(newName); newName == "" { 115 return os.ErrNotExist 116 } 117 if root := filepath.Clean(string(d)); root == oldName || root == newName { 118 // Prohibit renaming from or to the virtual root directory. 119 return os.ErrInvalid 120 } 121 return os.Rename(oldName, newName) 122 } 123 124 func (d Dir) Stat(ctx context.Context, name string) (os.FileInfo, error) { 125 if name = d.resolve(name); name == "" { 126 return nil, os.ErrNotExist 127 } 128 return os.Stat(name) 129 } 130 131 // NewMemFS returns a new in-memory FileSystem implementation. 132 func NewMemFS() FileSystem { 133 return &memFS{ 134 root: memFSNode{ 135 children: make(map[string]*memFSNode), 136 mode: 0660 | os.ModeDir, 137 modTime: time.Now(), 138 }, 139 } 140 } 141 142 // A memFS implements FileSystem, storing all metadata and actual file data 143 // in-memory. No limits on filesystem size are used, so it is not recommended 144 // this be used where the clients are untrusted. 145 // 146 // Concurrent access is permitted. The tree structure is protected by a mutex, 147 // and each node's contents and metadata are protected by a per-node mutex. 148 // 149 // TODO: Enforce file permissions. 150 type memFS struct { 151 mu sync.Mutex 152 root memFSNode 153 } 154 155 // TODO: clean up and rationalize the walk/find code. 156 157 // walk walks the directory tree for the fullname, calling f at each step. If f 158 // returns an error, the walk will be aborted and return that same error. 159 // 160 // dir is the directory at that step, frag is the name fragment, and final is 161 // whether it is the final step. For example, walking "/foo/bar/x" will result 162 // in 3 calls to f: 163 // - "/", "foo", false 164 // - "/foo/", "bar", false 165 // - "/foo/bar/", "x", true 166 // 167 // The frag argument will be empty only if dir is the root node and the walk 168 // ends at that root node. 169 func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, final bool) error) error { 170 original := fullname 171 fullname = slashClean(fullname) 172 173 // Strip any leading "/"s to make fullname a relative path, as the walk 174 // starts at fs.root. 175 if fullname[0] == '/' { 176 fullname = fullname[1:] 177 } 178 dir := &fs.root 179 180 for { 181 frag, remaining := fullname, "" 182 i := strings.IndexRune(fullname, '/') 183 final := i < 0 184 if !final { 185 frag, remaining = fullname[:i], fullname[i+1:] 186 } 187 if frag == "" && dir != &fs.root { 188 panic("webdav: empty path fragment for a clean path") 189 } 190 if err := f(dir, frag, final); err != nil { 191 return &os.PathError{ 192 Op: op, 193 Path: original, 194 Err: err, 195 } 196 } 197 if final { 198 break 199 } 200 child := dir.children[frag] 201 if child == nil { 202 return &os.PathError{ 203 Op: op, 204 Path: original, 205 Err: os.ErrNotExist, 206 } 207 } 208 if !child.mode.IsDir() { 209 return &os.PathError{ 210 Op: op, 211 Path: original, 212 Err: os.ErrInvalid, 213 } 214 } 215 dir, fullname = child, remaining 216 } 217 return nil 218 } 219 220 // find returns the parent of the named node and the relative name fragment 221 // from the parent to the child. For example, if finding "/foo/bar/baz" then 222 // parent will be the node for "/foo/bar" and frag will be "baz". 223 // 224 // If the fullname names the root node, then parent, frag and err will be zero. 225 // 226 // find returns an error if the parent does not already exist or the parent 227 // isn't a directory, but it will not return an error per se if the child does 228 // not already exist. The error returned is either nil or an *os.PathError 229 // whose Op is op. 230 func (fs *memFS) find(op, fullname string) (parent *memFSNode, frag string, err error) { 231 err = fs.walk(op, fullname, func(parent0 *memFSNode, frag0 string, final bool) error { 232 if !final { 233 return nil 234 } 235 if frag0 != "" { 236 parent, frag = parent0, frag0 237 } 238 return nil 239 }) 240 return parent, frag, err 241 } 242 243 func (fs *memFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error { 244 fs.mu.Lock() 245 defer fs.mu.Unlock() 246 247 dir, frag, err := fs.find("mkdir", name) 248 if err != nil { 249 return err 250 } 251 if dir == nil { 252 // We can't create the root. 253 return os.ErrInvalid 254 } 255 if _, ok := dir.children[frag]; ok { 256 return os.ErrExist 257 } 258 dir.children[frag] = &memFSNode{ 259 children: make(map[string]*memFSNode), 260 mode: perm.Perm() | os.ModeDir, 261 modTime: time.Now(), 262 } 263 return nil 264 } 265 266 func (fs *memFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) { 267 fs.mu.Lock() 268 defer fs.mu.Unlock() 269 270 dir, frag, err := fs.find("open", name) 271 if err != nil { 272 return nil, err 273 } 274 var n *memFSNode 275 if dir == nil { 276 // We're opening the root. 277 if runtime.GOOS == "zos" { 278 if flag&os.O_WRONLY != 0 { 279 return nil, os.ErrPermission 280 } 281 } else { 282 if flag&(os.O_WRONLY|os.O_RDWR) != 0 { 283 return nil, os.ErrPermission 284 } 285 } 286 n, frag = &fs.root, "/" 287 288 } else { 289 n = dir.children[frag] 290 if flag&(os.O_SYNC|os.O_APPEND) != 0 { 291 // memFile doesn't support these flags yet. 292 return nil, os.ErrInvalid 293 } 294 if flag&os.O_CREATE != 0 { 295 if flag&os.O_EXCL != 0 && n != nil { 296 return nil, os.ErrExist 297 } 298 if n == nil { 299 n = &memFSNode{ 300 mode: perm.Perm(), 301 } 302 dir.children[frag] = n 303 } 304 } 305 if n == nil { 306 return nil, os.ErrNotExist 307 } 308 if flag&(os.O_WRONLY|os.O_RDWR) != 0 && flag&os.O_TRUNC != 0 { 309 n.mu.Lock() 310 n.data = nil 311 n.mu.Unlock() 312 } 313 } 314 315 children := make([]os.FileInfo, 0, len(n.children)) 316 for cName, c := range n.children { 317 children = append(children, c.stat(cName)) 318 } 319 return &memFile{ 320 n: n, 321 nameSnapshot: frag, 322 childrenSnapshot: children, 323 }, nil 324 } 325 326 func (fs *memFS) RemoveAll(ctx context.Context, name string) error { 327 fs.mu.Lock() 328 defer fs.mu.Unlock() 329 330 dir, frag, err := fs.find("remove", name) 331 if err != nil { 332 return err 333 } 334 if dir == nil { 335 // We can't remove the root. 336 return os.ErrInvalid 337 } 338 delete(dir.children, frag) 339 return nil 340 } 341 342 func (fs *memFS) Rename(ctx context.Context, oldName, newName string) error { 343 fs.mu.Lock() 344 defer fs.mu.Unlock() 345 346 oldName = slashClean(oldName) 347 newName = slashClean(newName) 348 if oldName == newName { 349 return nil 350 } 351 if strings.HasPrefix(newName, oldName+"/") { 352 // We can't rename oldName to be a sub-directory of itself. 353 return os.ErrInvalid 354 } 355 356 oDir, oFrag, err := fs.find("rename", oldName) 357 if err != nil { 358 return err 359 } 360 if oDir == nil { 361 // We can't rename from the root. 362 return os.ErrInvalid 363 } 364 365 nDir, nFrag, err := fs.find("rename", newName) 366 if err != nil { 367 return err 368 } 369 if nDir == nil { 370 // We can't rename to the root. 371 return os.ErrInvalid 372 } 373 374 oNode, ok := oDir.children[oFrag] 375 if !ok { 376 return os.ErrNotExist 377 } 378 if oNode.children != nil { 379 if nNode, ok := nDir.children[nFrag]; ok { 380 if nNode.children == nil { 381 return errNotADirectory 382 } 383 if len(nNode.children) != 0 { 384 return errDirectoryNotEmpty 385 } 386 } 387 } 388 delete(oDir.children, oFrag) 389 nDir.children[nFrag] = oNode 390 return nil 391 } 392 393 func (fs *memFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { 394 fs.mu.Lock() 395 defer fs.mu.Unlock() 396 397 dir, frag, err := fs.find("stat", name) 398 if err != nil { 399 return nil, err 400 } 401 if dir == nil { 402 // We're stat'ting the root. 403 return fs.root.stat("/"), nil 404 } 405 if n, ok := dir.children[frag]; ok { 406 return n.stat(path.Base(name)), nil 407 } 408 return nil, os.ErrNotExist 409 } 410 411 // A memFSNode represents a single entry in the in-memory filesystem and also 412 // implements os.FileInfo. 413 type memFSNode struct { 414 // children is protected by memFS.mu. 415 children map[string]*memFSNode 416 417 mu sync.Mutex 418 data []byte 419 mode os.FileMode 420 modTime time.Time 421 deadProps map[xml.Name]Property 422 } 423 424 func (n *memFSNode) stat(name string) *memFileInfo { 425 n.mu.Lock() 426 defer n.mu.Unlock() 427 return &memFileInfo{ 428 name: name, 429 size: int64(len(n.data)), 430 mode: n.mode, 431 modTime: n.modTime, 432 } 433 } 434 435 func (n *memFSNode) DeadProps() (map[xml.Name]Property, error) { 436 n.mu.Lock() 437 defer n.mu.Unlock() 438 if len(n.deadProps) == 0 { 439 return nil, nil 440 } 441 ret := make(map[xml.Name]Property, len(n.deadProps)) 442 for k, v := range n.deadProps { 443 ret[k] = v 444 } 445 return ret, nil 446 } 447 448 func (n *memFSNode) Patch(patches []Proppatch) ([]Propstat, error) { 449 n.mu.Lock() 450 defer n.mu.Unlock() 451 pstat := Propstat{Status: http.StatusOK} 452 for _, patch := range patches { 453 for _, p := range patch.Props { 454 pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName}) 455 if patch.Remove { 456 delete(n.deadProps, p.XMLName) 457 continue 458 } 459 if n.deadProps == nil { 460 n.deadProps = map[xml.Name]Property{} 461 } 462 n.deadProps[p.XMLName] = p 463 } 464 } 465 return []Propstat{pstat}, nil 466 } 467 468 type memFileInfo struct { 469 name string 470 size int64 471 mode os.FileMode 472 modTime time.Time 473 } 474 475 func (f *memFileInfo) Name() string { return f.name } 476 func (f *memFileInfo) Size() int64 { return f.size } 477 func (f *memFileInfo) Mode() os.FileMode { return f.mode } 478 func (f *memFileInfo) ModTime() time.Time { return f.modTime } 479 func (f *memFileInfo) IsDir() bool { return f.mode.IsDir() } 480 func (f *memFileInfo) Sys() interface{} { return nil } 481 482 // A memFile is a File implementation for a memFSNode. It is a per-file (not 483 // per-node) read/write position, and a snapshot of the memFS' tree structure 484 // (a node's name and children) for that node. 485 type memFile struct { 486 n *memFSNode 487 nameSnapshot string 488 childrenSnapshot []os.FileInfo 489 // pos is protected by n.mu. 490 pos int 491 } 492 493 // A *memFile implements the optional DeadPropsHolder interface. 494 var _ DeadPropsHolder = (*memFile)(nil) 495 496 func (f *memFile) DeadProps() (map[xml.Name]Property, error) { return f.n.DeadProps() } 497 func (f *memFile) Patch(patches []Proppatch) ([]Propstat, error) { return f.n.Patch(patches) } 498 499 func (f *memFile) Close() error { 500 return nil 501 } 502 503 func (f *memFile) Read(p []byte) (int, error) { 504 f.n.mu.Lock() 505 defer f.n.mu.Unlock() 506 if f.n.mode.IsDir() { 507 return 0, os.ErrInvalid 508 } 509 if f.pos >= len(f.n.data) { 510 return 0, io.EOF 511 } 512 n := copy(p, f.n.data[f.pos:]) 513 f.pos += n 514 return n, nil 515 } 516 517 func (f *memFile) Readdir(count int) ([]os.FileInfo, error) { 518 f.n.mu.Lock() 519 defer f.n.mu.Unlock() 520 if !f.n.mode.IsDir() { 521 return nil, os.ErrInvalid 522 } 523 old := f.pos 524 if old >= len(f.childrenSnapshot) { 525 // The os.File Readdir docs say that at the end of a directory, 526 // the error is io.EOF if count > 0 and nil if count <= 0. 527 if count > 0 { 528 return nil, io.EOF 529 } 530 return nil, nil 531 } 532 if count > 0 { 533 f.pos += count 534 if f.pos > len(f.childrenSnapshot) { 535 f.pos = len(f.childrenSnapshot) 536 } 537 } else { 538 f.pos = len(f.childrenSnapshot) 539 old = 0 540 } 541 return f.childrenSnapshot[old:f.pos], nil 542 } 543 544 func (f *memFile) Seek(offset int64, whence int) (int64, error) { 545 f.n.mu.Lock() 546 defer f.n.mu.Unlock() 547 npos := f.pos 548 // TODO: How to handle offsets greater than the size of system int? 549 switch whence { 550 case io.SeekStart: 551 npos = int(offset) 552 case io.SeekCurrent: 553 npos += int(offset) 554 case io.SeekEnd: 555 npos = len(f.n.data) + int(offset) 556 default: 557 npos = -1 558 } 559 if npos < 0 { 560 return 0, os.ErrInvalid 561 } 562 f.pos = npos 563 return int64(f.pos), nil 564 } 565 566 func (f *memFile) Stat() (os.FileInfo, error) { 567 return f.n.stat(f.nameSnapshot), nil 568 } 569 570 func (f *memFile) Write(p []byte) (int, error) { 571 lenp := len(p) 572 f.n.mu.Lock() 573 defer f.n.mu.Unlock() 574 575 if f.n.mode.IsDir() { 576 return 0, os.ErrInvalid 577 } 578 if f.pos < len(f.n.data) { 579 n := copy(f.n.data[f.pos:], p) 580 f.pos += n 581 p = p[n:] 582 } else if f.pos > len(f.n.data) { 583 // Write permits the creation of holes, if we've seek'ed past the 584 // existing end of file. 585 if f.pos <= cap(f.n.data) { 586 oldLen := len(f.n.data) 587 f.n.data = f.n.data[:f.pos] 588 hole := f.n.data[oldLen:] 589 for i := range hole { 590 hole[i] = 0 591 } 592 } else { 593 d := make([]byte, f.pos, f.pos+len(p)) 594 copy(d, f.n.data) 595 f.n.data = d 596 } 597 } 598 599 if len(p) > 0 { 600 // We should only get here if f.pos == len(f.n.data). 601 f.n.data = append(f.n.data, p...) 602 f.pos = len(f.n.data) 603 } 604 f.n.modTime = time.Now() 605 return lenp, nil 606 } 607 608 // moveFiles moves files and/or directories from src to dst. 609 // 610 // See section 9.9.4 for when various HTTP status codes apply. 611 func moveFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bool) (status int, err error) { 612 created := false 613 if _, err := fs.Stat(ctx, dst); err != nil { 614 if !os.IsNotExist(err) { 615 return http.StatusForbidden, err 616 } 617 created = true 618 } else if overwrite { 619 // Section 9.9.3 says that "If a resource exists at the destination 620 // and the Overwrite header is "T", then prior to performing the move, 621 // the server must perform a DELETE with "Depth: infinity" on the 622 // destination resource. 623 if err := fs.RemoveAll(ctx, dst); err != nil { 624 return http.StatusForbidden, err 625 } 626 } else { 627 return http.StatusPreconditionFailed, os.ErrExist 628 } 629 if err := fs.Rename(ctx, src, dst); err != nil { 630 return http.StatusForbidden, err 631 } 632 if created { 633 return http.StatusCreated, nil 634 } 635 return http.StatusNoContent, nil 636 } 637 638 func copyProps(dst, src File) error { 639 d, ok := dst.(DeadPropsHolder) 640 if !ok { 641 return nil 642 } 643 s, ok := src.(DeadPropsHolder) 644 if !ok { 645 return nil 646 } 647 m, err := s.DeadProps() 648 if err != nil { 649 return err 650 } 651 props := make([]Property, 0, len(m)) 652 for _, prop := range m { 653 props = append(props, prop) 654 } 655 _, err = d.Patch([]Proppatch{{Props: props}}) 656 return err 657 } 658 659 // copyFiles copies files and/or directories from src to dst. 660 // 661 // See section 9.8.5 for when various HTTP status codes apply. 662 func copyFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bool, depth int, recursion int) (status int, err error) { 663 if recursion == 1000 { 664 return http.StatusInternalServerError, errRecursionTooDeep 665 } 666 recursion++ 667 668 // TODO: section 9.8.3 says that "Note that an infinite-depth COPY of /A/ 669 // into /A/B/ could lead to infinite recursion if not handled correctly." 670 671 srcFile, err := fs.OpenFile(ctx, src, os.O_RDONLY, 0) 672 if err != nil { 673 if os.IsNotExist(err) { 674 return http.StatusNotFound, err 675 } 676 return http.StatusInternalServerError, err 677 } 678 defer srcFile.Close() 679 srcStat, err := srcFile.Stat() 680 if err != nil { 681 if os.IsNotExist(err) { 682 return http.StatusNotFound, err 683 } 684 return http.StatusInternalServerError, err 685 } 686 srcPerm := srcStat.Mode() & os.ModePerm 687 688 created := false 689 if _, err := fs.Stat(ctx, dst); err != nil { 690 if os.IsNotExist(err) { 691 created = true 692 } else { 693 return http.StatusForbidden, err 694 } 695 } else { 696 if !overwrite { 697 return http.StatusPreconditionFailed, os.ErrExist 698 } 699 if err := fs.RemoveAll(ctx, dst); err != nil && !os.IsNotExist(err) { 700 return http.StatusForbidden, err 701 } 702 } 703 704 if srcStat.IsDir() { 705 if err := fs.Mkdir(ctx, dst, srcPerm); err != nil { 706 return http.StatusForbidden, err 707 } 708 if depth == infiniteDepth { 709 children, err := srcFile.Readdir(-1) 710 if err != nil { 711 return http.StatusForbidden, err 712 } 713 for _, c := range children { 714 name := c.Name() 715 s := path.Join(src, name) 716 d := path.Join(dst, name) 717 cStatus, cErr := copyFiles(ctx, fs, s, d, overwrite, depth, recursion) 718 if cErr != nil { 719 // TODO: MultiStatus. 720 return cStatus, cErr 721 } 722 } 723 } 724 725 } else { 726 dstFile, err := fs.OpenFile(ctx, dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcPerm) 727 if err != nil { 728 if os.IsNotExist(err) { 729 return http.StatusConflict, err 730 } 731 return http.StatusForbidden, err 732 733 } 734 _, copyErr := io.Copy(dstFile, srcFile) 735 propsErr := copyProps(dstFile, srcFile) 736 closeErr := dstFile.Close() 737 if copyErr != nil { 738 return http.StatusInternalServerError, copyErr 739 } 740 if propsErr != nil { 741 return http.StatusInternalServerError, propsErr 742 } 743 if closeErr != nil { 744 return http.StatusInternalServerError, closeErr 745 } 746 } 747 748 if created { 749 return http.StatusCreated, nil 750 } 751 return http.StatusNoContent, nil 752 } 753 754 // walkFS traverses filesystem fs starting at name up to depth levels. 755 // 756 // Allowed values for depth are 0, 1 or infiniteDepth. For each visited node, 757 // walkFS calls walkFn. If a visited file system node is a directory and 758 // walkFn returns filepath.SkipDir, walkFS will skip traversal of this node. 759 func walkFS(ctx context.Context, fs FileSystem, depth int, name string, info os.FileInfo, walkFn filepath.WalkFunc) error { 760 // This implementation is based on Walk's code in the standard path/filepath package. 761 err := walkFn(name, info, nil) 762 if err != nil { 763 if info.IsDir() && err == filepath.SkipDir { 764 return nil 765 } 766 return err 767 } 768 if !info.IsDir() || depth == 0 { 769 return nil 770 } 771 if depth == 1 { 772 depth = 0 773 } 774 775 // Read directory names. 776 f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0) 777 if err != nil { 778 return walkFn(name, info, err) 779 } 780 fileInfos, err := f.Readdir(0) 781 f.Close() 782 if err != nil { 783 return walkFn(name, info, err) 784 } 785 786 for _, fileInfo := range fileInfos { 787 filename := path.Join(name, fileInfo.Name()) 788 fileInfo, err := fs.Stat(ctx, filename) 789 if err != nil { 790 if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { 791 return err 792 } 793 } else { 794 err = walkFS(ctx, fs, depth, filename, fileInfo, walkFn) 795 if err != nil { 796 if !fileInfo.IsDir() || err != filepath.SkipDir { 797 return err 798 } 799 } 800 } 801 } 802 return nil 803 }