fix: harden webdav directory copies
This commit is contained in:
@@ -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
|
return err
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmp = fmt.Sprintf("%s.chfs-copy-%d-%d", dst, time.Now().UnixNano(), i)
|
|
||||||
}
|
}
|
||||||
out, err := createExclusive(st, tmp)
|
info, err := st.Stat(src)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user