fix: preserve WebDAV MOVE destination precedence

This commit is contained in:
OpenClaw Agent
2026-09-21 22:34:16 +08:00
parent 52f0a1218b
commit bf526cac2a
2 changed files with 42 additions and 7 deletions
+16
View File
@@ -1437,11 +1437,20 @@ func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav
// This matters because MOVE to an occupied destination returns 412 even // This matters because MOVE to an occupied destination returns 412 even
// when the source has disappeared, while a missing source with a free // when the source has disappeared, while a missing source with a free
// destination is reported as 403. // destination is reported as 403.
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 opts == nil || !opts.NoOverwrite {
if err := f.authorizeOverwrite(ctx, dest); err != nil { if err := f.authorizeOverwrite(ctx, dest); err != nil {
return false, err return false, err
} }
} }
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
}
if _, err := f.inner.Stat(ctx, name); err != nil { if _, err := f.inner.Stat(ctx, name); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("source not found")) return false, webdav.NewHTTPError(http.StatusForbidden, fmt.Errorf("source not found"))
@@ -1454,6 +1463,13 @@ func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav
return f.inner.Move(ctx, name, dest, opts) 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 { func (f *webdavAuthFS) authorizeOverwrite(ctx context.Context, dest string) error {
info, err := f.inner.Stat(ctx, dest) info, err := f.inner.Stat(ctx, dest)
if err != nil { if err != nil {
+23 -4
View File
@@ -24,11 +24,11 @@ func TestWebDAVAuthFSCheckTree(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
account := &auth.Account{Mask: auth.Delete, Rules: []auth.PathRule{ account := &auth.Account{Mask: auth.Read | auth.Write | auth.Delete, Rules: []auth.PathRule{
{Dir: "test", Mask: auth.Delete}, {Dir: "test", Mask: auth.Read | auth.Write | auth.Delete},
{Dir: "test/private", Mask: auth.None}, {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 { if err := fs.checkTree(context.Background(), "/test", auth.Delete); err == nil {
t.Fatal("expected recursive delete permission check to reject inaccessible descendant") t.Fatal("expected recursive delete permission check to reject inaccessible descendant")
@@ -47,7 +47,7 @@ func TestWebDAVAuthFSOverwriteRequiresDelete(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
account := &auth.Account{Mask: auth.Write} 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 { if err := fs.authorizeOverwrite(context.Background(), "/dst.txt"); err == nil {
t.Fatal("expected overwrite authorization to require delete permission") 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) { func TestWebDAVStorageFSCopyAndMove(t *testing.T) {
root1 := t.TempDir() root1 := t.TempDir()
root2 := t.TempDir() root2 := t.TempDir()