316 lines
9.8 KiB
Go
316 lines
9.8 KiB
Go
// Package store defines storage interfaces and a filesystem-backed
|
|
// implementation used by the 9P server.
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// StoreEntry is an open handle to a single named blob.
|
|
type StoreEntry interface {
|
|
Stat() (os.FileInfo, error)
|
|
Read() ([]byte, error)
|
|
Write(data []byte) error
|
|
BlockingRead(ctx context.Context, base string) (content []byte, nextBase string, err error)
|
|
// OneShot returns true if the entry yields data once per open (offset>0 → EOF).
|
|
OneShot() bool
|
|
// IsBlocking returns true if Read blocks until data is available (use BlockingRead).
|
|
IsBlocking() bool
|
|
// Async returns true if writes should be dispatched asynchronously.
|
|
Async() bool
|
|
}
|
|
|
|
// Store is a named collection of entries.
|
|
type Store interface {
|
|
Stat(name string) (os.FileInfo, error)
|
|
List() ([]os.DirEntry, error)
|
|
Open(name string) (StoreEntry, error)
|
|
Create(name string) error
|
|
Delete(name string) error
|
|
Rename(oldName, newName string) error
|
|
}
|
|
|
|
// Runnable is a running agent that can be observed and controlled.
|
|
type Runnable interface {
|
|
RunnableID() string
|
|
Cancel()
|
|
Interrupt()
|
|
AppendLog([]byte)
|
|
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)
|
|
ReadFn func() ([]byte, error)
|
|
WriteFn func([]byte) error
|
|
BlockingReadFn func(context.Context, string) ([]byte, string, error)
|
|
OneShot_ bool
|
|
IsBlocking_ bool
|
|
Async_ bool
|
|
}
|
|
|
|
func (e *EntryConfig) Stat() (os.FileInfo, error) { return e.StatFn() }
|
|
func (e *EntryConfig) Read() ([]byte, error) { return e.ReadFn() }
|
|
func (e *EntryConfig) Write(data []byte) error { return e.WriteFn(data) }
|
|
func (e *EntryConfig) BlockingRead(ctx context.Context, base string) ([]byte, string, error) {
|
|
return e.BlockingReadFn(ctx, base)
|
|
}
|
|
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) }
|
|
|
|
// 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.
|
|
type SyntheticFileInfo struct {
|
|
Name_ string
|
|
Mode_ os.FileMode
|
|
Size_ int64
|
|
IsDir_ bool
|
|
}
|
|
|
|
func (f *SyntheticFileInfo) Name() string { return f.Name_ }
|
|
func (f *SyntheticFileInfo) Size() int64 { return f.Size_ }
|
|
func (f *SyntheticFileInfo) Mode() os.FileMode { return f.Mode_ }
|
|
func (f *SyntheticFileInfo) ModTime() time.Time { return time.Time{} }
|
|
func (f *SyntheticFileInfo) IsDir() bool { return f.IsDir_ }
|
|
func (f *SyntheticFileInfo) Sys() any { return nil }
|
|
|
|
// SyntheticEntry implements os.DirEntry for entries with no backing file.
|
|
type SyntheticEntry struct {
|
|
Name_ string
|
|
Mode_ os.FileMode
|
|
IsDir_ bool
|
|
}
|
|
|
|
func (e *SyntheticEntry) Name() string { return e.Name_ }
|
|
func (e *SyntheticEntry) IsDir() bool { return e.IsDir_ }
|
|
func (e *SyntheticEntry) Type() os.FileMode {
|
|
if e.IsDir_ {
|
|
return os.ModeDir
|
|
}
|
|
return 0
|
|
}
|
|
func (e *SyntheticEntry) Info() (os.FileInfo, error) {
|
|
return &SyntheticFileInfo{Name_: e.Name_, Mode_: e.Mode_, IsDir_: e.IsDir_}, nil
|
|
}
|
|
|
|
// FileEntry returns a synthetic file DirEntry.
|
|
func FileEntry(name string, mode os.FileMode) os.DirEntry {
|
|
return &SyntheticEntry{Name_: name, Mode_: mode}
|
|
}
|
|
|
|
// DirEntry returns a synthetic directory DirEntry.
|
|
func DirEntry(name string, mode os.FileMode) os.DirEntry {
|
|
return &SyntheticEntry{Name_: name, Mode_: mode, IsDir_: true}
|
|
}
|