diff --git a/server.go b/server.go index 0083b85..8266336 100644 --- a/server.go +++ b/server.go @@ -28,7 +28,6 @@ import ( type ( StoreEntry = store.StoreEntry Store = store.Store - RunnableStore = store.RunnableStore Session = store.Session SessionStore = store.SessionStore diff --git a/store/dirstore.go b/store/dirstore.go index 3961759..1b8b180 100644 --- a/store/dirstore.go +++ b/store/dirstore.go @@ -148,7 +148,6 @@ 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) @@ -156,6 +155,14 @@ func (d *DirStore) Stat(name string) (os.FileInfo, error) { if name == "idx" && d.indexFn != 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) if err != nil { return nil, err diff --git a/store/filestore.go b/store/filestore.go new file mode 100644 index 0000000..af70b47 --- /dev/null +++ b/store/filestore.go @@ -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 + }), + ) +} diff --git a/store/runfilestore.go b/store/runfilestore.go deleted file mode 100644 index f7d6a0d..0000000 --- a/store/runfilestore.go +++ /dev/null @@ -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 -} diff --git a/store/session.go b/store/session.go index b33bc5e..e0e3463 100644 --- a/store/session.go +++ b/store/session.go @@ -124,7 +124,7 @@ type SessionStoreConfig struct { // SessionStore implements Store for session management. type SessionStore struct { - *storeConfig + *DirStore cfg SessionStoreConfig mu sync.RWMutex sessions map[string]*Session @@ -148,14 +148,13 @@ func NewSessionStore(cfg SessionStoreConfig) *SessionStore { sessions: make(map[string]*Session), } ss.nextUID.Store(9999) - ss.storeConfig = &storeConfig{ - StatFn: ss.stat, - ListFn: ss.list, - OpenFn: ss.openEntry, - DeleteFn: ss.del, - CreateFn: ss.create, - RenameFn: ss.renameSession, - } + ss.DirStore = NewDirStore(nil, 0, + WithOpener(func(_ []string, name string) (StoreEntry, error) { return ss.openEntry(name) }), + WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }), + WithDeleter(func(_ []string, name string) error { return ss.del(name) }), + WithCreator(func(_ []string, name string, _ os.FileMode) error { return ss.create(name) }), + WithRenamer(func(_ []string, old, new string) error { return ss.renameSession(old, new) }), + ) return ss } @@ -216,6 +215,10 @@ func (s *SessionStore) ListDir(name string) ([]os.DirEntry, error) { 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) { // Top-level fixed files (new, idx, sh, etc.) if mode, ok := sessionStoreFiles[name]; ok { @@ -428,15 +431,15 @@ func (s *SessionStore) SessionByUname(uname string) *Session { return nil } -// OpenStore returns a RunnableStore for the given session ID. -func (s *SessionStore) OpenStore(id string) (RunnableStore, error) { +// OpenStore returns a DirStore for the given session ID. +func (s *SessionStore) OpenStore(id string) (*DirStore, error) { if sess := s.Session(id); sess != nil { return s.openStore(sess) } 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() return NewSessionFileStore( sess, diff --git a/store/sessionfile.go b/store/sessionfile.go index c185882..7e0df79 100644 --- a/store/sessionfile.go +++ b/store/sessionfile.go @@ -41,8 +41,8 @@ var SessionFileList = []struct { {"context", 0444, false, false}, } -// SessionFileStore is a RunFileStore for a session directory. -type SessionFileStore = RunFileStore +// SessionFileStore is a DirStore for a session directory. +type SessionFileStore = DirStore 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} @@ -52,7 +52,7 @@ func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename fu specs[i].OneShot = f.OneShot specs[i].Async = f.Async } - return NewRunFileStore(sess, specs) + return NewFileStore(specs) } // sessionHelper holds the dependencies needed to build session FileSpecs. diff --git a/store/store.go b/store/store.go index 968f6e0..f5aa8df 100644 --- a/store/store.go +++ b/store/store.go @@ -41,12 +41,6 @@ type Runnable interface { 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. type EntryConfig struct { 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) 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. type SyntheticFileInfo struct { Name_ string