230 lines
7.2 KiB
Go
230 lines
7.2 KiB
Go
// Package virtfs provides an embedded domain-specific language for declaring
|
|
// synthetic filesystems. It is designed for building 9P namespaces where the
|
|
// entire filesystem structure is declared statically but populated dynamically.
|
|
//
|
|
// Handlers are plain closures that capture their state at binding time.
|
|
// There is no context type parameter — each handler already knows its target.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// users := getUserList()
|
|
// spec := DirNode("/",
|
|
// FileNode("version", 0444, Read(func() ([]byte, error) {
|
|
// return []byte("1.0\n"), nil
|
|
// })),
|
|
// Each("{user}", func() ([]FsNodeDecl, error) {
|
|
// var out []FsNodeDecl
|
|
// for _, u := range users {
|
|
// user := u
|
|
// out = append(out, FsNodeDecl{
|
|
// Name: user.Name,
|
|
// Children: []FsNodeDecl{
|
|
// FileNode("name", 0444, Read(func() ([]byte, error) {
|
|
// return []byte(user.Name + "\n"), nil
|
|
// })),
|
|
// },
|
|
// })
|
|
// }
|
|
// return out, nil
|
|
// }),
|
|
// )
|
|
//
|
|
// tree := BuildTree(spec)
|
|
package virtfs
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
)
|
|
|
|
// FsNodeDecl describes one node in a filesystem namespace.
|
|
// Handlers are closures — they capture their target at construction time.
|
|
type FsNodeDecl struct {
|
|
// Identity.
|
|
Name string
|
|
Aliases []string // alternate names that resolve to this node
|
|
UID string // "" = inherit from parent
|
|
GID string // "" = inherit from parent
|
|
Mode os.FileMode // 0 = inherit parent default
|
|
Desc string // human-readable description
|
|
|
|
// Tstat — metadata override.
|
|
Stat func() os.FileInfo
|
|
|
|
// Twalk — children (implies directory; at most one set).
|
|
Children []FsNodeDecl // static, known at compile time
|
|
Bindings func() ([]FsNodeDecl, error) // Each: dynamic children
|
|
|
|
// Tread (non-blocking) + Twrite.
|
|
Read func() ([]byte, error)
|
|
Write func(data []byte) error
|
|
|
|
// Tread — blocking variants (at most one set).
|
|
BlockOnce func(ctx context.Context, base string) ([]byte, string, error)
|
|
Stream func(ctx context.Context, base string) ([]byte, string, error)
|
|
|
|
// Rdwr — atomic write-then-read: write triggers computation, read returns result once.
|
|
Rdwr func(ctx context.Context, data []byte) ([]byte, error)
|
|
|
|
// Tremove + Trename.
|
|
Remove func() error
|
|
Rename func(newName string) error
|
|
}
|
|
|
|
// NodeOption configures a FsNodeDecl at construction time.
|
|
type NodeOption func(*FsNodeDecl)
|
|
|
|
// ── EDSL constructors ────────────────────────────────────────────
|
|
|
|
// DirNode declares a static directory node.
|
|
func DirNode(name string, args ...any) FsNodeDecl {
|
|
d := FsNodeDecl{Name: name}
|
|
for _, a := range args {
|
|
switch v := a.(type) {
|
|
case FsNodeDecl:
|
|
d.Children = append(d.Children, v)
|
|
case []FsNodeDecl:
|
|
d.Children = append(d.Children, v...)
|
|
case NodeOption:
|
|
v(&d)
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Each declares a directory whose children are produced dynamically.
|
|
func Each(name string, list func() ([]FsNodeDecl, error), opts ...NodeOption) FsNodeDecl {
|
|
d := FsNodeDecl{Name: name, Bindings: list}
|
|
for _, o := range opts {
|
|
o(&d)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// FileNode declares a file node (non-directory).
|
|
func FileNode(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl {
|
|
d := FsNodeDecl{Name: name, Mode: mode}
|
|
for _, o := range opts {
|
|
o(&d)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// ── Options ──────────────────────────────────────────────────────
|
|
|
|
// Read sets the read handler for a file node.
|
|
func Read(fn func() ([]byte, error)) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Read = fn }
|
|
}
|
|
|
|
// Write sets the write handler for a file node.
|
|
func Write(fn func([]byte) error) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Write = fn }
|
|
}
|
|
|
|
// Stream sets a streaming read handler that blocks indefinitely.
|
|
// readFn returns (data, nextBase, error) given the current base.
|
|
// signal returns a channel that is closed when new data may be available.
|
|
// The framework blocks on signal until readFn returns non-empty data.
|
|
func Stream(readFn func(base string) ([]byte, string, error), signal func() <-chan struct{}) NodeOption {
|
|
return func(d *FsNodeDecl) {
|
|
d.Stream = func(ctx context.Context, base string) ([]byte, string, error) {
|
|
for {
|
|
data, nextBase, err := readFn(base)
|
|
if err != nil {
|
|
return nil, base, err
|
|
}
|
|
if len(data) > 0 {
|
|
return data, nextBase, nil
|
|
}
|
|
base = nextBase
|
|
select {
|
|
case <-signal():
|
|
case <-ctx.Done():
|
|
return nil, base, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// StreamRaw sets a raw streaming read handler with custom blocking logic.
|
|
func StreamRaw(fn func(context.Context, string) ([]byte, string, error)) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Stream = fn }
|
|
}
|
|
|
|
// BlockOnce sets a blocking read handler that returns once per open.
|
|
// readFn returns (data, hash, error) — the current value.
|
|
// signal returns a channel that is closed when the value may have changed.
|
|
// The framework blocks until readFn returns a hash different from base.
|
|
func BlockOnce(readFn func() ([]byte, string, error), signal func() <-chan struct{}) NodeOption {
|
|
return func(d *FsNodeDecl) {
|
|
d.BlockOnce = func(ctx context.Context, base string) ([]byte, string, error) {
|
|
for {
|
|
data, hash, err := readFn()
|
|
if err != nil {
|
|
return nil, base, err
|
|
}
|
|
if base == "" {
|
|
base = hash
|
|
}
|
|
if hash != base {
|
|
return data, hash, nil
|
|
}
|
|
select {
|
|
case <-signal():
|
|
case <-ctx.Done():
|
|
return nil, "", nil // timeout: empty response, client re-opens
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BlockOnceRaw sets a blocking read handler with custom blocking logic.
|
|
// Use for queue-style consumers where there is no "current value" to compare.
|
|
func BlockOnceRaw(fn func(context.Context, string) ([]byte, string, error)) NodeOption {
|
|
return func(d *FsNodeDecl) { d.BlockOnce = fn }
|
|
}
|
|
|
|
// Rdwr sets an atomic write-then-read handler.
|
|
// Write triggers computation; the subsequent read returns the result once.
|
|
func Rdwr(fn func(context.Context, []byte) ([]byte, error)) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Rdwr = fn }
|
|
}
|
|
|
|
// RemoveNode sets the removal handler for a node.
|
|
func RemoveNode(fn func() error) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Remove = fn }
|
|
}
|
|
|
|
// RenameNode sets the rename handler for a node.
|
|
func RenameNode(fn func(string) error) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Rename = fn }
|
|
}
|
|
|
|
// Alias adds alternate lookup names for this node.
|
|
func Alias(names ...string) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Aliases = append(d.Aliases, names...) }
|
|
}
|
|
|
|
// StatOverride sets a custom stat handler.
|
|
func StatOverride(fn func() os.FileInfo) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Stat = fn }
|
|
}
|
|
|
|
// UID sets the owner for a node.
|
|
func UID(uid string) NodeOption {
|
|
return func(d *FsNodeDecl) { d.UID = uid }
|
|
}
|
|
|
|
// GID sets the group for a node.
|
|
func GID(gid string) NodeOption {
|
|
return func(d *FsNodeDecl) { d.GID = gid }
|
|
}
|
|
|
|
// Doc sets a human-readable description for the node.
|
|
func Doc(desc string) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Desc = desc }
|
|
}
|