github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/file/fsnode/fsnode.go (about) 1 // fsnode represents a filesystem as a directed graph (probably a tree for many implementations). 2 // Directories are nodes with out edges (children). Files are nodes without. 3 // 4 // fsnode.T is designed for incremental iteration. Callers can step through the graph one link 5 // at a time (Parent.Child) or one level at a time (Parent.Children). In general, implementations 6 // should do incremental work for each step. See also: Cacheable. 7 // 8 // Compared to fs.FS: 9 // * Leaf explicitly models an unopened file. fs users have to choose their own representation, 10 // like the pair (fs.FS, name string) or func Open(...). 11 // * Graph traversal (that is, directory listing) uses the one node type, rather than a separate 12 // one (like fs.DirEntry). Callers can access "all cheaply available FileInfo" during listing 13 // or can Open nodes if they want completeness at higher cost. 14 // * Parent offers one, explicit way of traversing the graph. fs.FS has optional ReadDirFS or 15 // callers can Open(".") and see if ReadDirFile is returned. (fs.ReadDir unifies these but also 16 // disallows pagination). 17 // * Only supports directories and files. fs.FS supports much more. TODO: Add symlinks? 18 // * fs.FS.Open naturally allows "jumping" several levels deep without step-by-step traversal. 19 // (See Parent.Child) for performance note. 20 package fsnode 21 22 import ( 23 "context" 24 "fmt" 25 "io" 26 "os" 27 "time" 28 29 "github.com/Schaudge/grailbase/errors" 30 "github.com/Schaudge/grailbase/ioctx/fsctx" 31 ) 32 33 type ( 34 // T is a Parent or Leaf. A T that is not either of those is invalid. 35 T interface { 36 // Info provides immediately-available information. A subset of fields must be accurate: 37 // Name 38 // Mode&os.ModeType 39 // IsDir 40 // The rest can be zero values if they're not immediately available. 41 // Implementations may find FileInfo (in this package) convenient for embedding or use 42 // in public API. 43 // 44 // Leaf.Open().Stat() gets complete information. That returned FileInfo must have the same 45 // values for the fields listed above. The others can change if better information is 46 // available. 47 // TODO: Specify something about Info()'s return changing after a Stat call? 48 Info() os.FileInfo 49 // FSNodeT distinguishes T from os.FileInfo. It does nothing. 50 FSNodeT() 51 } 52 // Parent is a T that has zero or more child Ts. 53 Parent interface { 54 // T.Info must be consistent with directory (mode and IsDir). 55 T 56 // Child returns the named child. Returns nil, os.ErrNotExist if no such child exists. 57 // name is not a path and must not contain '/'. It must satisfy fs.ValidPath, too. 58 // 59 // In some implementations, Child lookup may be relatively expensive and implementations 60 // may want to reduce the cost of accessing a deeply-nested node. They may make all Child() 61 // requests succeed immediately and then return path errors from listing or Leaf.Open 62 // operations for the earlier path segment. 63 Child(_ context.Context, name string) (T, error) 64 // Children returns an iterator that can list all children. 65 // Children takes no Context; it's expected to simply construct a view and return errors to 66 // callers when they choose an Iterator operation. 67 Children() Iterator 68 // AddChildLeaf adds a child leaf to this parent, returning the new 69 // leaf and an open file for the leaf's contents. The behavior of name 70 // collisions may vary by implementation. It may be convenient to embed 71 // ParentReadOnly if your Parent implementation is read-only. 72 // TODO: Include mode? 73 AddChildLeaf(_ context.Context, name string, flags uint32) (Leaf, fsctx.File, error) 74 // AddChildParent adds a child parent to this parent, returning the new 75 // parent. The behavior of name collisions may vary by implementation. 76 // It may be convenient to embed ParentReadOnly if your Parent 77 // implementation is read-only. 78 AddChildParent(_ context.Context, name string) (Parent, error) 79 // RemoveChild removes a child. It may be convenient to embed 80 // ParentReadOnly if your Parent implementation is read-only. 81 RemoveChild(_ context.Context, name string) error 82 } 83 // Iterator yields child nodes iteratively. 84 // 85 // Users must serialize their own method calls. No calls can be made after Close(). 86 // TODO: Do we need Stat here, maybe to update directory mode? 87 Iterator interface { 88 // Next gets the next node. Must return (nil, io.EOF) at the end, not (non-nil, io.EOF). 89 Next(context.Context) (T, error) 90 // Close frees resources. 91 Close(context.Context) error 92 } 93 // Leaf is a T corresponding to a fsctx.File. It can be opened any number of times and must 94 // allow concurrent calls (it may lock internally if necessary). 95 Leaf interface { 96 // T is implementation of common node operations. The FileInfo returned 97 // by T.Info must be consistent with a regular file (mode and !IsDir). 98 T 99 // OpenFile opens the file. File.Stat()'s result must be consistent 100 // with T.Info. flag holds the flag bits, specified the same as those 101 // passed to os.OpenFile.See os.O_*. 102 OpenFile(ctx context.Context, flag int) (fsctx.File, error) 103 } 104 // Cacheable optionally lets users make use of caching. The cacheable data depends on 105 // which type Cacheable is defined on: 106 // * On any T, FileInfo. 107 // * On an fsctx.File, the FileInfo and contents. 108 // 109 // Common T implementations are expected to be "views" of remote data sources not under 110 // our exclusive control (like local filesystem or S3). As such, callers should generally 111 // expect best-effort consistency, regardless of caching. 112 Cacheable interface { 113 // CacheableFor is the maximum allowed cache time. 114 // Zero means don't cache. Negative means cache forever. 115 // TODO: Make this a non-Duration type to avoid confusion with negatives? 116 CacheableFor() time.Duration 117 } 118 cacheableFor struct{ time.Duration } 119 ) 120 121 const CacheForever = time.Duration(-1) 122 123 // CacheableFor returns the configured cache time if obj is Cacheable, otherwise returns default 0. 124 func CacheableFor(obj interface{}) time.Duration { 125 cacheable, ok := obj.(Cacheable) 126 if !ok { 127 return 0 128 } 129 return cacheable.CacheableFor() 130 } 131 func NewCacheable(d time.Duration) Cacheable { return cacheableFor{d} } 132 func (c cacheableFor) CacheableFor() time.Duration { return c.Duration } 133 134 // Open opens the file of a leaf in (the commonly desired) read-only mode. 135 func Open(ctx context.Context, n Leaf) (fsctx.File, error) { 136 return n.OpenFile(ctx, os.O_RDONLY) 137 } 138 139 // IterateFull reads the full len(dst) nodes from Iterator. If actual number read is less than 140 // len(dst), error is non-nil. Error is io.EOF for EOF. Unlike io.ReadFull, this doesn't return 141 // io.ErrUnexpectedEOF (unless iter does). 142 func IterateFull(ctx context.Context, iter Iterator, dst []T) (int, error) { 143 for i := range dst { 144 var err error 145 dst[i], err = iter.Next(ctx) 146 if err != nil { 147 if err == io.EOF && dst[i] != nil { 148 return i, iteratorEOFError(iter) 149 } 150 return i, err 151 } 152 } 153 return len(dst), nil 154 } 155 156 // IterateAll reads iter until EOF. Returns nil error on success, not io.EOF (like io.ReadAll). 157 func IterateAll(ctx context.Context, iter Iterator) ([]T, error) { 158 var dst []T 159 for { 160 node, err := iter.Next(ctx) 161 if err != nil { 162 if err == io.EOF { 163 if node != nil { 164 return dst, iteratorEOFError(iter) 165 } 166 return dst, nil 167 } 168 return dst, err 169 } 170 dst = append(dst, node) 171 } 172 } 173 174 func iteratorEOFError(iter Iterator) error { 175 return errors.E(errors.Precondition, fmt.Sprintf("BUG: iterator.Next (%T) returned element+EOF", iter)) 176 } 177 178 // ParentReadOnly is a partial implementation of Parent interface functions 179 // that returns NotSupported errors for all write operations. It may be 180 // convenient to embed if your Parent implementation is read-only. 181 // 182 // type MyParent struct { 183 // fsnode.ParentReadOnly 184 // } 185 // 186 // func (MyParent) ChildChild(context.Context, string) (T, error) { ... } 187 // func (MyParent) Children() Iterator { ... } 188 // // No need to implement write functions. 189 type ParentReadOnly struct{} 190 191 func (ParentReadOnly) AddChildLeaf(context.Context, string, uint32) (Leaf, fsctx.File, error) { 192 return nil, nil, errors.E(errors.NotSupported) 193 } 194 func (ParentReadOnly) AddChildParent(context.Context, string) (Parent, error) { 195 return nil, errors.E(errors.NotSupported) 196 } 197 func (ParentReadOnly) RemoveChild(context.Context, string) error { 198 return errors.E(errors.NotSupported) 199 }