github.com/creachadair/ffs@v0.17.3/fpath/fs.go (about) 1 // Copyright 2021 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 16 17 import ( 18 "context" 19 "errors" 20 "io/fs" 21 slashpath "path" 22 23 "github.com/creachadair/ffs/file" 24 ) 25 26 func pathErr(op, path string, err error) error { 27 return &fs.PathError{Op: op, Path: path, Err: err} 28 } 29 30 // FS implements the standard library fs.FS, fs.StatFS, fs.SubFS, and 31 // fs.ReadDirFS interfaces. Path traversal is rooted at a file assigned when 32 // the FS is created. 33 type FS struct { 34 ctx context.Context 35 root *file.File 36 } 37 38 // NewFS constructs an FS rooted at the given file. The context is held by the 39 // FS and must be valid for the full extent of its use. 40 func NewFS(ctx context.Context, root *file.File) FS { 41 return FS{ctx: ctx, root: root} 42 } 43 44 // Open implements the fs.FS interface. The concrete type of the file returned 45 // by a successful Open call is *file.Cursor. 46 func (fp FS) Open(path string) (fs.File, error) { 47 target, err := fp.openFile("open", path) 48 if err != nil { 49 return nil, err 50 } 51 return target.Cursor(fp.ctx), nil 52 } 53 54 // Stat implements the fs.StatFS interface. The concrete type of the info 55 // record returned by a successful Stat call is file.FileInfo. 56 func (fp FS) Stat(path string) (fs.FileInfo, error) { 57 target, err := fp.openFile("stat", path) 58 if err != nil { 59 return nil, err 60 } 61 return target.FileInfo(), nil 62 } 63 64 // Sub implements the fs.SubFS interface. 65 func (fp FS) Sub(dir string) (fs.FS, error) { 66 target, err := fp.openFile("sub", dir) 67 if err != nil { 68 return nil, err 69 } 70 return NewFS(fp.ctx, target), nil 71 } 72 73 // ReadDir implements the fs.ReadDirFS interface. 74 func (fp FS) ReadDir(path string) ([]fs.DirEntry, error) { 75 target, err := fp.openFile("readdir", path) 76 if err != nil { 77 return nil, err 78 } 79 kids := target.Child() 80 out := make([]fs.DirEntry, kids.Len()) 81 for i, name := range kids.Names() { 82 kid, err := target.Open(fp.ctx, name) 83 if err != nil { 84 return nil, pathErr("readdir", slashpath.Join(path, name), err) 85 } 86 out[i] = fs.FileInfoToDirEntry(kid.FileInfo()) 87 } 88 return out, nil 89 } 90 91 func (fp FS) openFile(op, path string) (*file.File, error) { 92 if !fs.ValidPath(path) { 93 return nil, pathErr(op, path, fs.ErrInvalid) 94 } 95 target, err := Open(fp.ctx, fp.root, path) 96 if err == nil { 97 return target, nil 98 } else if errors.Is(err, file.ErrChildNotFound) { 99 return nil, pathErr(op, path, fs.ErrNotExist) 100 } 101 return nil, pathErr(op, path, err) 102 }