betterNAS/apps/node-agent/internal/nodeagent/filesystem.go
Harivansh Rathi 273af4b0ab Stabilize the node agent runtime loop.
Keep the NAS-side runtime bounded to the configured export path,
make WebDAV and registration behavior env-driven, and add runtime
coverage so the first storage loop can be verified locally.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-01 13:58:24 +00:00

132 lines
2.8 KiB
Go

package nodeagent
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"golang.org/x/net/webdav"
)
type exportFileSystem struct {
root *os.Root
}
var _ webdav.FileSystem = (*exportFileSystem)(nil)
func newExportFileSystem(rootPath string) (*exportFileSystem, error) {
root, err := os.OpenRoot(rootPath)
if err != nil {
return nil, fmt.Errorf("open export root %s: %w", rootPath, err)
}
return &exportFileSystem{
root: root,
}, nil
}
func (f *exportFileSystem) Close() error {
if f.root == nil {
return nil
}
err := f.root.Close()
f.root = nil
return err
}
func (f *exportFileSystem) Mkdir(_ context.Context, name string, perm os.FileMode) error {
resolvedName, err := resolveExportName(name)
if err != nil {
return pathError("mkdir", name, err)
}
if resolvedName == "." {
return pathError("mkdir", name, os.ErrInvalid)
}
return f.root.Mkdir(resolvedName, perm)
}
func (f *exportFileSystem) OpenFile(_ context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
resolvedName, err := resolveExportName(name)
if err != nil {
return nil, pathError("open", name, err)
}
file, err := f.root.OpenFile(resolvedName, flag, perm)
if err != nil {
return nil, err
}
return file, nil
}
func (f *exportFileSystem) RemoveAll(_ context.Context, name string) error {
resolvedName, err := resolveExportName(name)
if err != nil {
return pathError("removeall", name, err)
}
if resolvedName == "." {
return pathError("removeall", name, os.ErrInvalid)
}
return f.root.RemoveAll(resolvedName)
}
func (f *exportFileSystem) Rename(_ context.Context, oldName, newName string) error {
resolvedOldName, err := resolveExportName(oldName)
if err != nil {
return pathError("rename", oldName, err)
}
resolvedNewName, err := resolveExportName(newName)
if err != nil {
return pathError("rename", newName, err)
}
if resolvedOldName == "." || resolvedNewName == "." {
return pathError("rename", oldName, os.ErrInvalid)
}
return f.root.Rename(resolvedOldName, resolvedNewName)
}
func (f *exportFileSystem) Stat(_ context.Context, name string) (os.FileInfo, error) {
resolvedName, err := resolveExportName(name)
if err != nil {
return nil, pathError("stat", name, err)
}
return f.root.Stat(resolvedName)
}
func resolveExportName(name string) (string, error) {
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
return "", os.ErrNotExist
}
if strings.Contains(name, "\x00") {
return "", os.ErrNotExist
}
cleanedName := path.Clean("/" + name)
cleanedName = strings.TrimPrefix(cleanedName, "/")
if cleanedName == "" {
return ".", nil
}
return filepath.FromSlash(cleanedName), nil
}
func pathError(op, path string, err error) error {
return &os.PathError{
Op: op,
Path: path,
Err: err,
}
}