fix: harden webdav directory copies

This commit is contained in:
OpenClaw Agent
2026-09-22 00:55:29 +08:00
parent a7b111234d
commit 098d79c253
2 changed files with 57 additions and 16 deletions
+55 -14
View File
@@ -1236,19 +1236,28 @@ func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *web
return false, err return false, err
} }
if info.IsDir() { 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 { if err := f.store.MkdirAll(dst); err != nil {
return false, err return false, err
} }
items, err := f.store.List(src) items, err := f.store.List(src)
if err != nil { if err != nil {
if !dstExists {
_ = f.store.Remove(dst)
}
return false, err return false, err
} }
for _, item := range items { for _, item := range items {
created, err := f.Copy(ctx, path.Join(src, item.Name()), path.Join(dst, item.Name()), opts) if _, err := f.Copy(ctx, path.Join(src, item.Name()), path.Join(dst, item.Name()), opts); err != nil {
if err != nil { if !dstExists {
_ = f.store.Remove(dst)
}
return false, err return false, err
} }
_ = created
} }
return !dstExists, nil return !dstExists, nil
} }
@@ -1257,26 +1266,59 @@ func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *web
return false, err return false, err
} }
defer in.Close() 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 false, err
} }
return !dstExists, nil return !dstExists, nil
} }
func copyFileToDestination(st storage.Storage, src io.Reader, dst string, dstExists bool) error { func preflightCopyNoOverwrite(st storage.Storage, src, dst string) error {
tmp := fmt.Sprintf("%s.chfs-copy-%d", dst, time.Now().UnixNano()) if _, err := st.Stat(dst); err == nil {
for i := 0; i < 16; i++ { return webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
if _, err := st.Stat(tmp); os.IsNotExist(err) { } else if !os.IsNotExist(err) {
break
} else if err != nil {
return err return err
} }
tmp = fmt.Sprintf("%s.chfs-copy-%d-%d", dst, time.Now().UnixNano(), i) info, err := st.Stat(src)
}
out, err := createExclusive(st, tmp)
if err != nil { if err != nil {
return err 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) _, copyErr := io.Copy(out, src)
closeErr := out.Close() closeErr := out.Close()
if copyErr != nil { if copyErr != nil {
@@ -1291,7 +1333,6 @@ func copyFileToDestination(st storage.Storage, src io.Reader, dst string, dstExi
_ = st.Remove(tmp) _ = st.Remove(tmp)
return err return err
} }
_ = dstExists
return nil return nil
} }
+1 -1
View File
@@ -153,7 +153,7 @@ func TestWebDAVStorageFSCopyFailureDoesNotCorruptDestination(t *testing.T) {
if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("original"), 0o644); err != nil { if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("original"), 0o644); err != nil {
t.Fatal(err) 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") t.Fatal("expected source read failure")
} }
got, err := os.ReadFile(filepath.Join(root, "dst.txt")) got, err := os.ReadFile(filepath.Join(root, "dst.txt"))