refactor: route WebDAV through storage backend

This commit is contained in:
OpenClaw Agent
2026-09-21 17:27:54 +08:00
parent 09b808042d
commit a7192fd96e
3 changed files with 230 additions and 6 deletions
+191 -6
View File
@@ -606,18 +606,203 @@ func (s *Server) webdav(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/" + vp r.URL.Path = "/" + vp
defer func() { r.URL.Path = old }() defer func() { r.URL.Path = old }()
inner := s.WebDAVFS inner := s.WebDAVFS
if inner == nil && s.MultiRoot {
if m, ok := s.Store.(storage.Multi); ok {
inner = newWebDAVMultiFS(m)
}
}
if inner == nil { if inner == nil {
inner = webdav.LocalFileSystem(s.WebDAVRoot) inner = &webdavStorageFS{store: s.Store, removeMode: s.RemoveMode}
} }
fs := &webdavAuthFS{inner: inner, account: account(r, s)} fs := &webdavAuthFS{inner: inner, account: account(r, s)}
(&webdav.Handler{FileSystem: fs}).ServeHTTP(w, r) (&webdav.Handler{FileSystem: fs}).ServeHTTP(w, r)
} }
type webdavStorageFS struct {
store storage.Storage
removeMode int
}
func davStoragePath(name string) string {
p := strings.Trim(strings.ReplaceAll(name, "\\", "/"), "/")
p = strings.TrimPrefix(p, "webdav/")
p = strings.TrimPrefix(p, "webdav")
return strings.Trim(p, "/")
}
func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
p := davStoragePath(name)
fi, err := f.store.Stat(p)
if err != nil {
return nil, err
}
return &webdav.FileInfo{Path: "/" + p, Size: fi.Size(), ModTime: fi.ModTime(), IsDir: fi.IsDir()}, nil
}
func (f *webdavStorageFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
p := davStoragePath(name)
fi, err := f.store.Stat(p)
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("is a directory")
}
return f.store.Open(p)
}
func (f *webdavStorageFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
return f.info(name)
}
func (f *webdavStorageFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
root := davStoragePath(name)
var out []webdav.FileInfo
var walk func(string) error
walk = func(dir string) error {
items, err := f.store.List(dir)
if err != nil {
return err
}
for _, item := range items {
p := path.Join(dir, item.Name())
out = append(out, webdav.FileInfo{Path: "/" + p, Size: item.Size(), ModTime: item.ModTime(), IsDir: item.IsDir()})
if recursive && item.IsDir() {
if err := walk(p); err != nil {
return err
}
}
}
return nil
}
if err := walk(root); err != nil {
return nil, err
}
return out, nil
}
func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
p := davStoragePath(name)
_, statErr := f.store.Stat(p)
if statErr != nil && !os.IsNotExist(statErr) {
return nil, false, statErr
}
created := os.IsNotExist(statErr)
file, err := f.store.Create(p)
if err != nil {
return nil, false, err
}
_, copyErr := io.Copy(file, body)
closeErr := file.Close()
body.Close()
if copyErr != nil {
return nil, false, copyErr
}
if closeErr != nil {
return nil, false, closeErr
}
fi, err := f.info(p)
return fi, created, err
}
func (f *webdavStorageFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
p := davStoragePath(name)
if p == "" {
return fmt.Errorf("cannot remove storage root")
}
if f.removeMode == 1 {
if t, ok := f.store.(storage.Trashable); ok {
return t.Trash(p)
}
}
if f.removeMode == 2 {
if t, ok := f.store.(storage.Trashable); ok {
home, err := os.UserHomeDir()
if err != nil {
return err
}
return t.MoveToTrash(p, filepath.Join(home, ".chfs_trashbin"))
}
}
return f.store.Remove(p)
}
func (f *webdavStorageFS) Mkdir(ctx context.Context, name string) error {
p := davStoragePath(name)
if p == "" {
return fmt.Errorf("cannot create storage root")
}
return f.store.MkdirAll(p)
}
func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
src := davStoragePath(name)
dst := davStoragePath(dest)
if m, ok := f.store.(storage.Multi); ok {
srcRoot, _, srcOK := m.FindRoot(src)
dstRoot, _, dstOK := m.FindRoot(dst)
if srcOK && dstOK && srcRoot != dstRoot {
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root copy is not supported"))
}
}
info, err := f.store.Stat(src)
if err != nil {
return false, err
}
if info.IsDir() {
if err := f.store.MkdirAll(dst); err != nil {
return false, err
}
items, err := f.store.List(src)
if err != nil {
return false, err
}
for _, item := range items {
created, err := f.Copy(ctx, path.Join(src, item.Name()), path.Join(dst, item.Name()), opts)
if err != nil {
return false, err
}
_ = created
}
return true, nil
}
in, err := f.store.Open(src)
if err != nil {
return false, err
}
out, err := f.store.Create(dst)
if err != nil {
in.Close()
return false, err
}
_, copyErr := io.Copy(out, in)
in.Close()
closeErr := out.Close()
if copyErr != nil {
return false, copyErr
}
if closeErr != nil {
return false, closeErr
}
return true, nil
}
func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
src := davStoragePath(name)
dst := davStoragePath(dest)
if opts != nil && opts.NoOverwrite {
if _, err := f.store.Stat(dst); err == nil {
return false, fmt.Errorf("destination exists")
}
}
if m, ok := f.store.(storage.Multi); ok {
srcRoot, _, srcOK := m.FindRoot(src)
dstRoot, _, dstOK := m.FindRoot(dst)
if srcOK && dstOK && srcRoot != dstRoot {
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root move is not supported"))
}
}
if err := f.store.Rename(src, dst); err != nil {
return false, webdav.NewHTTPError(http.StatusForbidden, err)
}
return true, nil
}
type webdavMultiFS struct { type webdavMultiFS struct {
roots map[string]webdav.FileSystem roots map[string]webdav.FileSystem
names []string names []string
+20
View File
@@ -7,6 +7,7 @@ import (
"testing" "testing"
"chfs-ng/internal/auth" "chfs-ng/internal/auth"
"chfs-ng/internal/storage"
"github.com/emersion/go-webdav" "github.com/emersion/go-webdav"
) )
@@ -35,3 +36,22 @@ func TestWebDAVAuthFSCheckTree(t *testing.T) {
t.Fatalf("public file should be deletable: %v", err) t.Fatalf("public file should be deletable: %v", err)
} }
} }
func TestWebDAVStorageFSCopyAndMove(t *testing.T) {
root1 := t.TempDir()
root2 := t.TempDir()
if err := os.WriteFile(filepath.Join(root1, "a.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
m := storage.NewMulti([]string{root1, root2})
fs := &webdavStorageFS{store: m}
if _, err := fs.Copy(context.Background(), "/"+m.Names[0]+"/a.txt", "/"+m.Names[0]+"/b.txt", &webdav.CopyOptions{}); err != nil {
t.Fatalf("copy: %v", err)
}
if _, err := os.Stat(filepath.Join(root1, "b.txt")); err != nil {
t.Fatal(err)
}
if _, err := fs.Move(context.Background(), "/"+m.Names[0]+"/b.txt", "/"+m.Names[1]+"/b.txt", &webdav.MoveOptions{}); err == nil {
t.Fatal("expected cross-root move rejection")
}
}
+19
View File
@@ -41,6 +41,25 @@ func NewMulti(paths []string) Multi {
func (m Multi) isVirtualRoot(p string) bool { return strings.Trim(p, "/") == "" } func (m Multi) isVirtualRoot(p string) bool { return strings.Trim(p, "/") == "" }
func (m Multi) FindRoot(p string) (string, string, bool) {
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
if p == "" {
return "", "", false
}
parts := strings.SplitN(p, "/", 2)
for _, n := range m.Names {
if parts[0] != n {
continue
}
rel := ""
if len(parts) == 2 {
rel = parts[1]
}
return n, rel, true
}
return "", "", false
}
func (m Multi) find(p string) (Local, string, bool) { func (m Multi) find(p string) (Local, string, bool) {
p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/") p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/")
if p == "" { if p == "" {