github.com/grailbio/base@v0.0.11/file/addfs/per_node.go (about) 1 package addfs 2 3 import ( 4 "context" 5 "fmt" 6 "io/fs" 7 "time" 8 9 "github.com/grailbio/base/file/fsnode" 10 "github.com/grailbio/base/ioctx/fsctx" 11 "github.com/grailbio/base/log" 12 ) 13 14 type ( 15 // PerNodeFunc computes nodes to add to a directory tree, for example to present alternate views 16 // of raw data, expand archive files, etc. It operates on a single node at a time. If it returns 17 // any "addition" nodes, ApplyPerNodeFuncs will place them under a sibling directory called 18 // "...". For example, suppose we have an input directory: 19 // parent/ 20 // └─dir1/ 21 // ├─fileA 22 // ├─fileB 23 // └─dir2/ 24 // and we call ApplyPerNodeFuncs(parent/, ourFns). The resulting directory tree will be 25 // parent/ 26 // ├─.../ 27 // │ └─dir1/ 28 // │ └─[ nodes returned by PerNodeFunc.Apply(_, dir1/) for all ourFns ] 29 // └─dir1/ 30 // ├─.../ 31 // │ ├─fileA/ 32 // │ │ └─[ nodes returned by PerNodeFunc.Apply(_, fileA) for all ourFns ] 33 // │ ├─fileB/ 34 // │ │ └─[ nodes returned by PerNodeFunc.Apply(_, fileB) for all ourFns ] 35 // │ └─dir2/ 36 // │ └─[ nodes returned by PerNodeFunc.Apply(_, dir2/) for all ourFns ] 37 // ├─fileA 38 // ├─fileB 39 // └─dir2/ 40 // └─.../ 41 // Users browsing this resulting tree can work with just the original files and ourFns won't 42 // be invoked. However, they can also navigate into any of the .../s if interested and then 43 // use the additional views generated by ourFns. If they're interested in our_view for 44 // /path/to/a/file, they just need to prepend .../, like /path/to/a/.../file/our_view. 45 // (Perhaps it'd be more intuitive to "append", like /path/to/a/file/our_view, but then the 46 // file name would conflict with the view-containing directory.) 47 // 48 // Funcs that need to list the children of a fsnode.Parent should be careful: they may want to 49 // set an upper limit on number of entries to read, and otherwise default to empty, to avoid 50 // performance problems (resulting in bad UX) for very large directories. 51 // 52 // Funcs that simply look at filenames and declare derived outputs may want to place their 53 // children directly under /.../file/ for convenient access. However, Funcs that are expensive, 54 // for example reading some file contents, etc., may want to separate themselves under their own 55 // subdirectory, like .../file/func_name/. This lets users browsing the tree "opt-in" to seeing 56 // the results of the expensive computation by navigating to .../file/func_name/. 57 // 58 // If the input tree has any "..." that conflict with the added ones, the added ones override. 59 // The originals will simply not be accessible. 60 PerNodeFunc interface { 61 Apply(context.Context, fsnode.T) (adds []fsnode.T, _ error) 62 } 63 perNodeFunc func(context.Context, fsnode.T) (adds []fsnode.T, _ error) 64 ) 65 66 func NewPerNodeFunc(fn func(context.Context, fsnode.T) ([]fsnode.T, error)) PerNodeFunc { 67 return perNodeFunc(fn) 68 } 69 func (f perNodeFunc) Apply(ctx context.Context, n fsnode.T) ([]fsnode.T, error) { return f(ctx, n) } 70 71 const addsDirName = "..." 72 73 // perNodeImpl extends the original Parent with the .../ child. 74 type perNodeImpl struct { 75 original fsnode.Parent 76 fns []PerNodeFunc 77 adds fsnode.Parent 78 } 79 80 var ( 81 _ fsnode.Parent = (*perNodeImpl)(nil) 82 _ fsnode.Cacheable = (*perNodeImpl)(nil) 83 ) 84 85 // ApplyPerNodeFuncs returns a new Parent that contains original's nodes plus any added by fns. 86 // See PerNodeFunc's for more documentation on how this works. 87 // Later fns's added nodes will overwrite earlier ones, if any names conflict. 88 func ApplyPerNodeFuncs(original fsnode.Parent, fns ...PerNodeFunc) fsnode.Parent { 89 fns = append([]PerNodeFunc{}, fns...) 90 adds := perNodeAdds{ 91 FileInfo: fsnode.CopyFileInfo(original.Info()). 92 WithName(addsDirName). 93 // ... directory is not writable. 94 WithModePerm(original.Info().Mode().Perm() & 0555), 95 original: original, 96 fns: fns, 97 } 98 return &perNodeImpl{original, fns, &adds} 99 } 100 101 func (n *perNodeImpl) FSNodeT() {} 102 func (n *perNodeImpl) Info() fs.FileInfo { return n.original.Info() } 103 func (n *perNodeImpl) CacheableFor() time.Duration { return fsnode.CacheableFor(n.original) } 104 func (n *perNodeImpl) Child(ctx context.Context, name string) (fsnode.T, error) { 105 if name == addsDirName { 106 return n.adds, nil 107 } 108 child, err := n.original.Child(ctx, name) 109 if err != nil { 110 return nil, err 111 } 112 return perNodeRecurse(child, n.fns), nil 113 } 114 func (n *perNodeImpl) Children() fsnode.Iterator { 115 return fsnode.NewConcatIterator( 116 // TODO: Consider omitting .../ if the directory has no other children. 117 fsnode.NewIterator(n.adds), 118 // TODO: Filter out any conflicting ... to be consistent with Child. 119 fsnode.MapIterator(n.original.Children(), func(_ context.Context, child fsnode.T) (fsnode.T, error) { 120 return perNodeRecurse(child, n.fns), nil 121 }), 122 ) 123 } 124 func (n *perNodeImpl) AddChildLeaf(ctx context.Context, name string, flags uint32) (fsnode.Leaf, fsctx.File, error) { 125 return n.original.AddChildLeaf(ctx, name, flags) 126 } 127 func (n *perNodeImpl) AddChildParent(ctx context.Context, name string) (fsnode.Parent, error) { 128 p, err := n.original.AddChildParent(ctx, name) 129 if err != nil { 130 return nil, err 131 } 132 return ApplyPerNodeFuncs(p, n.fns...), nil 133 } 134 func (n *perNodeImpl) RemoveChild(ctx context.Context, name string) error { 135 return n.original.RemoveChild(ctx, name) 136 } 137 138 // perNodeAdds is the .../ Parent. It has a child (directory) for each original child (both 139 // directories and files). The children contain the PerNodeFunc.Apply outputs. 140 type perNodeAdds struct { 141 fsnode.ParentReadOnly 142 fsnode.FileInfo 143 original fsnode.Parent 144 fns []PerNodeFunc 145 } 146 147 var ( 148 _ fsnode.Parent = (*perNodeAdds)(nil) 149 _ fsnode.Cacheable = (*perNodeAdds)(nil) 150 ) 151 152 func (n *perNodeAdds) Child(ctx context.Context, name string) (fsnode.T, error) { 153 child, err := n.original.Child(ctx, name) 154 if err != nil { 155 return nil, err 156 } 157 return n.newAddsForChild(child), nil 158 } 159 func (n *perNodeAdds) Children() fsnode.Iterator { 160 // TODO: Filter out any conflicting ... to be consistent with Child. 161 return fsnode.MapIterator(n.original.Children(), func(_ context.Context, child fsnode.T) (fsnode.T, error) { 162 return n.newAddsForChild(child), nil 163 }) 164 } 165 func (n *perNodeAdds) FSNodeT() {} 166 167 func (n *perNodeAdds) newAddsForChild(original fsnode.T) fsnode.Parent { 168 originalInfo := original.Info() 169 return fsnode.NewParent( 170 fsnode.NewDirInfo(originalInfo.Name()). 171 WithModTime(originalInfo.ModTime()). 172 // Derived directory must be executable to be usable, even if original file wasn't. 173 WithModePerm(originalInfo.Mode().Perm()|0111). 174 WithCacheableFor(fsnode.CacheableFor(original)), 175 fsnode.FuncChildren(func(ctx context.Context) ([]fsnode.T, error) { 176 adds := make(map[string]fsnode.T) 177 for _, fn := range n.fns { 178 fnAdds, err := fn.Apply(ctx, original) 179 if err != nil { 180 return nil, fmt.Errorf("addfs: error running func %v: %w", fn, err) 181 } 182 for _, add := range fnAdds { 183 name := add.Info().Name() 184 if _, exists := adds[name]; exists { 185 // TODO: Consider returning an error here. Or merging the added trees? 186 log.Error.Printf("addfs %s: conflict for added name: %s", originalInfo.Name(), name) 187 } 188 adds[name] = add 189 } 190 } 191 wrapped := make([]fsnode.T, 0, len(adds)) 192 for _, add := range adds { 193 wrapped = append(wrapped, perNodeRecurse(add, n.fns)) 194 } 195 return wrapped, nil 196 }), 197 ) 198 } 199 200 func perNodeRecurse(node fsnode.T, fns []PerNodeFunc) fsnode.T { 201 parent, ok := node.(fsnode.Parent) 202 if !ok { 203 return node 204 } 205 return ApplyPerNodeFuncs(parent, fns...) 206 }