2026-03-30 21:48:42 +00:00
|
|
|
// Example: nodeTree := node.New()
|
|
|
|
|
// Example: nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
|
|
|
|
// Example: snapshot, _ := nodeTree.ToTar()
|
|
|
|
|
// Example: restored, _ := node.FromTar(snapshot)
|
2026-03-06 09:31:28 +00:00
|
|
|
package node
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"archive/tar"
|
|
|
|
|
"bytes"
|
|
|
|
|
"cmp"
|
|
|
|
|
goio "io"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"path"
|
|
|
|
|
"slices"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-03-26 10:54:10 +00:00
|
|
|
core "dappco.re/go/core"
|
2026-03-21 23:44:10 +00:00
|
|
|
coreio "dappco.re/go/core/io"
|
2026-03-06 09:31:28 +00:00
|
|
|
)
|
|
|
|
|
|
2026-03-30 21:23:35 +00:00
|
|
|
// Example: nodeTree := node.New()
|
2026-03-30 21:48:42 +00:00
|
|
|
// Example: nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
|
|
|
|
// Example: snapshot, _ := nodeTree.ToTar()
|
|
|
|
|
// Example: restored, _ := node.FromTar(snapshot)
|
2026-03-06 09:31:28 +00:00
|
|
|
type Node struct {
|
|
|
|
|
files map[string]*dataFile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ coreio.Medium = (*Node)(nil)
|
|
|
|
|
var _ fs.ReadFileFS = (*Node)(nil)
|
|
|
|
|
|
2026-03-30 22:39:50 +00:00
|
|
|
// Example: nodeTree := node.New()
|
|
|
|
|
// Example: _ = nodeTree.Write("config/app.yaml", "port: 8080")
|
2026-03-06 09:31:28 +00:00
|
|
|
func New() *Node {
|
|
|
|
|
return &Node{files: make(map[string]*dataFile)}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
// Example: nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) AddData(name string, content []byte) {
|
2026-03-26 16:23:45 +00:00
|
|
|
name = core.TrimPrefix(name, "/")
|
2026-03-06 09:31:28 +00:00
|
|
|
if name == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
if core.HasSuffix(name, "/") {
|
2026-03-06 09:31:28 +00:00
|
|
|
return
|
|
|
|
|
}
|
2026-03-30 21:39:03 +00:00
|
|
|
node.files[name] = &dataFile{
|
2026-03-06 09:31:28 +00:00
|
|
|
name: name,
|
|
|
|
|
content: content,
|
|
|
|
|
modTime: time.Now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
// Example: snapshot, _ := nodeTree.ToTar()
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) ToTar() ([]byte, error) {
|
2026-03-31 13:25:00 +00:00
|
|
|
buffer := new(bytes.Buffer)
|
|
|
|
|
tarWriter := tar.NewWriter(buffer)
|
2026-03-06 09:31:28 +00:00
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
for _, file := range node.files {
|
2026-03-06 09:31:28 +00:00
|
|
|
hdr := &tar.Header{
|
|
|
|
|
Name: file.name,
|
|
|
|
|
Mode: 0600,
|
|
|
|
|
Size: int64(len(file.content)),
|
|
|
|
|
ModTime: file.modTime,
|
|
|
|
|
}
|
2026-03-31 13:25:00 +00:00
|
|
|
if err := tarWriter.WriteHeader(hdr); err != nil {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-03-31 13:25:00 +00:00
|
|
|
if _, err := tarWriter.Write(file.content); err != nil {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 13:25:00 +00:00
|
|
|
if err := tarWriter.Close(); err != nil {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 13:25:00 +00:00
|
|
|
return buffer.Bytes(), nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:29:35 +00:00
|
|
|
// Example: restored, _ := node.FromTar(snapshot)
|
2026-03-06 09:31:28 +00:00
|
|
|
func FromTar(data []byte) (*Node, error) {
|
2026-03-30 21:52:52 +00:00
|
|
|
restoredNode := New()
|
|
|
|
|
if err := restoredNode.LoadTar(data); err != nil {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-03-30 21:52:52 +00:00
|
|
|
return restoredNode, nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
// Example: _ = nodeTree.LoadTar(snapshot)
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) LoadTar(data []byte) error {
|
2026-03-06 09:31:28 +00:00
|
|
|
newFiles := make(map[string]*dataFile)
|
2026-03-31 13:25:00 +00:00
|
|
|
tarReader := tar.NewReader(bytes.NewReader(data))
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
for {
|
2026-03-31 13:25:00 +00:00
|
|
|
header, err := tarReader.Next()
|
2026-03-06 09:31:28 +00:00
|
|
|
if err == goio.EOF {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if header.Typeflag == tar.TypeReg {
|
2026-03-31 13:25:00 +00:00
|
|
|
content, err := goio.ReadAll(tarReader)
|
2026-03-06 09:31:28 +00:00
|
|
|
if err != nil {
|
2026-03-26 16:23:45 +00:00
|
|
|
return core.E("node.LoadTar", "read tar entry", err)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
name := core.TrimPrefix(header.Name, "/")
|
|
|
|
|
if name == "" || core.HasSuffix(name, "/") {
|
2026-03-06 09:31:28 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
newFiles[name] = &dataFile{
|
|
|
|
|
name: name,
|
|
|
|
|
content: content,
|
|
|
|
|
modTime: header.ModTime,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
node.files = newFiles
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:23:35 +00:00
|
|
|
// Example: options := node.WalkOptions{MaxDepth: 1, SkipErrors: true}
|
2026-03-06 09:31:28 +00:00
|
|
|
type WalkOptions struct {
|
2026-03-30 23:02:53 +00:00
|
|
|
MaxDepth int
|
|
|
|
|
Filter func(entryPath string, entry fs.DirEntry) bool
|
2026-03-06 09:31:28 +00:00
|
|
|
SkipErrors bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 05:18:17 +00:00
|
|
|
// Example: _ = nodeTree.Walk(".", func(_ string, _ fs.DirEntry, _ error) error { return nil }, node.WalkOptions{MaxDepth: 1, SkipErrors: true})
|
2026-03-31 05:24:39 +00:00
|
|
|
func (node *Node) Walk(root string, fn fs.WalkDirFunc, options WalkOptions) error {
|
|
|
|
|
if options.SkipErrors {
|
2026-03-30 21:39:03 +00:00
|
|
|
if _, err := node.Stat(root); err != nil {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
return fs.WalkDir(node, root, func(entryPath string, entry fs.DirEntry, err error) error {
|
2026-03-31 05:24:39 +00:00
|
|
|
if options.Filter != nil && err == nil {
|
|
|
|
|
if !options.Filter(entryPath, entry) {
|
2026-03-30 20:18:30 +00:00
|
|
|
if entry != nil && entry.IsDir() {
|
2026-03-06 09:31:28 +00:00
|
|
|
return fs.SkipDir
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 13:25:00 +00:00
|
|
|
walkResult := fn(entryPath, entry, err)
|
2026-03-06 09:31:28 +00:00
|
|
|
|
2026-03-31 13:25:00 +00:00
|
|
|
if walkResult == nil && options.MaxDepth > 0 && entry != nil && entry.IsDir() && entryPath != root {
|
|
|
|
|
relativePath := core.TrimPrefix(entryPath, root)
|
|
|
|
|
relativePath = core.TrimPrefix(relativePath, "/")
|
|
|
|
|
depth := len(core.Split(relativePath, "/"))
|
2026-03-31 05:24:39 +00:00
|
|
|
if depth >= options.MaxDepth {
|
2026-03-06 09:31:28 +00:00
|
|
|
return fs.SkipDir
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 13:25:00 +00:00
|
|
|
return walkResult
|
2026-03-06 09:31:28 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 22:39:50 +00:00
|
|
|
// Example: content, _ := nodeTree.ReadFile("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) ReadFile(name string) ([]byte, error) {
|
2026-03-26 16:23:45 +00:00
|
|
|
name = core.TrimPrefix(name, "/")
|
2026-03-30 21:48:42 +00:00
|
|
|
file, ok := node.files[name]
|
2026-03-06 09:31:28 +00:00
|
|
|
if !ok {
|
2026-03-26 16:23:45 +00:00
|
|
|
return nil, core.E("node.ReadFile", core.Concat("path not found: ", name), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
result := make([]byte, len(file.content))
|
|
|
|
|
copy(result, file.content)
|
2026-03-06 09:31:28 +00:00
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
// Example: _ = nodeTree.CopyFile("config/app.yaml", "/tmp/app.yaml", 0644)
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) CopyFile(sourcePath, destinationPath string, perm fs.FileMode) error {
|
2026-03-30 20:18:30 +00:00
|
|
|
sourcePath = core.TrimPrefix(sourcePath, "/")
|
2026-03-30 21:48:42 +00:00
|
|
|
file, ok := node.files[sourcePath]
|
2026-03-06 09:31:28 +00:00
|
|
|
if !ok {
|
2026-03-30 21:39:03 +00:00
|
|
|
info, err := node.Stat(sourcePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
if err != nil {
|
2026-03-30 20:18:30 +00:00
|
|
|
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
if info.IsDir() {
|
2026-03-30 20:18:30 +00:00
|
|
|
return core.E("node.CopyFile", core.Concat("source is a directory: ", sourcePath), fs.ErrInvalid)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 20:18:30 +00:00
|
|
|
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 20:18:30 +00:00
|
|
|
parent := core.PathDir(destinationPath)
|
|
|
|
|
if parent != "." && parent != "" && parent != destinationPath && !coreio.Local.IsDir(parent) {
|
|
|
|
|
return &fs.PathError{Op: "copyfile", Path: destinationPath, Err: fs.ErrNotExist}
|
2026-03-26 10:54:10 +00:00
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
return coreio.Local.WriteMode(destinationPath, string(file.content), perm)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 22:00:45 +00:00
|
|
|
// Example: _ = nodeTree.CopyTo(io.NewMemoryMedium(), "config", "backup/config")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error {
|
2026-03-26 16:23:45 +00:00
|
|
|
sourcePath = core.TrimPrefix(sourcePath, "/")
|
2026-03-30 21:39:03 +00:00
|
|
|
info, err := node.Stat(sourcePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !info.IsDir() {
|
2026-03-30 21:48:42 +00:00
|
|
|
file, ok := node.files[sourcePath]
|
2026-03-06 09:31:28 +00:00
|
|
|
if !ok {
|
2026-03-26 16:23:45 +00:00
|
|
|
return core.E("node.CopyTo", core.Concat("path not found: ", sourcePath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
return target.Write(destPath, string(file.content))
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prefix := sourcePath
|
2026-03-26 16:23:45 +00:00
|
|
|
if prefix != "" && !core.HasSuffix(prefix, "/") {
|
2026-03-06 09:31:28 +00:00
|
|
|
prefix += "/"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
for filePath, file := range node.files {
|
2026-03-30 20:18:30 +00:00
|
|
|
if !core.HasPrefix(filePath, prefix) && filePath != sourcePath {
|
2026-03-06 09:31:28 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-30 20:18:30 +00:00
|
|
|
rel := core.TrimPrefix(filePath, prefix)
|
2026-03-06 09:31:28 +00:00
|
|
|
dest := destPath
|
|
|
|
|
if rel != "" {
|
2026-03-26 16:23:45 +00:00
|
|
|
dest = core.Concat(destPath, "/", rel)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
if err := target.Write(dest, string(file.content)); err != nil {
|
2026-03-06 09:31:28 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: file, _ := nodeTree.Open("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Open(name string) (fs.File, error) {
|
2026-03-26 16:23:45 +00:00
|
|
|
name = core.TrimPrefix(name, "/")
|
2026-03-30 21:48:42 +00:00
|
|
|
if dataFile, ok := node.files[name]; ok {
|
|
|
|
|
return &dataFileReader{file: dataFile}, nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
prefix := name + "/"
|
|
|
|
|
if name == "." || name == "" {
|
|
|
|
|
prefix = ""
|
|
|
|
|
}
|
2026-03-30 21:39:03 +00:00
|
|
|
for filePath := range node.files {
|
2026-03-30 20:18:30 +00:00
|
|
|
if core.HasPrefix(filePath, prefix) {
|
2026-03-06 09:31:28 +00:00
|
|
|
return &dirFile{path: name, modTime: time.Now()}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
return nil, core.E("node.Open", core.Concat("path not found: ", name), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: info, _ := nodeTree.Stat("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Stat(name string) (fs.FileInfo, error) {
|
2026-03-26 16:23:45 +00:00
|
|
|
name = core.TrimPrefix(name, "/")
|
2026-03-30 21:48:42 +00:00
|
|
|
if dataFile, ok := node.files[name]; ok {
|
|
|
|
|
return dataFile.Stat()
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
prefix := name + "/"
|
|
|
|
|
if name == "." || name == "" {
|
|
|
|
|
prefix = ""
|
|
|
|
|
}
|
2026-03-30 21:39:03 +00:00
|
|
|
for filePath := range node.files {
|
2026-03-30 20:18:30 +00:00
|
|
|
if core.HasPrefix(filePath, prefix) {
|
2026-03-06 09:31:28 +00:00
|
|
|
return &dirInfo{name: path.Base(name), modTime: time.Now()}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
return nil, core.E("node.Stat", core.Concat("path not found: ", name), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: entries, _ := nodeTree.ReadDir("config")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) ReadDir(name string) ([]fs.DirEntry, error) {
|
2026-03-26 16:23:45 +00:00
|
|
|
name = core.TrimPrefix(name, "/")
|
2026-03-06 09:31:28 +00:00
|
|
|
if name == "." {
|
|
|
|
|
name = ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
if info, err := node.Stat(name); err == nil && !info.IsDir() {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries := []fs.DirEntry{}
|
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
|
|
|
|
|
|
prefix := ""
|
|
|
|
|
if name != "" {
|
|
|
|
|
prefix = name + "/"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
for filePath := range node.files {
|
2026-03-30 20:18:30 +00:00
|
|
|
if !core.HasPrefix(filePath, prefix) {
|
2026-03-06 09:31:28 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:18:30 +00:00
|
|
|
relPath := core.TrimPrefix(filePath, prefix)
|
2026-03-26 16:23:45 +00:00
|
|
|
firstComponent := core.SplitN(relPath, "/", 2)[0]
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
if seen[firstComponent] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[firstComponent] = true
|
|
|
|
|
|
2026-03-26 16:23:45 +00:00
|
|
|
if core.Contains(relPath, "/") {
|
2026-03-30 21:52:52 +00:00
|
|
|
directoryInfo := &dirInfo{name: firstComponent, modTime: time.Now()}
|
|
|
|
|
entries = append(entries, fs.FileInfoToDirEntry(directoryInfo))
|
2026-03-06 09:31:28 +00:00
|
|
|
} else {
|
2026-03-30 21:39:03 +00:00
|
|
|
file := node.files[filePath]
|
2026-03-06 09:31:28 +00:00
|
|
|
info, _ := file.Stat()
|
|
|
|
|
entries = append(entries, fs.FileInfoToDirEntry(info))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slices.SortFunc(entries, func(a, b fs.DirEntry) int {
|
|
|
|
|
return cmp.Compare(a.Name(), b.Name())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return entries, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: content, _ := nodeTree.Read("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Read(filePath string) (string, error) {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
2026-03-30 21:48:42 +00:00
|
|
|
file, ok := node.files[filePath]
|
2026-03-06 09:31:28 +00:00
|
|
|
if !ok {
|
2026-03-30 20:18:30 +00:00
|
|
|
return "", core.E("node.Read", core.Concat("path not found: ", filePath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
return string(file.content), nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: _ = nodeTree.Write("config/app.yaml", "port: 8080")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Write(filePath, content string) error {
|
|
|
|
|
node.AddData(filePath, []byte(content))
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: _ = nodeTree.WriteMode("keys/private.key", key, 0600)
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) WriteMode(filePath, content string, mode fs.FileMode) error {
|
|
|
|
|
return node.Write(filePath, content)
|
2026-03-17 17:23:23 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:23:35 +00:00
|
|
|
// Example: _ = nodeTree.EnsureDir("config")
|
2026-03-31 05:10:35 +00:00
|
|
|
func (node *Node) EnsureDir(directoryPath string) error {
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: exists := nodeTree.Exists("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Exists(filePath string) bool {
|
|
|
|
|
_, err := node.Stat(filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
return err == nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: isFile := nodeTree.IsFile("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) IsFile(filePath string) bool {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
2026-03-30 21:39:03 +00:00
|
|
|
_, ok := node.files[filePath]
|
2026-03-06 09:31:28 +00:00
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: isDirectory := nodeTree.IsDir("config")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) IsDir(filePath string) bool {
|
|
|
|
|
info, err := node.Stat(filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return info.IsDir()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: _ = nodeTree.Delete("config/app.yaml")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Delete(filePath string) error {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
2026-03-30 21:39:03 +00:00
|
|
|
if _, ok := node.files[filePath]; ok {
|
|
|
|
|
delete(node.files, filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-03-30 20:18:30 +00:00
|
|
|
return core.E("node.Delete", core.Concat("path not found: ", filePath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: _ = nodeTree.DeleteAll("logs/archive")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) DeleteAll(filePath string) error {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
found := false
|
2026-03-30 21:39:03 +00:00
|
|
|
if _, ok := node.files[filePath]; ok {
|
|
|
|
|
delete(node.files, filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
found = true
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:18:30 +00:00
|
|
|
prefix := filePath + "/"
|
2026-03-30 21:39:03 +00:00
|
|
|
for entryPath := range node.files {
|
2026-03-30 20:18:30 +00:00
|
|
|
if core.HasPrefix(entryPath, prefix) {
|
2026-03-30 21:39:03 +00:00
|
|
|
delete(node.files, entryPath)
|
2026-03-06 09:31:28 +00:00
|
|
|
found = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !found {
|
2026-03-30 20:18:30 +00:00
|
|
|
return core.E("node.DeleteAll", core.Concat("path not found: ", filePath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: _ = nodeTree.Rename("drafts/todo.txt", "archive/todo.txt")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Rename(oldPath, newPath string) error {
|
2026-03-26 16:23:45 +00:00
|
|
|
oldPath = core.TrimPrefix(oldPath, "/")
|
|
|
|
|
newPath = core.TrimPrefix(newPath, "/")
|
2026-03-06 09:31:28 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
file, ok := node.files[oldPath]
|
2026-03-06 09:31:28 +00:00
|
|
|
if !ok {
|
2026-03-26 16:23:45 +00:00
|
|
|
return core.E("node.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
file.name = newPath
|
|
|
|
|
node.files[newPath] = file
|
2026-03-30 21:39:03 +00:00
|
|
|
delete(node.files, oldPath)
|
2026-03-06 09:31:28 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: entries, _ := nodeTree.List("config")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) List(filePath string) ([]fs.DirEntry, error) {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
|
|
|
|
if filePath == "" || filePath == "." {
|
2026-03-30 21:39:03 +00:00
|
|
|
return node.ReadDir(".")
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:39:03 +00:00
|
|
|
return node.ReadDir(filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: writer, _ := nodeTree.Create("logs/app.log")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Create(filePath string) (goio.WriteCloser, error) {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
2026-03-30 21:39:03 +00:00
|
|
|
return &nodeWriter{node: node, path: filePath}, nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:19:56 +01:00
|
|
|
// Example: writer, _ := nodeTree.Append("logs/app.log")
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) Append(filePath string) (goio.WriteCloser, error) {
|
2026-03-30 20:18:30 +00:00
|
|
|
filePath = core.TrimPrefix(filePath, "/")
|
2026-03-06 09:31:28 +00:00
|
|
|
var existing []byte
|
2026-03-30 21:48:42 +00:00
|
|
|
if file, ok := node.files[filePath]; ok {
|
|
|
|
|
existing = make([]byte, len(file.content))
|
|
|
|
|
copy(existing, file.content)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:39:03 +00:00
|
|
|
return &nodeWriter{node: node, path: filePath, buf: existing}, nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) ReadStream(filePath string) (goio.ReadCloser, error) {
|
2026-03-30 21:48:42 +00:00
|
|
|
file, err := node.Open(filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
return goio.NopCloser(file), nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:39:03 +00:00
|
|
|
func (node *Node) WriteStream(filePath string) (goio.WriteCloser, error) {
|
|
|
|
|
return node.Create(filePath)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type nodeWriter struct {
|
|
|
|
|
node *Node
|
|
|
|
|
path string
|
|
|
|
|
buf []byte
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (writer *nodeWriter) Write(data []byte) (int, error) {
|
|
|
|
|
writer.buf = append(writer.buf, data...)
|
|
|
|
|
return len(data), nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (writer *nodeWriter) Close() error {
|
|
|
|
|
writer.node.files[writer.path] = &dataFile{
|
|
|
|
|
name: writer.path,
|
|
|
|
|
content: writer.buf,
|
2026-03-06 09:31:28 +00:00
|
|
|
modTime: time.Now(),
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type dataFile struct {
|
|
|
|
|
name string
|
|
|
|
|
content []byte
|
|
|
|
|
modTime time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (file *dataFile) Stat() (fs.FileInfo, error) { return &dataFileInfo{file: file}, nil }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-31 05:10:35 +00:00
|
|
|
func (file *dataFile) Read(buffer []byte) (int, error) { return 0, goio.EOF }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (file *dataFile) Close() error { return nil }
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
type dataFileInfo struct{ file *dataFile }
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dataFileInfo) Name() string { return path.Base(info.file.name) }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dataFileInfo) Size() int64 { return int64(len(info.file.content)) }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dataFileInfo) Mode() fs.FileMode { return 0444 }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dataFileInfo) ModTime() time.Time { return info.file.modTime }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dataFileInfo) IsDir() bool { return false }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dataFileInfo) Sys() any { return nil }
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
type dataFileReader struct {
|
|
|
|
|
file *dataFile
|
|
|
|
|
reader *bytes.Reader
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (reader *dataFileReader) Stat() (fs.FileInfo, error) { return reader.file.Stat() }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (reader *dataFileReader) Read(buffer []byte) (int, error) {
|
|
|
|
|
if reader.reader == nil {
|
|
|
|
|
reader.reader = bytes.NewReader(reader.file.content)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-30 21:48:42 +00:00
|
|
|
return reader.reader.Read(buffer)
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (reader *dataFileReader) Close() error { return nil }
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
type dirInfo struct {
|
|
|
|
|
name string
|
|
|
|
|
modTime time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dirInfo) Name() string { return info.name }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dirInfo) Size() int64 { return 0 }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dirInfo) Mode() fs.FileMode { return fs.ModeDir | 0555 }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dirInfo) ModTime() time.Time { return info.modTime }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dirInfo) IsDir() bool { return true }
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (info *dirInfo) Sys() any { return nil }
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
type dirFile struct {
|
|
|
|
|
path string
|
|
|
|
|
modTime time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (directory *dirFile) Stat() (fs.FileInfo, error) {
|
|
|
|
|
return &dirInfo{name: path.Base(directory.path), modTime: directory.modTime}, nil
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (directory *dirFile) Read([]byte) (int, error) {
|
|
|
|
|
return 0, core.E("node.dirFile.Read", core.Concat("cannot read directory: ", directory.path), &fs.PathError{Op: "read", Path: directory.path, Err: fs.ErrInvalid})
|
2026-03-06 09:31:28 +00:00
|
|
|
}
|
2026-03-26 16:23:45 +00:00
|
|
|
|
2026-03-30 21:48:42 +00:00
|
|
|
func (directory *dirFile) Close() error { return nil }
|
2026-03-06 09:31:28 +00:00
|
|
|
|
|
|
|
|
var _ fs.FS = (*Node)(nil)
|
|
|
|
|
|
|
|
|
|
var _ fs.StatFS = (*Node)(nil)
|
|
|
|
|
var _ fs.ReadDirFS = (*Node)(nil)
|
|
|
|
|
|
|
|
|
|
var _ goio.ReadCloser = goio.NopCloser(nil)
|
|
|
|
|
|
|
|
|
|
var _ goio.WriteCloser = (*nodeWriter)(nil)
|
|
|
|
|
|
|
|
|
|
var _ fs.File = (*dirFile)(nil)
|
|
|
|
|
|
|
|
|
|
var _ fs.File = (*dataFileReader)(nil)
|