package store import ( "context" "fmt" "os" "path/filepath" ) // DirStore is a configurable filesystem-backed Store. // It supports single or multiple directories (union), read-only mode, // a synthetic index entry, subdirectory operations, custom path resolution, // and mounting child DirStores at names (sub-stores). type DirStore struct { dirs []string perm os.FileMode readOnly bool indexFn func(Store) ([]byte, error) resolveFn func([]string, string) (string, error) listFn func([]string) ([]os.DirEntry, error) createFn func([]string, string, os.FileMode) error deleteFn func([]string, string) error renameFn func([]string, string, string) error openFn func([]string, string) (StoreEntry, error) children map[string]*DirStore } // DirOption configures a DirStore. type DirOption func(*DirStore) // WithReadOnly makes the store reject writes, creates, deletes, and renames. func WithReadOnly() DirOption { return func(d *DirStore) { d.readOnly = true } } // WithIndex adds a synthetic read-only "idx" entry whose content is // generated by calling fn with the store itself. func WithIndex(fn func(Store) ([]byte, error)) DirOption { return func(d *DirStore) { d.indexFn = fn } } // WithResolver overrides how virtual names map to filesystem paths. func WithResolver(fn func(dirs []string, name string) (string, error)) DirOption { return func(d *DirStore) { d.resolveFn = fn } } // WithLister overrides how the store lists its entries. func WithLister(fn func(dirs []string) ([]os.DirEntry, error)) DirOption { return func(d *DirStore) { d.listFn = fn } } // WithCreator overrides file creation logic. func WithCreator(fn func(dirs []string, name string, perm os.FileMode) error) DirOption { return func(d *DirStore) { d.createFn = fn } } // WithDeleter overrides file deletion logic. func WithDeleter(fn func(dirs []string, name string) error) DirOption { return func(d *DirStore) { d.deleteFn = fn } } // WithRenamer overrides rename logic. func WithRenamer(fn func(dirs []string, old, new string) error) DirOption { return func(d *DirStore) { d.renameFn = fn } } // WithOpener overrides how entries are opened (bypasses filesystem read). func WithOpener(fn func(dirs []string, name string) (StoreEntry, error)) DirOption { return func(d *DirStore) { d.openFn = fn } } // NewDirStore creates a DirStore backed by one or more directories. // The first directory is the write target for unions. func NewDirStore(dirs []string, perm os.FileMode, opts ...DirOption) *DirStore { d := &DirStore{dirs: dirs, perm: perm} for _, o := range opts { o(d) } return d } // Mount attaches a child DirStore at the given name. func (d *DirStore) Mount(name string, child *DirStore) { if d.children == nil { d.children = make(map[string]*DirStore) } d.children[name] = child } // Unmount removes a mounted child. func (d *DirStore) Unmount(name string) { delete(d.children, name) } // Child returns the child mounted at name, or nil. func (d *DirStore) Child(name string) *DirStore { if d.children == nil { return nil } return d.children[name] } // split checks if name routes to a child store. Returns (child, rest, true) // if a child handles this path, or (nil, "", false) otherwise. func (d *DirStore) split(name string) (*DirStore, string, bool) { if d.children == nil { return nil, "", false } // Check for exact child match (directory itself). if child, ok := d.children[name]; ok { return child, "", true } // Check for child prefix: "childname/rest" if i := indexOf(name, '/'); i >= 0 { prefix := name[:i] if child, ok := d.children[prefix]; ok { return child, name[i+1:], true } } return nil, "", false } func indexOf(s string, b byte) int { for i := range len(s) { if s[i] == b { return i } } return -1 } func (d *DirStore) resolve(name string) (string, error) { if d.resolveFn != nil { return d.resolveFn(d.dirs, name) } for _, dir := range d.dirs { p := filepath.Join(dir, name) if _, err := os.Stat(p); err == nil { return p, nil } } return "", fmt.Errorf("%s: not found", name) } func (d *DirStore) writePath(name string) string { return filepath.Join(d.dirs[0], name) } func (d *DirStore) Stat(name string) (os.FileInfo, error) { // Check children first. if child, rest, ok := d.split(name); ok { if rest == "" { // The child directory itself. return &SyntheticFileInfo{Name_: name, Mode_: 0555, IsDir_: true}, nil } return child.Stat(rest) } if name == "idx" && d.indexFn != nil { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil } p, err := d.resolve(name) if err != nil { return nil, err } info, err := os.Stat(p) if err != nil { return nil, err } if d.resolveFn != nil { return &SyntheticFileInfo{Name_: name, Mode_: info.Mode(), Size_: info.Size()}, nil } return info, nil } func (d *DirStore) List() ([]os.DirEntry, error) { if d.listFn != nil { entries, err := d.listFn(d.dirs) if err != nil { return nil, err } if d.indexFn != nil { result := make([]os.DirEntry, 0, len(entries)+1) result = append(result, FileEntry("idx", 0444)) result = append(result, entries...) entries = result } for name := range d.children { entries = append(entries, DirEntry(name, 0555)) } return entries, nil } seen := make(map[string]bool) var result []os.DirEntry if d.indexFn != nil { result = append(result, FileEntry("idx", 0444)) seen["idx"] = true } for _, dir := range d.dirs { entries, err := os.ReadDir(dir) if err != nil { continue } for _, e := range entries { if !seen[e.Name()] { seen[e.Name()] = true result = append(result, e) } } } for name := range d.children { if !seen[name] { result = append(result, DirEntry(name, 0555)) } } return result, nil } func (d *DirStore) Open(name string) (StoreEntry, error) { // Delegate to child stores. if child, rest, ok := d.split(name); ok { if rest == "" { return nil, fmt.Errorf("%s: is a directory", name) } return child.Open(rest) } notBlocking := func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") } if name == "idx" && d.indexFn != nil { return &EntryConfig{ StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil }, ReadFn: func() ([]byte, error) { return d.indexFn(d) }, WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") }, BlockingReadFn: notBlocking, }, nil } if d.openFn != nil { return d.openFn(d.dirs, name) } p, err := d.resolve(name) if err != nil { if d.readOnly { return nil, err } // Not found anywhere; write path is in first dir. p = d.writePath(name) } wp := d.writePath(name) writeFn := func(data []byte) error { if d.readOnly { return fmt.Errorf("%s: read-only", name) } if err := os.MkdirAll(filepath.Dir(wp), 0755); err != nil { return err } return os.WriteFile(wp, data, d.perm) } return &EntryConfig{ StatFn: func() (os.FileInfo, error) { return os.Stat(p) }, ReadFn: func() ([]byte, error) { return os.ReadFile(p) }, WriteFn: writeFn, BlockingReadFn: notBlocking, }, nil } func (d *DirStore) Create(name string) error { if child, rest, ok := d.split(name); ok && rest != "" { return child.Create(rest) } if d.readOnly { return fmt.Errorf("read-only store") } if d.createFn != nil { return d.createFn(d.dirs, name, d.perm) } p := d.writePath(name) if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { return err } return os.WriteFile(p, nil, d.perm) } func (d *DirStore) Delete(name string) error { if child, rest, ok := d.split(name); ok && rest != "" { return child.Delete(rest) } if d.readOnly { return fmt.Errorf("read-only store") } if d.deleteFn != nil { return d.deleteFn(d.dirs, name) } p, err := d.resolve(name) if err != nil { return err } return os.Remove(p) } func (d *DirStore) Rename(old, new string) error { if child, rest, ok := d.split(old); ok && rest != "" { return child.Rename(rest, new) } if d.readOnly { return fmt.Errorf("read-only store") } if d.renameFn != nil { return d.renameFn(d.dirs, old, new) } p, err := d.resolve(old) if err != nil { return err } return os.Rename(p, filepath.Join(filepath.Dir(p), new)) } // ListDir lists entries in a subdirectory. func (d *DirStore) ListDir(rel string) ([]os.DirEntry, error) { // Check if rel routes to a child. if child, rest, ok := d.split(rel); ok { if rest == "" { return child.List() } return child.ListDir(rest) } if len(d.dirs) == 0 { return nil, fmt.Errorf("%s: not found", rel) } return os.ReadDir(filepath.Join(d.dirs[0], rel)) } // MkdirAll creates a directory relative to the first backing dir. func (d *DirStore) MkdirAll(rel string) error { if child, rest, ok := d.split(rel); ok && rest != "" { return child.MkdirAll(rest) } if d.readOnly { return fmt.Errorf("read-only store") } if len(d.dirs) == 0 { return fmt.Errorf("no backing directory") } return os.MkdirAll(filepath.Join(d.dirs[0], rel), 0755) }