From 098d79c2534cfd3630b8f48328725bd995abdcbc Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Tue, 22 Sep 2026 00:55:29 +0800 Subject: [PATCH] fix: harden webdav directory copies --- chfs-ng/internal/api/server.go | 71 +++++++++++++++++++++++------ chfs-ng/internal/api/webdav_test.go | 2 +- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 8f0b8f5..b7f2b4c 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -1236,19 +1236,28 @@ func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *web return false, err } if info.IsDir() { + if opts != nil && opts.NoOverwrite { + if err := preflightCopyNoOverwrite(f.store, src, dst); err != nil { + return false, err + } + } if err := f.store.MkdirAll(dst); err != nil { return false, err } items, err := f.store.List(src) if err != nil { + if !dstExists { + _ = f.store.Remove(dst) + } 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 { + if _, err := f.Copy(ctx, path.Join(src, item.Name()), path.Join(dst, item.Name()), opts); err != nil { + if !dstExists { + _ = f.store.Remove(dst) + } return false, err } - _ = created } return !dstExists, nil } @@ -1257,26 +1266,59 @@ func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *web return false, err } defer in.Close() - if err := copyFileToDestination(f.store, in, dst, dstExists); err != nil { + if err := copyFileToDestination(f.store, in, dst); err != nil { return false, err } return !dstExists, nil } -func copyFileToDestination(st storage.Storage, src io.Reader, dst string, dstExists bool) error { - tmp := fmt.Sprintf("%s.chfs-copy-%d", dst, time.Now().UnixNano()) - for i := 0; i < 16; i++ { - if _, err := st.Stat(tmp); os.IsNotExist(err) { - break - } else if err != nil { - return err - } - tmp = fmt.Sprintf("%s.chfs-copy-%d-%d", dst, time.Now().UnixNano(), i) +func preflightCopyNoOverwrite(st storage.Storage, src, dst string) error { + if _, err := st.Stat(dst); err == nil { + return webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists")) + } else if !os.IsNotExist(err) { + return err } - out, err := createExclusive(st, tmp) + info, err := st.Stat(src) if err != nil { return err } + if !info.IsDir() { + return nil + } + items, err := st.List(src) + if err != nil { + return err + } + for _, item := range items { + if err := preflightCopyNoOverwrite(st, path.Join(src, item.Name()), path.Join(dst, item.Name())); err != nil { + return err + } + } + return nil +} + +func copyFileToDestination(st storage.Storage, src io.Reader, dst string) error { + var tmp string + var out *os.File + for i := 0; i < 16; i++ { + suffix := fmt.Sprintf("%d", time.Now().UnixNano()) + if i > 0 { + suffix = fmt.Sprintf("%s-%d", suffix, i) + } + candidate := fmt.Sprintf("%s.chfs-copy-%s", dst, suffix) + var err error + out, err = createExclusive(st, candidate) + if err == nil { + tmp = candidate + break + } + if !os.IsExist(err) { + return err + } + } + if out == nil { + return fmt.Errorf("unable to allocate temporary copy path") + } _, copyErr := io.Copy(out, src) closeErr := out.Close() if copyErr != nil { @@ -1291,7 +1333,6 @@ func copyFileToDestination(st storage.Storage, src io.Reader, dst string, dstExi _ = st.Remove(tmp) return err } - _ = dstExists return nil } diff --git a/chfs-ng/internal/api/webdav_test.go b/chfs-ng/internal/api/webdav_test.go index 7b302a2..c16c765 100644 --- a/chfs-ng/internal/api/webdav_test.go +++ b/chfs-ng/internal/api/webdav_test.go @@ -153,7 +153,7 @@ func TestWebDAVStorageFSCopyFailureDoesNotCorruptDestination(t *testing.T) { if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("original"), 0o644); err != nil { t.Fatal(err) } - if err := copyFileToDestination(store, failingReader{}, "dst.txt", true); err == nil { + if err := copyFileToDestination(store, failingReader{}, "dst.txt"); err == nil { t.Fatal("expected source read failure") } got, err := os.ReadFile(filepath.Join(root, "dst.txt"))