github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/file/gfilefs/gfilefs.go (about) 1 // Copyright 2022 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 package gfilefs 6 7 import ( 8 "context" 9 "os" 10 "sync/atomic" 11 "time" 12 13 "github.com/Schaudge/grailbase/errors" 14 "github.com/Schaudge/grailbase/file" 15 "github.com/Schaudge/grailbase/file/fsnode" 16 "github.com/Schaudge/grailbase/grail/biofs/biofseventlog" 17 "github.com/Schaudge/grailbase/ioctx/fsctx" 18 "github.com/Schaudge/grailbase/log" 19 "github.com/Schaudge/grailbase/vcontext" 20 v23context "v.io/v23/context" 21 ) 22 23 // New returns a new parent node rooted at root. root must be a directory path 24 // that can be handled by github.com/Schaudge/grailbase/file. name will become the 25 // name of the returned node. 26 func New(root, name string) fsnode.Parent { 27 return newDirNode(root, name) 28 } 29 30 const fileInfoCacheFor = 1 * time.Hour 31 32 func newDirNode(path, name string) fsnode.Parent { 33 return dirNode{ 34 FileInfo: fsnode.NewDirInfo(name). 35 WithModePerm(0777). 36 WithCacheableFor(fileInfoCacheFor). 37 // TODO: Remove after updating fragments to support fsnode.T directly. 38 WithSys(path), 39 path: path, 40 } 41 } 42 43 // dirNode implements fsnode.Parent and represents a directory. 44 type dirNode struct { 45 fsnode.ParentReadOnly 46 fsnode.FileInfo 47 path string 48 } 49 50 var ( 51 _ fsnode.Parent = dirNode{} 52 _ fsnode.Cacheable = dirNode{} 53 ) 54 55 // Child implements fsnode.Parent. 56 func (d dirNode) Child(ctx context.Context, name string) (fsnode.T, error) { 57 log.Debug.Printf("gfilefs child name=%s", name) 58 biofseventlog.UsedFeature("gfilefs.dir.child") 59 var ( 60 path = file.Join(d.path, name) 61 child fsnode.T 62 ) 63 vctx := v23context.FromGoContextWithValues(ctx, vcontext.Background()) 64 lister := file.List(vctx, path, true /* recursive */) 65 // Look for either a file or a directory at this path. If both exist, 66 // assume file is a directory marker. 67 // TODO: Consider making base/file API more ergonomic for file and 68 // directory name collisions, e.g. by making it easy to systematically 69 // shadow one. 70 for lister.Scan() { 71 if lister.IsDir() || // We've found an exact match, and it's a directory. 72 lister.Path() != path { // We're seeing children, so path must be a directory. 73 child = newDirNode(path, name) 74 break 75 } 76 child = newFileNode(path, toRegInfo(name, lister.Info())) 77 } 78 if err := lister.Err(); err != nil { 79 return nil, errors.E(err, "scanning", path) 80 } 81 if child == nil { 82 return nil, errors.E(errors.NotExist, path, "not found") 83 } 84 return child, nil 85 } 86 87 // Children implements fsnode.Parent. 88 func (d dirNode) Children() fsnode.Iterator { 89 biofseventlog.UsedFeature("gfilefs.dir.children") 90 return fsnode.NewLazyIterator(d.generateChildren) 91 } 92 93 // AddChildLeaf implements fsnode.Parent. 94 func (d dirNode) AddChildLeaf( 95 ctx context.Context, 96 name string, 97 flags uint32, 98 ) (fsnode.Leaf, fsctx.File, error) { 99 biofseventlog.UsedFeature("gfilefs.dir.addLeaf") 100 path := file.Join(d.path, name) 101 info := fsnode.NewRegInfo(name). 102 WithModePerm(0444). 103 WithCacheableFor(fileInfoCacheFor) 104 n := newFileNode(path, info) 105 f, err := n.OpenFile(ctx, int(flags)) 106 if err != nil { 107 return nil, nil, errors.E(err, "creating file") 108 } 109 return n, f, nil 110 } 111 112 // AddChildParent implements fsnode.Parent. 113 func (d dirNode) AddChildParent(_ context.Context, name string) (fsnode.Parent, error) { 114 biofseventlog.UsedFeature("gfilefs.dir.addParent") 115 // TODO: Consider supporting directories better in base/file, maybe with 116 // some kind of directory marker. 117 path := file.Join(d.path, name) 118 return newDirNode(path, name), nil 119 } 120 121 // RemoveChild implements fsnode.Parent. 122 func (d dirNode) RemoveChild(ctx context.Context, name string) error { 123 biofseventlog.UsedFeature("gfilefs.rmChild") 124 return file.Remove(ctx, file.Join(d.path, name)) 125 } 126 127 func (d dirNode) FSNodeT() {} 128 129 func (d dirNode) generateChildren(ctx context.Context) ([]fsnode.T, error) { 130 var ( 131 // byName is keyed by child name and is used to handle duplicate names 132 // we may get when scanning, i.e. if there is a directory and file with 133 // the same name (which is possible in S3). 134 byName = make(map[string]fsnode.T) 135 vctx = v23context.FromGoContextWithValues(ctx, vcontext.Background()) 136 lister = file.List(vctx, d.path, false) 137 ) 138 for lister.Scan() { 139 var ( 140 childPath = lister.Path() 141 name = file.Base(childPath) 142 ) 143 // Resolve duplicates by preferring the directory and shadowing the 144 // file. This should be kept consistent with the behavior of Child. 145 // We do not expect multiple files or directories with the same name, 146 // so behavior of that case is undefined. 147 if lister.IsDir() { 148 byName[name] = newDirNode(childPath, name) 149 } else if _, ok := byName[name]; !ok { 150 byName[name] = newFileNode(childPath, toRegInfo(name, lister.Info())) 151 } 152 } 153 if err := lister.Err(); err != nil { 154 return nil, errors.E(err, "listing", d.path) 155 } 156 children := make([]fsnode.T, 0, len(byName)) 157 for _, child := range byName { 158 children = append(children, child) 159 } 160 return children, nil 161 } 162 163 type fileNode struct { 164 // path of the file this node represents. 165 path string 166 167 // TODO: Consider expiring this info to pick up external changes (or fix 168 // possible inconsistency due to races?). 169 info atomic.Value // fsnode.FileInfo 170 } 171 172 var ( 173 _ (fsnode.Cacheable) = (*fileNode)(nil) 174 _ (fsnode.Leaf) = (*fileNode)(nil) 175 ) 176 177 func newFileNode(path string, info fsnode.FileInfo) *fileNode { 178 // TODO: Remove after updating fragments to support fsnode.T directly. 179 info = info.WithSys(path) 180 n := fileNode{path: path} 181 n.info.Store(info) 182 return &n 183 } 184 185 func (n *fileNode) Info() os.FileInfo { 186 return n.fsnodeInfo() 187 } 188 189 func (fileNode) FSNodeT() {} 190 191 func (n *fileNode) OpenFile(ctx context.Context, flag int) (fsctx.File, error) { 192 biofseventlog.UsedFeature("gfilefs.file.open") 193 return OpenFile(ctx, n, flag) 194 } 195 196 func (n *fileNode) CacheableFor() time.Duration { 197 return fsnode.CacheableFor(n.fsnodeInfo()) 198 } 199 200 func (n *fileNode) fsnodeInfo() fsnode.FileInfo { 201 return n.info.Load().(fsnode.FileInfo) 202 } 203 204 func (n *fileNode) setFsnodeInfo(info fsnode.FileInfo) { 205 n.info.Store(info) 206 } 207 208 func toRegInfo(name string, info file.Info) fsnode.FileInfo { 209 return fsnode.NewRegInfo(name). 210 WithModePerm(0666). 211 WithSize(info.Size()). 212 WithModTime(info.ModTime()). 213 WithCacheableFor(fileInfoCacheFor) 214 }