174 lines
5.3 KiB
Go
174 lines
5.3 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() ([]Binding, error) {
|
|
// var out []Binding
|
|
// for _, u := range users {
|
|
// user := u
|
|
// out = append(out, Binding{
|
|
// 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() ([]Binding, 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)
|
|
|
|
// Request — rdwr: write triggers result, read returns it once.
|
|
Request func(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)
|
|
|
|
// Binding provides identity and pre-bound children for one iteration of Each.
|
|
type Binding struct {
|
|
Name string
|
|
Aliases []string
|
|
UID string
|
|
GID string
|
|
Children []FsNodeDecl // fully-bound subtree (handlers are closures)
|
|
Remove func()
|
|
Rename func(string) error
|
|
}
|
|
|
|
// ── 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 NodeOption:
|
|
v(&d)
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Each declares a directory whose children are produced dynamically.
|
|
// The bindings function returns pre-bound children for each instance.
|
|
func Each(name string, bindings func() ([]Binding, error), opts ...NodeOption) FsNodeDecl {
|
|
d := FsNodeDecl{Name: name, Bindings: bindings}
|
|
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.
|
|
func Stream(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.
|
|
func BlockOnce(fn func(context.Context, string) ([]byte, string, error)) NodeOption {
|
|
return func(d *FsNodeDecl) { d.BlockOnce = fn }
|
|
}
|
|
|
|
// Request sets a request-response handler (rdwr pattern).
|
|
func Request(fn func([]byte) ([]byte, error)) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Request = fn }
|
|
}
|
|
|
|
// RemoveNode sets the removal handler for a node.
|
|
func RemoveNode(fn func() error) NodeOption {
|
|
return func(d *FsNodeDecl) { d.Remove = fn }
|
|
}
|
|
|
|
// 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 }
|
|
}
|