160 lines
4.1 KiB
Go
160 lines
4.1 KiB
Go
package virtfs
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Tree is a synthetic filesystem node.
|
|
//
|
|
// Two usage modes:
|
|
//
|
|
// 1. EDSL (BuildTree): Returns a single root node. Children are resolved lazily
|
|
// via closures over the FsNodeDecl spec. The children map is NOT populated;
|
|
// all path resolution happens through the handler closures (openFn, statFn, etc).
|
|
//
|
|
// 2. Manual (Mount): Children are attached explicitly via Mount(). The split()
|
|
// method routes path lookups through the children map before falling through
|
|
// to handler closures.
|
|
//
|
|
// In production, only mode 1 is used. Mode 2 exists for composability and testing.
|
|
type Tree struct {
|
|
Tree []string // backing dirs (nil in EDSL trees)
|
|
perm os.FileMode
|
|
listFn func([]string) ([]os.DirEntry, error)
|
|
createFn func([]string, string, os.FileMode) error
|
|
deleteFn func([]string, string) error
|
|
renameFn func([]string, string, string) error
|
|
openFn func([]string, string) (File, error)
|
|
statFn func([]string, string) (os.FileInfo, error)
|
|
readdirFn func([]string, string) ([]os.DirEntry, error)
|
|
children map[string]*Tree
|
|
}
|
|
|
|
// split checks if name routes to a child tree.
|
|
func (d *Tree) split(name string) (*Tree, string, bool) {
|
|
if d.children == nil {
|
|
return nil, "", false
|
|
}
|
|
if child, ok := d.children[name]; ok {
|
|
return child, "", true
|
|
}
|
|
if i := strings.IndexByte(name, '/'); i >= 0 {
|
|
prefix := name[:i]
|
|
if child, ok := d.children[prefix]; ok {
|
|
return child, name[i+1:], true
|
|
}
|
|
}
|
|
return nil, "", false
|
|
}
|
|
|
|
func (d *Tree) Stat(name string) (os.FileInfo, error) {
|
|
if child, rest, ok := d.split(name); ok {
|
|
if rest == "" {
|
|
return &SyntheticFileInfo{Name_: name, Mode_: PermChildDir, IsDir_: true}, nil
|
|
}
|
|
return child.Stat(rest)
|
|
}
|
|
if d.statFn != nil {
|
|
return d.statFn(d.Tree, name)
|
|
}
|
|
if d.openFn != nil {
|
|
e, err := d.openFn(d.Tree, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer e.Close()
|
|
return e.Stat()
|
|
}
|
|
return nil, fmt.Errorf("%s: not found", name)
|
|
}
|
|
|
|
func (d *Tree) List() ([]os.DirEntry, error) {
|
|
if d.listFn != nil {
|
|
entries, err := d.listFn(d.Tree)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for name := range d.children {
|
|
entries = append(entries, dirEntry(name, PermChildDir))
|
|
}
|
|
return entries, nil
|
|
}
|
|
// No list function and no backing dirs — return children only.
|
|
var result []os.DirEntry
|
|
for name := range d.children {
|
|
result = append(result, dirEntry(name, PermChildDir))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (d *Tree) Open(name string) (File, error) {
|
|
if child, rest, ok := d.split(name); ok {
|
|
if rest == "" {
|
|
return nil, fmt.Errorf("%s: is a directory", name)
|
|
}
|
|
return child.Open(rest)
|
|
}
|
|
if d.openFn != nil {
|
|
return d.openFn(d.Tree, name)
|
|
}
|
|
return nil, fmt.Errorf("%s: not found", name)
|
|
}
|
|
|
|
func (d *Tree) Create(name string) error {
|
|
if child, rest, ok := d.split(name); ok && rest != "" {
|
|
return child.Create(rest)
|
|
}
|
|
if d.createFn != nil {
|
|
return d.createFn(d.Tree, name, d.perm)
|
|
}
|
|
return fmt.Errorf("create not supported")
|
|
}
|
|
|
|
func (d *Tree) Delete(name string) error {
|
|
if child, rest, ok := d.split(name); ok && rest != "" {
|
|
return child.Delete(rest)
|
|
}
|
|
if d.deleteFn != nil {
|
|
return d.deleteFn(d.Tree, name)
|
|
}
|
|
return fmt.Errorf("delete not supported")
|
|
}
|
|
|
|
func (d *Tree) Rename(old, new string) error {
|
|
if child, rest, ok := d.split(old); ok && rest != "" {
|
|
return child.Rename(rest, new)
|
|
}
|
|
if d.renameFn != nil {
|
|
return d.renameFn(d.Tree, old, new)
|
|
}
|
|
return fmt.Errorf("rename not supported")
|
|
}
|
|
|
|
// Readdir lists entries in a subdirectory.
|
|
func (d *Tree) Readdir(rel string) ([]os.DirEntry, error) {
|
|
if child, rest, ok := d.split(rel); ok {
|
|
if rest == "" {
|
|
return child.List()
|
|
}
|
|
return child.Readdir(rest)
|
|
}
|
|
if d.readdirFn != nil {
|
|
return d.readdirFn(d.Tree, rel)
|
|
}
|
|
return nil, fmt.Errorf("%s: not found", rel)
|
|
}
|
|
|
|
// MkdirAll creates a directory relative to the first backing dir.
|
|
func (d *Tree) MkdirAll(rel string) error {
|
|
if child, rest, ok := d.split(rel); ok && rest != "" {
|
|
return child.MkdirAll(rest)
|
|
}
|
|
if len(d.Tree) == 0 {
|
|
return fmt.Errorf("no backing directory")
|
|
}
|
|
return os.MkdirAll(filepath.Join(d.Tree[0], rel), PermMkdir)
|
|
}
|