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  }