From c0f3b94c30adebdc2252f1112f24f7ee8e17e5b1 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 17:02:34 +0800 Subject: [PATCH] feat: expose multi-root storage through WebDAV --- chfs-ng/internal/api/server.go | 168 ++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 06612c4..2f7c023 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -30,6 +30,7 @@ type Server struct { SessionMu sync.RWMutex SessionTimeout int WebDAVRoot string + WebDAVFS webdav.FileSystem Allow string MultiRoot bool RemoveMode int @@ -603,10 +604,175 @@ func (s *Server) webdav(w http.ResponseWriter, r *http.Request) { old := r.URL.Path r.URL.Path = "/" + vp defer func() { r.URL.Path = old }() - fs := &webdavAuthFS{inner: webdav.LocalFileSystem(s.WebDAVRoot), account: account(r, s)} + inner := s.WebDAVFS + if inner == nil && s.MultiRoot { + if m, ok := s.Store.(storage.Multi); ok { + inner = newWebDAVMultiFS(m) + } + } + if inner == nil { + inner = webdav.LocalFileSystem(s.WebDAVRoot) + } + fs := &webdavAuthFS{inner: inner, account: account(r, s)} (&webdav.Handler{FileSystem: fs}).ServeHTTP(w, r) } +type webdavMultiFS struct { + roots map[string]webdav.FileSystem + names []string +} + +func newWebDAVMultiFS(m storage.Multi) *webdavMultiFS { + fs := &webdavMultiFS{roots: make(map[string]webdav.FileSystem), names: append([]string(nil), m.Names...)} + for i, name := range m.Names { + if i < len(m.Roots) { + fs.roots[name] = webdav.LocalFileSystem(m.Roots[i].Root) + } + } + return fs +} +func splitDAVPath(name string) (string, string) { + p := strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/") + p = strings.TrimPrefix(p, "webdav/") + p = strings.TrimPrefix(p, "webdav") + if p == "" { + return "", "" + } + parts := strings.SplitN(p, "/", 2) + if len(parts) == 1 { + return parts[0], "" + } + return parts[0], parts[1] +} +func (f *webdavMultiFS) root(name string) (webdav.FileSystem, string, error) { + r, rel := splitDAVPath(name) + if r == "" { + return nil, "", nil + } + fs, ok := f.roots[r] + if !ok { + return nil, "", webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("root not found")) + } + return fs, rel, nil +} +func (f *webdavMultiFS) Open(ctx context.Context, name string) (io.ReadCloser, error) { + fs, rel, err := f.root(name) + if err != nil { + return nil, err + } + if fs == nil { + return nil, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("virtual root is not a file")) + } + return fs.Open(ctx, "/"+rel) +} +func (f *webdavMultiFS) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) { + if strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/") == "" { + return &webdav.FileInfo{Path: "/", IsDir: true}, nil + } + fs, rel, err := f.root(name) + if err != nil { + return nil, err + } + if rel == "" { + return &webdav.FileInfo{Path: "/" + strings.Trim(strings.SplitN(strings.Trim(name, "/"), "/", 2)[0], "/"), IsDir: true}, nil + } + fi, err := fs.Stat(ctx, "/"+rel) + if err != nil { + return nil, err + } + fi.Path = "/" + strings.Trim(name, "/") + return fi, nil +} +func (f *webdavMultiFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) { + p := strings.Trim(strings.ReplaceAll(name, "\\\\", "/"), "/") + if p == "" { + out := make([]webdav.FileInfo, 0, len(f.names)) + for _, n := range f.names { + out = append(out, webdav.FileInfo{Path: "/" + n, IsDir: true}) + } + return out, nil + } + fs, rel, err := f.root(p) + if err != nil { + return nil, err + } + items, err := fs.ReadDir(ctx, "/"+rel, recursive) + if err != nil { + return nil, err + } + root, _ := splitDAVPath(p) + for i := range items { + itemPath := strings.TrimPrefix(items[i].Path, "/") + if itemPath == "" { + itemPath = filepath.Base(items[i].Path) + } + items[i].Path = "/" + path.Join(root, itemPath) + } + return items, nil +} +func (f *webdavMultiFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) { + fs, rel, err := f.root(name) + if err != nil { + return nil, false, err + } + if fs == nil || rel == "" { + return nil, false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot create virtual root")) + } + fi, created, err := fs.Create(ctx, "/"+rel, body, opts) + if err == nil { + fi.Path = "/" + strings.Trim(name, "/") + } + return fi, created, err +} +func (f *webdavMultiFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error { + fs, rel, err := f.root(name) + if err != nil { + return err + } + if fs == nil || rel == "" { + return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot remove virtual root")) + } + return fs.RemoveAll(ctx, "/"+rel, opts) +} +func (f *webdavMultiFS) Mkdir(ctx context.Context, name string) error { + fs, rel, err := f.root(name) + if err != nil { + return err + } + if fs == nil || rel == "" { + return webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cannot create virtual root")) + } + return fs.Mkdir(ctx, "/"+rel) +} +func (f *webdavMultiFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) { + fs, rel, err := f.root(name) + if err != nil { + return false, err + } + dfs, drel, err := f.root(dest) + if err != nil { + return false, err + } + if fs == nil || dfs == nil || fs != dfs { + return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root copy is not supported")) + } + return fs.Copy(ctx, "/"+rel, "/"+drel, opts) +} +func (f *webdavMultiFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) { + fs, rel, err := f.root(name) + if err != nil { + return false, err + } + dfs, drel, err := f.root(dest) + if err != nil { + return false, err + } + if fs == nil || dfs == nil || fs != dfs { + return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("cross-root move is not supported")) + } + return fs.Move(ctx, "/"+rel, "/"+drel, opts) +} + type webdavAuthFS struct { inner webdav.FileSystem account *auth.Account