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  }