From bf526cac2aa324c3a810da48d1901b94db5181ad Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 22:34:16 +0800 Subject: [PATCH] fix: preserve WebDAV MOVE destination precedence --- chfs-ng/internal/api/server.go | 22 +++++++++++++++++++--- chfs-ng/internal/api/webdav_test.go | 27 +++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index deb8edc..25d672a 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -1437,10 +1437,19 @@ func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav // This matters because MOVE to an occupied destination returns 412 even // when the source has disappeared, while a missing source with a free // destination is reported as 403. - if opts == nil || !opts.NoOverwrite { - if err := f.authorizeOverwrite(ctx, dest); err != nil { - return false, err + destExists := false + if _, err := f.inner.Stat(ctx, dest); err == nil { + destExists = true + } else if !isWebDAVNotFound(err) { + return false, err + } + if destExists { + if opts == nil || !opts.NoOverwrite { + if err := f.authorizeOverwrite(ctx, dest); err != nil { + return false, err + } } + return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists")) } if _, err := f.inner.Stat(ctx, name); err != nil { if os.IsNotExist(err) { @@ -1454,6 +1463,13 @@ func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav return f.inner.Move(ctx, name, dest, opts) } +func isWebDAVNotFound(err error) bool { + if err == nil { + return false + } + return os.IsNotExist(err) || strings.HasPrefix(err.Error(), "404 Not Found:") +} + func (f *webdavAuthFS) authorizeOverwrite(ctx context.Context, dest string) error { info, err := f.inner.Stat(ctx, dest) if err != nil { diff --git a/chfs-ng/internal/api/webdav_test.go b/chfs-ng/internal/api/webdav_test.go index 2c936c4..4f482e0 100644 --- a/chfs-ng/internal/api/webdav_test.go +++ b/chfs-ng/internal/api/webdav_test.go @@ -24,11 +24,11 @@ func TestWebDAVAuthFSCheckTree(t *testing.T) { t.Fatal(err) } - account := &auth.Account{Mask: auth.Delete, Rules: []auth.PathRule{ - {Dir: "test", Mask: auth.Delete}, + account := &auth.Account{Mask: auth.Read | auth.Write | auth.Delete, Rules: []auth.PathRule{ + {Dir: "test", Mask: auth.Read | auth.Write | auth.Delete}, {Dir: "test/private", Mask: auth.None}, }} - fs := &webdavAuthFS{inner: webdav.LocalFileSystem(root), account: account} + fs := &webdavAuthFS{inner: &webdavStorageFS{store: storage.Local{Root: root}}, account: account} if err := fs.checkTree(context.Background(), "/test", auth.Delete); err == nil { t.Fatal("expected recursive delete permission check to reject inaccessible descendant") @@ -47,7 +47,7 @@ func TestWebDAVAuthFSOverwriteRequiresDelete(t *testing.T) { t.Fatal(err) } account := &auth.Account{Mask: auth.Write} - fs := &webdavAuthFS{inner: webdav.LocalFileSystem(root), account: account} + fs := &webdavAuthFS{inner: &webdavStorageFS{store: storage.Local{Root: root}}, account: account} if err := fs.authorizeOverwrite(context.Background(), "/dst.txt"); err == nil { t.Fatal("expected overwrite authorization to require delete permission") } @@ -94,6 +94,25 @@ func TestWebDAVStorageFSRejectsDestinationInsideSource(t *testing.T) { } } +func TestWebDAVAuthFSMoveMissingSourcePrecedence(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "existing.txt"), []byte("existing"), 0o644); err != nil { + t.Fatal(err) + } + account := &auth.Account{Mask: auth.Read | auth.Write | auth.Delete} + fs := &webdavAuthFS{inner: &webdavStorageFS{store: storage.Local{Root: root}}, account: account} + + _, err := fs.Move(context.Background(), "/missing.txt", "/new.txt", &webdav.MoveOptions{}) + if err == nil || err.Error() != "403 Forbidden: source not found" { + t.Fatalf("missing source to free destination: got %v", err) + } + + _, err = fs.Move(context.Background(), "/missing.txt", "/existing.txt", &webdav.MoveOptions{}) + if err == nil || err.Error() != "412 Precondition Failed: destination exists" { + t.Fatalf("missing source to occupied destination: got %v", err) + } +} + func TestWebDAVStorageFSCopyAndMove(t *testing.T) { root1 := t.TempDir() root2 := t.TempDir()