one store type: DirStore is the only concrete Store implementation

- RunFileStore replaced by NewFileStore() returning *DirStore
- SessionStore now embeds *DirStore with hooks
- Removed: storeConfig, RunnableStore, RunFileStore, runfilestore.go
- New filestore.go: NewFileStore builds DirStore from FileSpec table
- DirStore.Stat uses opener for synthetic files
- Runnable concern stays on Session (not on store)
This commit is contained in:
Levi Neely 2026-05-19 17:21:16 +02:00
parent 30e171c1ba
commit f5841c4a0f
7 changed files with 106 additions and 150 deletions

View File

@ -28,7 +28,6 @@ import (
type ( type (
StoreEntry = store.StoreEntry StoreEntry = store.StoreEntry
Store = store.Store Store = store.Store
RunnableStore = store.RunnableStore
Session = store.Session Session = store.Session
SessionStore = store.SessionStore SessionStore = store.SessionStore

View File

@ -148,7 +148,6 @@ func (d *DirStore) Stat(name string) (os.FileInfo, error) {
// Check children first. // Check children first.
if child, rest, ok := d.split(name); ok { if child, rest, ok := d.split(name); ok {
if rest == "" { if rest == "" {
// The child directory itself.
return &SyntheticFileInfo{Name_: name, Mode_: 0555, IsDir_: true}, nil return &SyntheticFileInfo{Name_: name, Mode_: 0555, IsDir_: true}, nil
} }
return child.Stat(rest) return child.Stat(rest)
@ -156,6 +155,14 @@ func (d *DirStore) Stat(name string) (os.FileInfo, error) {
if name == "idx" && d.indexFn != nil { if name == "idx" && d.indexFn != nil {
return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil
} }
// Synthetic files: use opener for stat.
if d.openFn != nil {
e, err := d.openFn(d.dirs, name)
if err != nil {
return nil, err
}
return e.Stat()
}
p, err := d.resolve(name) p, err := d.resolve(name)
if err != nil { if err != nil {
return nil, err return nil, err

80
store/filestore.go Normal file
View File

@ -0,0 +1,80 @@
package store
import (
"context"
"fmt"
"os"
)
// FileSpec describes a single synthetic file exposed by a DirStore.
type FileSpec struct {
Name string
Mode os.FileMode
Read func() ([]byte, error)
Write func([]byte) error // nil = read-only
Wait func(ctx context.Context, base string) (content []byte, nextBase string, err error) // nil = not waitable
Size func() int64 // optional; if nil, len(Read())
OneShot bool // true = yields data once per open
Async bool // true = writes dispatched asynchronously
}
// NewFileStore creates a DirStore backed by a table of FileSpecs (synthetic files).
func NewFileStore(specs []FileSpec) *DirStore {
index := make(map[string]int, len(specs))
for i, s := range specs {
index[s.Name] = i
}
lookup := func(name string) (*FileSpec, bool) {
i, ok := index[name]
if !ok {
return nil, false
}
return &specs[i], true
}
return NewDirStore(nil, 0,
WithLister(func(_ []string) ([]os.DirEntry, error) {
entries := make([]os.DirEntry, len(specs))
for i, s := range specs {
entries[i] = FileEntry(s.Name, s.Mode)
}
return entries, nil
}),
WithOpener(func(_ []string, name string) (StoreEntry, error) {
spec, ok := lookup(name)
if !ok {
return nil, fmt.Errorf("%s: not found", name)
}
writeFn := func([]byte) error { return fmt.Errorf("%s: read-only", name) }
if spec.Write != nil {
writeFn = spec.Write
}
waitFn := func(context.Context, string) ([]byte, string, error) {
return nil, "", fmt.Errorf("%s: not a wait file", name)
}
if spec.Wait != nil {
waitFn = spec.Wait
}
return &EntryConfig{
StatFn: func() (os.FileInfo, error) {
var size int64
if spec.Size != nil {
size = spec.Size()
} else if spec.Wait != nil {
size = 4096
} else if data, err := spec.Read(); err == nil {
size = int64(len(data))
}
return &SyntheticFileInfo{Name_: name, Mode_: spec.Mode, Size_: size}, nil
},
ReadFn: spec.Read,
WriteFn: writeFn,
BlockingReadFn: waitFn,
OneShot_: spec.OneShot || spec.Wait != nil,
IsBlocking_: spec.Wait != nil,
Async_: spec.Async,
}, nil
}),
)
}

View File

@ -1,110 +0,0 @@
package store
import (
"context"
"fmt"
"os"
)
// FileSpec describes a single synthetic file exposed by a RunFileStore.
type FileSpec struct {
Name string
Mode os.FileMode
Read func() ([]byte, error)
Write func([]byte) error // nil = read-only
Wait func(ctx context.Context, base string) (content []byte, nextBase string, err error) // nil = not waitable
Size func() int64 // optional; if nil, len(Read())
OneShot bool // true = yields data once per open
Async bool // true = writes dispatched asynchronously
}
// RunFileStore implements RunnableStore for any Runnable using a table of FileSpecs.
type RunFileStore struct {
*storeConfig
Runnable
specs []FileSpec
index map[string]int // name -> index into specs
}
// NewRunFileStore creates a RunnableStore backed by the given Runnable and file table.
func NewRunFileStore(r Runnable, specs []FileSpec) *RunFileStore {
rs := &RunFileStore{
Runnable: r,
specs: specs,
index: make(map[string]int, len(specs)),
}
for i, s := range specs {
rs.index[s.Name] = i
}
notSupported := func(string) error { return fmt.Errorf("not supported") }
rs.storeConfig = &storeConfig{
StatFn: rs.stat,
ListFn: rs.list,
OpenFn: rs.open,
DeleteFn: notSupported,
CreateFn: notSupported,
RenameFn: func(string, string) error { return fmt.Errorf("not supported") },
}
return rs
}
func (rs *RunFileStore) lookup(name string) (*FileSpec, bool) {
i, ok := rs.index[name]
if !ok {
return nil, false
}
return &rs.specs[i], true
}
func (rs *RunFileStore) stat(name string) (os.FileInfo, error) {
spec, ok := rs.lookup(name)
if !ok {
return nil, fmt.Errorf("%s: not found", name)
}
var size int64
if spec.Size != nil {
size = spec.Size()
} else if spec.Wait != nil {
// Wait files block on read; report large size so clients read fully.
size = 4096
} else {
if data, err := spec.Read(); err == nil {
size = int64(len(data))
}
}
return &SyntheticFileInfo{Name_: name, Mode_: spec.Mode, Size_: size}, nil
}
func (rs *RunFileStore) list() ([]os.DirEntry, error) {
entries := make([]os.DirEntry, len(rs.specs))
for i, s := range rs.specs {
entries[i] = FileEntry(s.Name, s.Mode)
}
return entries, nil
}
func (rs *RunFileStore) open(name string) (StoreEntry, error) {
spec, ok := rs.lookup(name)
if !ok {
return nil, fmt.Errorf("%s: not found", name)
}
writeFn := func(data []byte) error { return fmt.Errorf("%s: read-only", name) }
if spec.Write != nil {
writeFn = spec.Write
}
waitFn := func(context.Context, string) ([]byte, string, error) {
return nil, "", fmt.Errorf("%s: not a wait file", name)
}
if spec.Wait != nil {
waitFn = spec.Wait
}
return &EntryConfig{
StatFn: func() (os.FileInfo, error) { return rs.stat(name) },
ReadFn: spec.Read,
WriteFn: writeFn,
BlockingReadFn: waitFn,
OneShot_: spec.OneShot || spec.Wait != nil,
IsBlocking_: spec.Wait != nil,
Async_: spec.Async,
}, nil
}

View File

@ -124,7 +124,7 @@ type SessionStoreConfig struct {
// SessionStore implements Store for session management. // SessionStore implements Store for session management.
type SessionStore struct { type SessionStore struct {
*storeConfig *DirStore
cfg SessionStoreConfig cfg SessionStoreConfig
mu sync.RWMutex mu sync.RWMutex
sessions map[string]*Session sessions map[string]*Session
@ -148,14 +148,13 @@ func NewSessionStore(cfg SessionStoreConfig) *SessionStore {
sessions: make(map[string]*Session), sessions: make(map[string]*Session),
} }
ss.nextUID.Store(9999) ss.nextUID.Store(9999)
ss.storeConfig = &storeConfig{ ss.DirStore = NewDirStore(nil, 0,
StatFn: ss.stat, WithOpener(func(_ []string, name string) (StoreEntry, error) { return ss.openEntry(name) }),
ListFn: ss.list, WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }),
OpenFn: ss.openEntry, WithDeleter(func(_ []string, name string) error { return ss.del(name) }),
DeleteFn: ss.del, WithCreator(func(_ []string, name string, _ os.FileMode) error { return ss.create(name) }),
CreateFn: ss.create, WithRenamer(func(_ []string, old, new string) error { return ss.renameSession(old, new) }),
RenameFn: ss.renameSession, )
}
return ss return ss
} }
@ -216,6 +215,10 @@ func (s *SessionStore) ListDir(name string) ([]os.DirEntry, error) {
return nil, fmt.Errorf("%s: not a directory", name) return nil, fmt.Errorf("%s: not a directory", name)
} }
func (s *SessionStore) Stat(name string) (os.FileInfo, error) {
return s.stat(name)
}
func (s *SessionStore) stat(name string) (os.FileInfo, error) { func (s *SessionStore) stat(name string) (os.FileInfo, error) {
// Top-level fixed files (new, idx, sh, etc.) // Top-level fixed files (new, idx, sh, etc.)
if mode, ok := sessionStoreFiles[name]; ok { if mode, ok := sessionStoreFiles[name]; ok {
@ -428,15 +431,15 @@ func (s *SessionStore) SessionByUname(uname string) *Session {
return nil return nil
} }
// OpenStore returns a RunnableStore for the given session ID. // OpenStore returns a DirStore for the given session ID.
func (s *SessionStore) OpenStore(id string) (RunnableStore, error) { func (s *SessionStore) OpenStore(id string) (*DirStore, error) {
if sess := s.Session(id); sess != nil { if sess := s.Session(id); sess != nil {
return s.openStore(sess) return s.openStore(sess)
} }
return nil, fmt.Errorf("session not found: %s", id) return nil, fmt.Errorf("session not found: %s", id)
} }
func (s *SessionStore) openStore(sess *Session) (RunnableStore, error) { func (s *SessionStore) openStore(sess *Session) (*DirStore, error) {
id := sess.RunnableID() id := sess.RunnableID()
return NewSessionFileStore( return NewSessionFileStore(
sess, sess,

View File

@ -41,8 +41,8 @@ var SessionFileList = []struct {
{"context", 0444, false, false}, {"context", 0444, false, false},
} }
// SessionFileStore is a RunFileStore for a session directory. // SessionFileStore is a DirStore for a session directory.
type SessionFileStore = RunFileStore type SessionFileStore = DirStore
func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore { func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript} h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript}
@ -52,7 +52,7 @@ func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename fu
specs[i].OneShot = f.OneShot specs[i].OneShot = f.OneShot
specs[i].Async = f.Async specs[i].Async = f.Async
} }
return NewRunFileStore(sess, specs) return NewFileStore(specs)
} }
// sessionHelper holds the dependencies needed to build session FileSpecs. // sessionHelper holds the dependencies needed to build session FileSpecs.

View File

@ -41,12 +41,6 @@ type Runnable interface {
LogInfo() (length int, vers uint32) LogInfo() (length int, vers uint32)
} }
// RunnableStore is a Store backed by a running agent.
type RunnableStore interface {
Store
Runnable
}
// EntryConfig implements StoreEntry via function pointers. // EntryConfig implements StoreEntry via function pointers.
type EntryConfig struct { type EntryConfig struct {
StatFn func() (os.FileInfo, error) StatFn func() (os.FileInfo, error)
@ -68,23 +62,6 @@ func (e *EntryConfig) OneShot() bool { return e.OneShot_ }
func (e *EntryConfig) IsBlocking() bool { return e.IsBlocking_ } func (e *EntryConfig) IsBlocking() bool { return e.IsBlocking_ }
func (e *EntryConfig) Async() bool { return e.Async_ } func (e *EntryConfig) Async() bool { return e.Async_ }
// storeConfig implements Store via function pointers.
type storeConfig struct {
StatFn func(string) (os.FileInfo, error)
ListFn func() ([]os.DirEntry, error)
OpenFn func(string) (StoreEntry, error)
CreateFn func(string) error
DeleteFn func(string) error
RenameFn func(string, string) error
}
func (s *storeConfig) Stat(name string) (os.FileInfo, error) { return s.StatFn(name) }
func (s *storeConfig) List() ([]os.DirEntry, error) { return s.ListFn() }
func (s *storeConfig) Open(name string) (StoreEntry, error) { return s.OpenFn(name) }
func (s *storeConfig) Create(name string) error { return s.CreateFn(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) }
// 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