unify DirStore: single type replaces FlatDir, Union, ToolStore
- New store.DirStore with options: WithReadOnly(), WithIndex(fn) - Supports single/multi dir, synthetic idx, ListDir, MkdirAll - Removes UtilStore, ExecStore, ToolStore wrapper types - toolIndex extracted as standalone function - All stores in server.go use store.NewDirStore() - SkillStore stays as-is (different storage model)
This commit is contained in:
parent
8c2f4a8754
commit
190ad4bcdf
243
server.go
243
server.go
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -30,21 +29,12 @@ type (
|
||||||
StoreEntry = store.StoreEntry
|
StoreEntry = store.StoreEntry
|
||||||
Store = store.Store
|
Store = store.Store
|
||||||
RunnableStore = store.RunnableStore
|
RunnableStore = store.RunnableStore
|
||||||
FlatDirStore = store.Store
|
|
||||||
Session = store.Session
|
Session = store.Session
|
||||||
SessionStore = store.SessionStore
|
SessionStore = store.SessionStore
|
||||||
|
|
||||||
syntheticFileInfo = store.SyntheticFileInfo
|
syntheticFileInfo = store.SyntheticFileInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewFlatDirStore(dir string, perm os.FileMode) FlatDirStore {
|
|
||||||
return store.NewFlatDir(dir, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSkillStore() Store {
|
|
||||||
return store.NewSkillStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
func syntheticEntry(name string, mode os.FileMode) os.DirEntry {
|
func syntheticEntry(name string, mode os.FileMode) os.DirEntry {
|
||||||
return store.FileEntry(name, mode)
|
return store.FileEntry(name, mode)
|
||||||
}
|
}
|
||||||
|
|
@ -53,170 +43,22 @@ func syntheticDirEntry(name string, mode os.FileMode) os.DirEntry {
|
||||||
return store.DirEntry(name, mode)
|
return store.DirEntry(name, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- util ---
|
// toolIndex generates a tool index from a store's file listing.
|
||||||
|
func toolIndex(s Store) ([]byte, error) {
|
||||||
// UtilStore is a FlatDirStore backed by the scripts/u/ directory.
|
entries, err := s.List()
|
||||||
type UtilStore struct {
|
|
||||||
FlatDirStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUtilStore() *UtilStore {
|
|
||||||
return &UtilStore{FlatDirStore: NewFlatDirStore(paths.CfgDir()+"/scripts/u", 0555)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UtilStore) Stat(name string) (os.FileInfo, error) {
|
|
||||||
return s.FlatDirStore.Stat(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UtilStore) List() ([]os.DirEntry, error) {
|
|
||||||
return s.FlatDirStore.List()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UtilStore) Open(name string) (StoreEntry, error) {
|
|
||||||
return s.FlatDirStore.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- exec ---
|
|
||||||
|
|
||||||
// ExecStore is a read-only FlatDirStore backed by the scripts/x/ directory.
|
|
||||||
type ExecStore struct {
|
|
||||||
FlatDirStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewExecStore() *ExecStore {
|
|
||||||
return &ExecStore{FlatDirStore: NewFlatDirStore(execute.PluginsPath(), 0555)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ExecStore) Stat(name string) (os.FileInfo, error) {
|
|
||||||
return s.FlatDirStore.Stat(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ExecStore) List() ([]os.DirEntry, error) {
|
|
||||||
return s.FlatDirStore.List()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ExecStore) Open(name string) (StoreEntry, error) {
|
|
||||||
return s.FlatDirStore.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRootStore returns a read-only Store for synthetic root-level files.
|
|
||||||
func NewRootStore() Store {
|
|
||||||
helpPath := paths.CfgDir() + "/help.md"
|
|
||||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
|
||||||
return nil, "", fmt.Errorf("blocking read not supported")
|
|
||||||
}
|
|
||||||
readOnly := func([]byte) error { return fmt.Errorf("read-only") }
|
|
||||||
|
|
||||||
entries := map[string]func() ([]byte, error){
|
|
||||||
"backends": func() ([]byte, error) {
|
|
||||||
return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil
|
|
||||||
},
|
|
||||||
"help": func() ([]byte, error) {
|
|
||||||
return os.ReadFile(helpPath)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &rootStore{entries: entries, notBlocking: notBlocking, readOnly: readOnly}
|
|
||||||
}
|
|
||||||
|
|
||||||
type rootStore struct {
|
|
||||||
entries map[string]func() ([]byte, error)
|
|
||||||
notBlocking func(context.Context, string) ([]byte, string, error)
|
|
||||||
readOnly func([]byte) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rootStore) Stat(name string) (os.FileInfo, error) {
|
|
||||||
if _, ok := r.entries[name]; ok {
|
|
||||||
return &syntheticFileInfo{Name_: name, Mode_: 0444}, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%s: not found", name)
|
|
||||||
}
|
|
||||||
func (r *rootStore) List() ([]os.DirEntry, error) {
|
|
||||||
return []os.DirEntry{
|
|
||||||
syntheticEntry("backends", 0444),
|
|
||||||
syntheticEntry("help", 0444),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
func (r *rootStore) Open(name string) (StoreEntry, error) {
|
|
||||||
readFn, ok := r.entries[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%s: not found", name)
|
|
||||||
}
|
|
||||||
return &store.EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return &syntheticFileInfo{Name_: name, Mode_: 0444}, nil },
|
|
||||||
ReadFn: readFn,
|
|
||||||
WriteFn: r.readOnly,
|
|
||||||
BlockingReadFn: r.notBlocking,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
func (r *rootStore) Create(string) error { return fmt.Errorf("read-only store") }
|
|
||||||
func (r *rootStore) Delete(string) error { return fmt.Errorf("read-only store") }
|
|
||||||
func (r *rootStore) Rename(string, string) error { return fmt.Errorf("read-only store") }
|
|
||||||
|
|
||||||
// --- tools ---
|
|
||||||
|
|
||||||
// ToolStore is a BlobStore backed by the tools directory.
|
|
||||||
// It extends FlatDirStore with a synthetic "idx" entry that lists all tools
|
|
||||||
// with their descriptions and argument signatures.
|
|
||||||
// It supports subdirectories (e.g. _lib/) for shared library code.
|
|
||||||
type ToolStore struct {
|
|
||||||
FlatDirStore
|
|
||||||
dir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewToolStore() *ToolStore {
|
|
||||||
dir := execute.ToolsPath()
|
|
||||||
return &ToolStore{FlatDirStore: NewFlatDirStore(dir, 0755), dir: dir}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDir lists entries in a subdirectory relative to the tools root.
|
|
||||||
func (s *ToolStore) ListDir(rel string) ([]os.DirEntry, error) {
|
|
||||||
return os.ReadDir(filepath.Join(s.dir, rel))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MkdirAll creates a directory relative to the tools root.
|
|
||||||
func (s *ToolStore) MkdirAll(rel string) error {
|
|
||||||
return os.MkdirAll(filepath.Join(s.dir, rel), 0755)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ToolStore) Stat(name string) (os.FileInfo, error) {
|
|
||||||
if name == "idx" {
|
|
||||||
return &syntheticFileInfo{Name_: "idx", Mode_: 0444}, nil
|
|
||||||
}
|
|
||||||
return s.FlatDirStore.Stat(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ToolStore) List() ([]os.DirEntry, error) {
|
|
||||||
entries, err := s.FlatDirStore.List()
|
|
||||||
result := make([]os.DirEntry, 0, len(entries)+1)
|
|
||||||
result = append(result, syntheticEntry("idx", 0444))
|
|
||||||
result = append(result, entries...)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ToolStore) Open(name string) (StoreEntry, error) {
|
|
||||||
if name == "idx" {
|
|
||||||
return &store.EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return &syntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
|
|
||||||
ReadFn: func() ([]byte, error) { return s.index() },
|
|
||||||
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
|
||||||
BlockingReadFn: func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") },
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return s.FlatDirStore.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ToolStore) index() ([]byte, error) {
|
|
||||||
entries, err := s.FlatDirStore.List()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() {
|
if e.IsDir() || e.Name() == "idx" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data, err := storeRead(s.FlatDirStore, e.Name())
|
entry, err := s.Open(e.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := entry.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -246,6 +88,51 @@ func (s *ToolStore) index() ([]byte, error) {
|
||||||
return []byte(sb.String()), nil
|
return []byte(sb.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRootStore returns a read-only Store for synthetic root-level files.
|
||||||
|
func NewRootStore() *store.DirStore {
|
||||||
|
helpPath := paths.CfgDir() + "/help.md"
|
||||||
|
entries := map[string]func() ([]byte, error){
|
||||||
|
"backends": func() ([]byte, error) {
|
||||||
|
return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil
|
||||||
|
},
|
||||||
|
"help": func() ([]byte, error) {
|
||||||
|
return os.ReadFile(helpPath)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
||||||
|
return nil, "", fmt.Errorf("blocking read not supported")
|
||||||
|
}
|
||||||
|
readOnly := func([]byte) error { return fmt.Errorf("read-only") }
|
||||||
|
|
||||||
|
return store.NewDirStore(nil, 0444,
|
||||||
|
store.WithReadOnly(),
|
||||||
|
store.WithResolver(func(_ []string, name string) (string, error) {
|
||||||
|
if _, ok := entries[name]; ok {
|
||||||
|
return name, nil // virtual path, handled by opener
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%s: not found", name)
|
||||||
|
}),
|
||||||
|
store.WithLister(func(_ []string) ([]os.DirEntry, error) {
|
||||||
|
return []os.DirEntry{
|
||||||
|
syntheticEntry("backends", 0444),
|
||||||
|
syntheticEntry("help", 0444),
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
store.WithOpener(func(_ []string, name string) (StoreEntry, error) {
|
||||||
|
readFn, ok := entries[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: not found", name)
|
||||||
|
}
|
||||||
|
return &store.EntryConfig{
|
||||||
|
StatFn: func() (os.FileInfo, error) { return &syntheticFileInfo{Name_: name, Mode_: 0444}, nil },
|
||||||
|
ReadFn: readFn,
|
||||||
|
WriteFn: readOnly,
|
||||||
|
BlockingReadFn: notBlocking,
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
QTDir = plan9.QTDIR
|
QTDir = plan9.QTDIR
|
||||||
QTFile = plan9.QTFILE
|
QTFile = plan9.QTFILE
|
||||||
|
|
@ -280,14 +167,14 @@ type Server struct {
|
||||||
agentStore Store
|
agentStore Store
|
||||||
promptStore Store
|
promptStore Store
|
||||||
memStore Store
|
memStore Store
|
||||||
toolStore *ToolStore
|
toolStore *store.DirStore
|
||||||
utilStore Store
|
utilStore Store
|
||||||
pluginStore Store
|
pluginStore Store
|
||||||
skillStore Store
|
skillStore Store
|
||||||
sessionStore *SessionStore
|
sessionStore *SessionStore
|
||||||
transcriptStore Store
|
transcriptStore Store
|
||||||
tmpStore Store
|
tmpStore Store
|
||||||
rootStore Store
|
rootStore *store.DirStore
|
||||||
strict bool
|
strict bool
|
||||||
yolo bool
|
yolo bool
|
||||||
groups map[string]map[string]bool // group → set of members
|
groups map[string]map[string]bool // group → set of members
|
||||||
|
|
@ -314,15 +201,15 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server {
|
||||||
log: sink.Logger("9p", olog.LevelDebug),
|
log: sink.Logger("9p", olog.LevelDebug),
|
||||||
sink: sink,
|
sink: sink,
|
||||||
agentsDir: agentsDir,
|
agentsDir: agentsDir,
|
||||||
agentStore: store.NewFlatDirWritableUnion(agentsDirs, 0644),
|
agentStore: store.NewDirStore(agentsDirs, 0644),
|
||||||
promptStore: store.NewFlatDirUnion(agent.PromptsDirs(), 0444),
|
promptStore: store.NewDirStore(agent.PromptsDirs(), 0444, store.WithReadOnly()),
|
||||||
memStore: NewFlatDirStore(memDir, 0644),
|
memStore: store.NewDirStore([]string{memDir}, 0644),
|
||||||
toolStore: NewToolStore(),
|
toolStore: store.NewDirStore([]string{execute.ToolsPath()}, 0755, store.WithIndex(toolIndex)),
|
||||||
utilStore: NewUtilStore(),
|
utilStore: store.NewDirStore([]string{paths.CfgDir() + "/scripts/u"}, 0555, store.WithReadOnly()),
|
||||||
pluginStore: NewExecStore(),
|
pluginStore: store.NewDirStore([]string{execute.PluginsPath()}, 0555, store.WithReadOnly()),
|
||||||
skillStore: NewSkillStore(),
|
skillStore: store.NewSkillStore(),
|
||||||
transcriptStore: NewFlatDirStore(transcriptDir, 0444),
|
transcriptStore: store.NewDirStore([]string{transcriptDir}, 0444, store.WithReadOnly()),
|
||||||
tmpStore: NewFlatDirStore(tmpDir, 0600),
|
tmpStore: store.NewDirStore([]string{tmpDir}, 0600),
|
||||||
rootStore: NewRootStore(),
|
rootStore: NewRootStore(),
|
||||||
groups: make(map[string]map[string]bool),
|
groups: make(map[string]map[string]bool),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
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, and custom path resolution.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 {
|
||||||
|
// Virtual name may differ from physical filename.
|
||||||
|
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...)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DirStore) Open(name string) (StoreEntry, error) {
|
||||||
|
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 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 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 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 relative to the first backing dir.
|
||||||
|
func (d *DirStore) ListDir(rel string) ([]os.DirEntry, error) {
|
||||||
|
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 d.readOnly {
|
||||||
|
return fmt.Errorf("read-only store")
|
||||||
|
}
|
||||||
|
return os.MkdirAll(filepath.Join(d.dirs[0], rel), 0755)
|
||||||
|
}
|
||||||
155
store/skill.go
155
store/skill.go
|
|
@ -1,7 +1,6 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -12,8 +11,13 @@ import (
|
||||||
"ollie/pkg/skills"
|
"ollie/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SkillStoreConfig holds optional dependencies for NewSkillStore.
|
// NewSkillStore returns a DirStore configured for the skill directory layout.
|
||||||
// Nil functions default to their os package equivalents.
|
// Skills are directories containing SKILL.md; virtual names are "skillname.md".
|
||||||
|
func NewSkillStore() *DirStore {
|
||||||
|
return NewSkillStoreWith(SkillStoreConfig{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkillStoreConfig holds optional dependencies for NewSkillStoreWith.
|
||||||
type SkillStoreConfig struct {
|
type SkillStoreConfig struct {
|
||||||
Dirs []string
|
Dirs []string
|
||||||
ReadDir func(string) ([]os.DirEntry, error)
|
ReadDir func(string) ([]os.DirEntry, error)
|
||||||
|
|
@ -25,22 +29,7 @@ type SkillStoreConfig struct {
|
||||||
Rename func(string, string) error
|
Rename func(string, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type skillState struct {
|
func NewSkillStoreWith(cfg SkillStoreConfig) *DirStore {
|
||||||
dirs []string
|
|
||||||
readDir func(string) ([]os.DirEntry, error)
|
|
||||||
openFile func(string) (*os.File, error)
|
|
||||||
readFile func(string) ([]byte, error)
|
|
||||||
writeFile func(string, []byte, os.FileMode) error
|
|
||||||
mkdirAll func(string, os.FileMode) error
|
|
||||||
removeAll func(string) error
|
|
||||||
rename func(string, string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSkillStore() Store {
|
|
||||||
return NewSkillStoreWith(SkillStoreConfig{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSkillStoreWith(cfg SkillStoreConfig) Store {
|
|
||||||
if cfg.Dirs == nil {
|
if cfg.Dirs == nil {
|
||||||
cfg.Dirs = skillDirs()
|
cfg.Dirs = skillDirs()
|
||||||
}
|
}
|
||||||
|
|
@ -66,25 +55,16 @@ func NewSkillStoreWith(cfg SkillStoreConfig) Store {
|
||||||
cfg.Rename = os.Rename
|
cfg.Rename = os.Rename
|
||||||
}
|
}
|
||||||
|
|
||||||
ss := &skillState{
|
ss := &skillHelper{cfg: cfg}
|
||||||
dirs: cfg.Dirs,
|
|
||||||
readDir: cfg.ReadDir,
|
|
||||||
openFile: cfg.Open,
|
|
||||||
readFile: cfg.ReadFile,
|
|
||||||
writeFile: cfg.WriteFile,
|
|
||||||
mkdirAll: cfg.MkdirAll,
|
|
||||||
removeAll: cfg.RemoveAll,
|
|
||||||
rename: cfg.Rename,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storeConfig{
|
return NewDirStore(cfg.Dirs, 0644,
|
||||||
StatFn: ss.stat,
|
WithIndex(ss.index),
|
||||||
ListFn: ss.list,
|
WithResolver(ss.resolve),
|
||||||
OpenFn: ss.open,
|
WithLister(ss.list),
|
||||||
DeleteFn: ss.del,
|
WithCreator(ss.create),
|
||||||
CreateFn: ss.create,
|
WithDeleter(ss.del),
|
||||||
RenameFn: ss.ren,
|
WithRenamer(ss.rename),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func skillDirs() []string {
|
func skillDirs() []string {
|
||||||
|
|
@ -94,11 +74,15 @@ func skillDirs() []string {
|
||||||
return []string{paths.CfgDir() + "/skills"}
|
return []string{paths.CfgDir() + "/skills"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) listSkills() []skills.Meta {
|
type skillHelper struct {
|
||||||
|
cfg SkillStoreConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *skillHelper) listSkills() []skills.Meta {
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
var result []skills.Meta
|
var result []skills.Meta
|
||||||
for _, dir := range ss.dirs {
|
for _, dir := range ss.cfg.Dirs {
|
||||||
entries, err := ss.readDir(dir)
|
entries, err := ss.cfg.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +91,7 @@ func (ss *skillState) listSkills() []skills.Meta {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
skillDir := filepath.Join(dir, e.Name())
|
skillDir := filepath.Join(dir, e.Name())
|
||||||
f, err := ss.openFile(filepath.Join(skillDir, "SKILL.md"))
|
f, err := ss.cfg.Open(filepath.Join(skillDir, "SKILL.md"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -124,107 +108,56 @@ func (ss *skillState) listSkills() []skills.Meta {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) readSkill(name string) ([]byte, error) {
|
func (ss *skillHelper) resolve(_ []string, name string) (string, error) {
|
||||||
for _, m := range ss.listSkills() {
|
|
||||||
if m.Name == name {
|
|
||||||
return ss.readFile(filepath.Join(m.Dir, "SKILL.md"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *skillState) stat(name string) (os.FileInfo, error) {
|
|
||||||
if name == "idx" {
|
|
||||||
return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil
|
|
||||||
}
|
|
||||||
skillName := strings.TrimSuffix(name, ".md")
|
skillName := strings.TrimSuffix(name, ".md")
|
||||||
if _, err := ss.readSkill(skillName); err != nil {
|
for _, m := range ss.listSkills() {
|
||||||
return nil, fmt.Errorf("%s: not found", name)
|
if m.Name == skillName {
|
||||||
|
return filepath.Join(m.Dir, "SKILL.md"), nil
|
||||||
}
|
}
|
||||||
return &SyntheticFileInfo{Name_: name, Mode_: 0666}, nil
|
}
|
||||||
|
return "", fmt.Errorf("%s: not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) list() ([]os.DirEntry, error) {
|
func (ss *skillHelper) list(_ []string) ([]os.DirEntry, error) {
|
||||||
result := []os.DirEntry{FileEntry("idx", 0444)}
|
var result []os.DirEntry
|
||||||
for _, m := range ss.listSkills() {
|
for _, m := range ss.listSkills() {
|
||||||
result = append(result, FileEntry(m.Name+".md", 0666))
|
result = append(result, FileEntry(m.Name+".md", 0666))
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) open(name string) (StoreEntry, error) {
|
func (ss *skillHelper) create(_ []string, name string, _ os.FileMode) error {
|
||||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
|
||||||
return nil, "", fmt.Errorf("blocking read not supported")
|
|
||||||
}
|
|
||||||
if name == "idx" {
|
|
||||||
return &EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
|
|
||||||
ReadFn: func() ([]byte, error) { return ss.index() },
|
|
||||||
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
|
||||||
BlockingReadFn: notBlocking,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
skillName := strings.TrimSuffix(name, ".md")
|
skillName := strings.TrimSuffix(name, ".md")
|
||||||
return &EntryConfig{
|
dir := filepath.Join(ss.cfg.Dirs[0], skillName)
|
||||||
StatFn: func() (os.FileInfo, error) {
|
if err := ss.cfg.MkdirAll(dir, 0755); err != nil {
|
||||||
if _, err := ss.readSkill(skillName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &SyntheticFileInfo{Name_: name, Mode_: 0666}, nil
|
|
||||||
},
|
|
||||||
ReadFn: func() ([]byte, error) { return ss.readSkill(skillName) },
|
|
||||||
WriteFn: func(data []byte) error {
|
|
||||||
dir := ""
|
|
||||||
for _, m := range ss.listSkills() {
|
|
||||||
if m.Name == skillName {
|
|
||||||
dir = m.Dir
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dir == "" {
|
|
||||||
dir = filepath.Join(ss.dirs[0], skillName)
|
|
||||||
}
|
|
||||||
if err := ss.mkdirAll(dir, 0755); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return ss.writeFile(filepath.Join(dir, "SKILL.md"), data, 0644)
|
return ss.cfg.WriteFile(filepath.Join(dir, "SKILL.md"), nil, 0644)
|
||||||
},
|
|
||||||
BlockingReadFn: notBlocking,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) del(name string) error {
|
func (ss *skillHelper) del(_ []string, name string) error {
|
||||||
skillName := strings.TrimSuffix(name, ".md")
|
skillName := strings.TrimSuffix(name, ".md")
|
||||||
for _, m := range ss.listSkills() {
|
for _, m := range ss.listSkills() {
|
||||||
if m.Name == skillName {
|
if m.Name == skillName {
|
||||||
return ss.removeAll(m.Dir)
|
return ss.cfg.RemoveAll(m.Dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("skill not found: %s", skillName)
|
return fmt.Errorf("skill not found: %s", skillName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) ren(oldName, newName string) error {
|
func (ss *skillHelper) rename(_ []string, old, new string) error {
|
||||||
oldSkill := strings.TrimSuffix(oldName, ".md")
|
oldSkill := strings.TrimSuffix(old, ".md")
|
||||||
newSkill := strings.TrimSuffix(newName, ".md")
|
newSkill := strings.TrimSuffix(new, ".md")
|
||||||
for _, m := range ss.listSkills() {
|
for _, m := range ss.listSkills() {
|
||||||
if m.Name == oldSkill {
|
if m.Name == oldSkill {
|
||||||
newDir := filepath.Join(filepath.Dir(m.Dir), newSkill)
|
newDir := filepath.Join(filepath.Dir(m.Dir), newSkill)
|
||||||
return ss.rename(m.Dir, newDir)
|
return ss.cfg.Rename(m.Dir, newDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("skill not found: %s", oldSkill)
|
return fmt.Errorf("skill not found: %s", oldSkill)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *skillState) create(name string) error {
|
func (ss *skillHelper) index(_ Store) ([]byte, error) {
|
||||||
skillName := strings.TrimSuffix(name, ".md")
|
|
||||||
dir := filepath.Join(ss.dirs[0], skillName)
|
|
||||||
if err := ss.mkdirAll(dir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ss.writeFile(filepath.Join(dir, "SKILL.md"), nil, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *skillState) index() ([]byte, error) {
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, m := range ss.listSkills() {
|
for _, m := range ss.listSkills() {
|
||||||
fmt.Fprintf(&sb, "## %s\n", m.Name)
|
fmt.Fprintf(&sb, "## %s\n", m.Name)
|
||||||
|
|
|
||||||
185
store/store.go
185
store/store.go
|
|
@ -4,9 +4,7 @@ package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -87,189 +85,6 @@ func (s *storeConfig) Create(name string) error { return s.CreateF
|
||||||
func (s *storeConfig) Delete(name string) error { return s.DeleteFn(name) }
|
func (s *storeConfig) Delete(name string) error { return s.DeleteFn(name) }
|
||||||
func (s *storeConfig) Rename(old, new string) error { return s.RenameFn(old, new) }
|
func (s *storeConfig) Rename(old, new string) error { return s.RenameFn(old, new) }
|
||||||
|
|
||||||
// NewFlatDir returns a Store backed by a directory on the local filesystem.
|
|
||||||
func NewFlatDir(dir string, perm os.FileMode) Store {
|
|
||||||
join := func(name string) string { return filepath.Join(dir, name) }
|
|
||||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
|
||||||
return nil, "", fmt.Errorf("blocking read not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storeConfig{
|
|
||||||
StatFn: func(name string) (os.FileInfo, error) { return os.Stat(join(name)) },
|
|
||||||
ListFn: func() ([]os.DirEntry, error) { return os.ReadDir(dir) },
|
|
||||||
OpenFn: func(name string) (StoreEntry, error) {
|
|
||||||
path := join(name)
|
|
||||||
return &EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return os.Stat(path) },
|
|
||||||
ReadFn: func() ([]byte, error) { return os.ReadFile(path) },
|
|
||||||
WriteFn: func(data []byte) error {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(path, data, perm)
|
|
||||||
},
|
|
||||||
BlockingReadFn: notBlocking,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
CreateFn: func(name string) error {
|
|
||||||
path := join(name)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(path, nil, perm)
|
|
||||||
},
|
|
||||||
DeleteFn: func(name string) error { return os.Remove(join(name)) },
|
|
||||||
RenameFn: func(old, new string) error { return os.Rename(join(old), join(new)) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFlatDirWritableUnion returns a Store that reads from all dirs but writes to the first.
|
|
||||||
// Files are deduplicated by name; the first directory containing a name wins.
|
|
||||||
func NewFlatDirWritableUnion(dirs []string, perm os.FileMode) Store {
|
|
||||||
writeDir := dirs[0]
|
|
||||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
|
||||||
return nil, "", fmt.Errorf("blocking read not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve := func(name string) (string, error) {
|
|
||||||
for _, dir := range dirs {
|
|
||||||
p := filepath.Join(dir, name)
|
|
||||||
if _, err := os.Stat(p); err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("%s: not found", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storeConfig{
|
|
||||||
StatFn: func(name string) (os.FileInfo, error) {
|
|
||||||
p, err := resolve(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return os.Stat(p)
|
|
||||||
},
|
|
||||||
ListFn: func() ([]os.DirEntry, error) {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
var result []os.DirEntry
|
|
||||||
for _, dir := range 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
},
|
|
||||||
OpenFn: func(name string) (StoreEntry, error) {
|
|
||||||
p, err := resolve(name)
|
|
||||||
if err != nil {
|
|
||||||
// Not found anywhere; write path is in writeDir.
|
|
||||||
p = filepath.Join(writeDir, name)
|
|
||||||
}
|
|
||||||
wp := filepath.Join(writeDir, name)
|
|
||||||
return &EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return os.Stat(p) },
|
|
||||||
ReadFn: func() ([]byte, error) { return os.ReadFile(p) },
|
|
||||||
WriteFn: func(data []byte) error {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(wp), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(wp, data, perm)
|
|
||||||
},
|
|
||||||
BlockingReadFn: notBlocking,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
CreateFn: func(name string) error {
|
|
||||||
p := filepath.Join(writeDir, name)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(p, nil, perm)
|
|
||||||
},
|
|
||||||
DeleteFn: func(name string) error {
|
|
||||||
// Delete from wherever it exists.
|
|
||||||
p, err := resolve(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Remove(p)
|
|
||||||
},
|
|
||||||
RenameFn: func(old, new string) error {
|
|
||||||
p, err := resolve(old)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Rename(p, filepath.Join(filepath.Dir(p), new))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFlatDirUnion returns a read-only Store that merges multiple directories.
|
|
||||||
// Files are deduplicated by name; the first directory containing a name wins.
|
|
||||||
func NewFlatDirUnion(dirs []string, perm os.FileMode) Store {
|
|
||||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
|
||||||
return nil, "", fmt.Errorf("blocking read not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve := func(name string) (string, error) {
|
|
||||||
for _, dir := range dirs {
|
|
||||||
p := filepath.Join(dir, name)
|
|
||||||
if _, err := os.Stat(p); err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("%s: not found", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storeConfig{
|
|
||||||
StatFn: func(name string) (os.FileInfo, error) {
|
|
||||||
p, err := resolve(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return os.Stat(p)
|
|
||||||
},
|
|
||||||
ListFn: func() ([]os.DirEntry, error) {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
var result []os.DirEntry
|
|
||||||
for _, dir := range 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
},
|
|
||||||
OpenFn: func(name string) (StoreEntry, error) {
|
|
||||||
p, err := resolve(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return os.Stat(p) },
|
|
||||||
ReadFn: func() ([]byte, error) { return os.ReadFile(p) },
|
|
||||||
WriteFn: func([]byte) error { return fmt.Errorf("%s: read-only", name) },
|
|
||||||
BlockingReadFn: notBlocking,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
CreateFn: func(string) error { return fmt.Errorf("read-only store") },
|
|
||||||
DeleteFn: func(string) error { return fmt.Errorf("read-only store") },
|
|
||||||
RenameFn: func(string, string) error { return fmt.Errorf("read-only store") },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyntheticFileInfo implements os.FileInfo for entries with no backing file.
|
// SyntheticFileInfo implements os.FileInfo for entries with no backing file.
|
||||||
type SyntheticFileInfo struct {
|
type SyntheticFileInfo struct {
|
||||||
Name_ string
|
Name_ string
|
||||||
|
|
|
||||||
|
|
@ -323,18 +323,18 @@ func checkStoreContract(t *testing.T, s store.Store, name string) {
|
||||||
// ===== FlatDir =====
|
// ===== FlatDir =====
|
||||||
|
|
||||||
func TestFlatDirContract(t *testing.T) {
|
func TestFlatDirContract(t *testing.T) {
|
||||||
checkStoreContract(t, store.NewFlatDir(t.TempDir(), 0644), "test-file")
|
checkStoreContract(t, store.NewDirStore([]string{t.TempDir()}, 0644), "test-file")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlatDirCreateMkdirError(t *testing.T) {
|
func TestFlatDirCreateMkdirError(t *testing.T) {
|
||||||
fd := store.NewFlatDir("/nonexistent/path", 0644)
|
fd := store.NewDirStore([]string{"/nonexistent/path"}, 0644)
|
||||||
if err := fd.Create("f"); err == nil {
|
if err := fd.Create("f"); err == nil {
|
||||||
t.Error("Create should fail when dir doesn't exist")
|
t.Error("Create should fail when dir doesn't exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlatDirPutMkdirError(t *testing.T) {
|
func TestFlatDirPutMkdirError(t *testing.T) {
|
||||||
fd := store.NewFlatDir("/nonexistent/path", 0644)
|
fd := store.NewDirStore([]string{"/nonexistent/path"}, 0644)
|
||||||
e, err := fd.Open("f")
|
e, err := fd.Open("f")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Open: %v", err)
|
t.Fatalf("Open: %v", err)
|
||||||
|
|
|
||||||
Reference in New Issue