package overlay import ( "io/fs" "os" "path" ) // Interface guard // fs.ReadFileFS also fulfills fs.FS var _ fs.ReadFileFS = (*FS)(nil) // FS is an overlay File System type FS struct { fs fs.FS root string doCache bool cache map[string]bool } // PurgeCache purges the cache func (f *FS) PurgeCache() { f.cache = make(map[string]bool) } func (f *FS) apn(name string) string { return path.Join(f.root, name) } func (f *FS) exists(name string) bool { if has, ok := f.cache[name]; ok { return has } _, err := os.Stat(f.apn(name)) if err != nil { f.cache[name] = false return false } f.cache[name] = true return true } // Open opens an fs.File, preferring disk func (f *FS) Open(name string) (fs.File, error) { if f.exists(name) { return os.Open(f.apn(name)) } return f.fs.Open(name) } // ReadFile reads a file, preferring disk func (f *FS) ReadFile(name string) ([]byte, error) { if f.exists(name) { return os.ReadFile(f.apn(name)) } return fs.ReadFile(f.fs, name) } // ReadDir reads []fs.DirEntry // This method will prefer EMBEDDED, because that is the "real" FS for overlay func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) { return fs.ReadDir(f.fs, name) } // Option is a functional option for an FS type Option func(*FS) error // New returns a new FS func New(root string, fs fs.FS, opts ...Option) (*FS, error) { x := &FS{ fs: fs, root: root, cache: make(map[string]bool), } for _, opt := range opts { if err := opt(x); err != nil { return x, err } } return x, nil } // MustNew returns New and panics on error func MustNew(root string, fs fs.FS, opts ...Option) *FS { f, err := New(root, fs, opts...) if err != nil { panic(err) } return f } // WithSub sets a fs.Sub for an FS func WithSub(sub string) Option { return func(x *FS) (err error) { x.fs, err = fs.Sub(x.fs, sub) return } } // WithCaching sets a caching mode for an FS // Caching avoids subsequent os.Stat to determine if a file exists on disk // See bench.txt for differences in usage func WithCaching(doCache bool) Option { return func(x *FS) error { x.doCache = doCache return nil } }