diff --git a/store/dirstore.go b/store/dirstore.go index 3cdc042..3961759 100644 --- a/store/dirstore.go +++ b/store/dirstore.go @@ -9,7 +9,8 @@ import ( // DirStore is a configurable filesystem-backed Store. // It supports single or multiple directories (union), read-only mode, -// a synthetic index entry, subdirectory operations, and custom path resolution. +// 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 @@ -21,6 +22,7 @@ type DirStore struct { 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. @@ -75,6 +77,56 @@ func NewDirStore(dirs []string, perm os.FileMode, opts ...DirOption) *DirStore { 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) @@ -93,6 +145,14 @@ func (d *DirStore) writePath(name string) string { } 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 } @@ -105,7 +165,6 @@ func (d *DirStore) Stat(name string) (os.FileInfo, error) { return nil, err } if d.resolveFn != nil { - // Virtual name may differ from physical filename. return &SyntheticFileInfo{Name_: name, Mode_: info.Mode(), Size_: info.Size()}, nil } return info, nil @@ -121,7 +180,10 @@ func (d *DirStore) List() ([]os.DirEntry, error) { result := make([]os.DirEntry, 0, len(entries)+1) result = append(result, FileEntry("idx", 0444)) result = append(result, entries...) - return result, nil + entries = result + } + for name := range d.children { + entries = append(entries, DirEntry(name, 0555)) } return entries, nil } @@ -143,10 +205,22 @@ func (d *DirStore) List() ([]os.DirEntry, error) { } } } + 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") } @@ -188,6 +262,9 @@ func (d *DirStore) Open(name string) (StoreEntry, error) { } 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") } @@ -202,6 +279,9 @@ func (d *DirStore) Create(name string) error { } 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") } @@ -216,6 +296,9 @@ func (d *DirStore) Delete(name string) error { } 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") } @@ -229,15 +312,31 @@ func (d *DirStore) Rename(old, new string) error { return os.Rename(p, filepath.Join(filepath.Dir(p), new)) } -// ListDir lists entries in a subdirectory relative to the first backing dir. +// 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) }