ollie/fs/builder.go

430 lines
11 KiB
Go

package fs
import (
"context"
"fmt"
"os"
"strings"
)
// BuildTree walks an FsNodeDecl spec and produces a *Tree.
// It validates the spec, resolves inheritance, and wires handlers.
func BuildTree(spec FsNodeDecl, rootCtx HandlerCtx) *Tree {
validate(&spec, "")
tree := build(&spec, "", "", rootCtx)
return tree
}
// ── validation ───────────────────────────────────────────────────
func validate(d *FsNodeDecl, path string) {
hasChildren := len(d.Children) > 0
hasList := d.List != nil
isDir := hasChildren || hasList
hasLeaf := d.Read != nil || d.Write != nil || d.BlockOnce != nil || d.Stream != nil || d.Request != nil
hasCreate := d.Create != nil
hasRemove := d.Remove != nil
name := d.Name
fullPath := path + "/" + name
if path == "" {
fullPath = name
}
// A node with children or List is a directory.
// It must NOT have leaf handlers (Read/Write/BlockOnce/Stream/Request).
if isDir && hasLeaf {
panic(fmt.Sprintf("BuildTree: %q: directory node cannot have leaf handlers", fullPath))
}
// At most one blocking variant.
var blocking int
if d.BlockOnce != nil {
blocking++
}
if d.Stream != nil {
blocking++
}
if d.Request != nil {
blocking++
}
if blocking > 1 {
panic(fmt.Sprintf("BuildTree: %q: at most one of BlockOnce, Stream, Request may be set", fullPath))
}
// Create and Remove only valid on directory nodes.
if hasCreate && !isDir {
panic(fmt.Sprintf("BuildTree: %q: Create on non-directory node", fullPath))
}
if hasRemove && !isDir {
panic(fmt.Sprintf("BuildTree: %q: Remove on non-directory node", fullPath))
}
// Template nodes must have List set.
if strings.HasPrefix(name, "{") && strings.HasSuffix(name, "}") && !hasList {
panic(fmt.Sprintf("BuildTree: %q: template node %q requires List", fullPath, name))
}
// Validate children recursively.
for i := range d.Children {
validate(&d.Children[i], fullPath)
}
}
// build ────────────────────────────────────────────────────────
func build(d *FsNodeDecl, parentUid, parentGid string, parentCtx HandlerCtx) *Tree {
// Resolve inheritance.
uid := d.UID
if uid == "" {
uid = parentUid
}
gid := d.GID
if gid == "" {
gid = parentGid
}
mode := d.Mode
hasChildren := len(d.Children) > 0
hasList := d.List != nil
isDir := hasChildren || hasList
if isDir {
// Directory node.
if mode == 0 {
mode = 0755
}
t := NewTree(nil, mode,
WithLister(func(_ []string) ([]os.DirEntry, error) {
return listDir(d, uid, gid, parentCtx)
}),
WithReaddir(func(_ []string, name string) ([]os.DirEntry, error) {
return readdirDir(d, uid, gid, name, parentCtx)
}),
WithStat(func(_ []string, name string) (os.FileInfo, error) {
return statDir(d, uid, gid, name, parentCtx)
}),
WithOpener(func(_ []string, name string) (File, error) {
return openDir(d, uid, gid, name, parentCtx)
}),
WithDeleter(func(_ []string, name string) error {
return deleteDir(d, name, parentCtx)
}),
WithCreator(func(_ []string, name string, _ os.FileMode) error {
if d.Create != nil {
return d.Create(parentCtx, name)
}
return fmt.Errorf("create not supported")
}),
WithRenamer(func(_ []string, old, new string) error {
return renameDir(d, old, new, parentCtx)
}),
)
return t
}
// Leaf node.
if mode == 0 {
mode = 0444
}
return NewTree(nil, 0,
WithLister(func(_ []string) ([]os.DirEntry, error) {
return nil, fmt.Errorf("%s: is a file", d.Name)
}),
WithOpener(func(_ []string, name string) (File, error) {
return openLeaf(d, uid, gid, mode, parentCtx, name)
}),
)
}
// ── directory ops ────────────────────────────────────────────────
func listDir(d *FsNodeDecl, uid, gid string, ctx HandlerCtx) ([]os.DirEntry, error) {
// Template directories: children come from List.
if d.List != nil {
children, err := d.List(ctx)
if err != nil {
return nil, err
}
entries := make([]os.DirEntry, len(children))
for i, c := range children {
m := c.Mode
if m == 0 {
m = 0755
}
isDir := len(c.Children) > 0 || c.List != nil
if isDir {
entries[i] = DirEntry(c.Name, m)
} else {
entries[i] = FileEntry(c.Name, m)
}
}
return entries, nil
}
// Static directories: return static children and also resolve
// template children' List functions for dynamic entries.
var entries []os.DirEntry
for _, c := range d.Children {
if isTemplateName(c.Name) {
if c.List != nil {
dyn, err := c.List(ctx)
if err != nil {
continue
}
for _, dc := range dyn {
m := dc.Mode
if m == 0 {
m = 0755
}
isChildDir := len(dc.Children) > 0 || dc.List != nil
if isChildDir {
entries = append(entries, DirEntry(dc.Name, m))
} else {
entries = append(entries, FileEntry(dc.Name, m))
}
}
}
continue
}
m := c.Mode
if m == 0 {
m = 0755
}
isDir := len(c.Children) > 0 || c.List != nil
if isDir {
entries = append(entries, DirEntry(c.Name, m))
} else {
entries = append(entries, FileEntry(c.Name, m))
}
}
return entries, nil
}
func readdirDir(d *FsNodeDecl, uid, gid, name string, ctx HandlerCtx) ([]os.DirEntry, error) {
child := findChild(d, name, ctx)
if child == nil {
return nil, fmt.Errorf("%s: not found", name)
}
return listDir(child, resolveUID(child, uid), resolveGID(child, gid), ctx)
}
func statDir(d *FsNodeDecl, uid, gid, name string, ctx HandlerCtx) (os.FileInfo, error) {
// Stat of the dir itself.
if name == "" || name == "." {
m := d.Mode
if m == 0 {
m = 0755
}
return &SyntheticFileInfo{Name_: d.Name, Mode_: m | os.ModeDir, IsDir_: true, UID_: uid, GID_: gid}, nil
}
child := findChild(d, name, ctx)
if child == nil {
return nil, fmt.Errorf("%s: not found", name)
}
if child.Stat != nil {
return child.Stat(), nil
}
cm := child.Mode
if cm == 0 {
cm = 0755
}
cu := resolveUID(child, uid)
cg := resolveGID(child, gid)
isDir := len(child.Children) > 0 || child.List != nil
if isDir {
return &SyntheticFileInfo{Name_: name, Mode_: cm | os.ModeDir, IsDir_: true, UID_: cu, GID_: cg}, nil
}
return &SyntheticFileInfo{Name_: name, Mode_: cm, UID_: cu, GID_: cg}, nil
}
func openDir(d *FsNodeDecl, uid, gid string, name string, ctx HandlerCtx) (File, error) {
child := findChild(d, name, ctx)
if child == nil {
return nil, fmt.Errorf("%s: not found", name)
}
sub := build(child, resolveUID(child, uid), resolveGID(child, gid), ctx)
return sub.Open("")
}
func deleteDir(d *FsNodeDecl, name string, ctx HandlerCtx) error {
child := findChild(d, name, ctx)
if child == nil {
return fmt.Errorf("%s: not found", name)
}
if child.Remove != nil {
return child.Remove(ctx)
}
return fmt.Errorf("%s: not removable", name)
}
func renameDir(d *FsNodeDecl, old, new string, ctx HandlerCtx) error {
child := findChild(d, old, ctx)
if child == nil {
return fmt.Errorf("%s: not found", old)
}
if child.Rename != nil {
return child.Rename(new)
}
return fmt.Errorf("rename not supported")
}
// ── leaf ops ─────────────────────────────────────────────────────
func openLeaf(d *FsNodeDecl, uid, gid string, mode os.FileMode, ctx HandlerCtx, name string) (File, error) {
notBlocking := func(context.Context, string) ([]byte, string, error) {
return nil, "", fmt.Errorf("blocking read not supported")
}
// Read handler.
readFn := func() ([]byte, error) {
if d.Read != nil {
return d.Read(ctx)
}
return []byte{}, nil
}
// Write handler.
writeFn := func(data []byte) error {
if d.Write != nil {
return d.Write(ctx, data)
}
return fmt.Errorf("%s: read-only", d.Name)
}
// Blocking/Stream/Request handler.
var blockFn func(context.Context, string) ([]byte, string, error)
var isBlocking, isStreaming, isRequest bool
switch {
case d.BlockOnce != nil:
fn := d.BlockOnce
blockFn = func(c context.Context, b string) ([]byte, string, error) { return fn(ctx, c, b) }
isBlocking = true
case d.Stream != nil:
fn := d.Stream
blockFn = func(c context.Context, b string) ([]byte, string, error) { return fn(ctx, c, b) }
isStreaming = true
case d.Request != nil:
fn := d.Request
// Request: Write stores result, Read returns it.
var result []byte
blockFn = notBlocking
readFn = func() ([]byte, error) {
if result != nil {
r := result
result = nil
return r, nil
}
if d.Read != nil {
return d.Read(ctx)
}
return nil, nil
}
writeFn = func(data []byte) error {
r, err := fn(ctx, data)
if err != nil {
return err
}
result = r
return nil
}
isRequest = true
default:
blockFn = notBlocking
}
return &FileConfig{
StatFn: func() (os.FileInfo, error) {
if d.Stat != nil {
return d.Stat(), nil
}
data, _ := readFn()
return &SyntheticFileInfo{Name_: name, Mode_: mode, Size_: int64(len(data)), UID_: uid, GID_: gid}, nil
},
ReadFn: readFn,
WriteFn: writeFn,
BlockingReadFn: blockFn,
BlockingRead_: isBlocking,
StreamMode_: isStreaming,
RequestResp_: isRequest,
}, nil
}
// ── helpers ──────────────────────────────────────────────────────
func findChild(d *FsNodeDecl, name string, ctx HandlerCtx) *FsNodeDecl {
// Handle nested paths recursively.
if i := strings.IndexByte(name, '/'); i >= 0 {
prefix := name[:i]
rest := name[i+1:]
child := findChild(d, prefix, ctx)
if child == nil {
return nil
}
return findChild(child, rest, ctx)
}
// Static children.
for i := range d.Children {
if d.Children[i].Name == name {
return &d.Children[i]
}
}
// Dynamic children from this node's own List.
if d.List != nil {
children, err := d.List(ctx)
if err != nil {
return nil
}
for i := range children {
if children[i].Name == name || matchAlias(children[i].Aliases, name) {
return &children[i]
}
}
}
// Dynamic children from template children (e.g. session/{id}).
for i := range d.Children {
if isTemplateName(d.Children[i].Name) && d.Children[i].List != nil {
children, err := d.Children[i].List(ctx)
if err != nil {
continue
}
for j := range children {
if children[j].Name == name || matchAlias(children[j].Aliases, name) {
return &children[j]
}
}
}
}
return nil
}
func matchAlias(aliases []string, name string) bool {
for _, a := range aliases {
if a == name {
return true
}
}
return false
}
func resolveUID(d *FsNodeDecl, parent string) string {
if d.UID != "" {
return d.UID
}
return parent
}
func resolveGID(d *FsNodeDecl, parent string) string {
if d.GID != "" {
return d.GID
}
return parent
}
func isTemplateName(name string) bool {
return len(name) > 2 && name[0] == '{' && name[len(name)-1] == '}'
}