Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions drivers/139/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package _139

import "testing"

func TestConfigNoOverwriteRenameOnlyForPersonalNew(t *testing.T) {
for typ, want := range map[string]bool{
MetaPersonalNew: true,
MetaPersonal: false,
MetaFamily: false,
MetaGroup: false,
"share": false,
} {
d := &Yun139{Addition: Addition{Type: typ}}
if got := d.Config().NoOverwriteRename; got != want {
t.Errorf("type %q: NoOverwriteRename=%v, want %v", typ, got, want)
}
}
}
6 changes: 5 additions & 1 deletion drivers/139/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ type Yun139 struct {
}

func (d *Yun139) Config() driver.Config {
return config
c := config
// Verified for personal_new only: /file/update onto an existing name keeps
// both files and gives the renamed one a timestamp suffix.
c.NoOverwriteRename = d.Addition.Type == MetaPersonalNew
return c
}

func (d *Yun139) GetAddition() driver.Additional {
Expand Down
4 changes: 4 additions & 0 deletions internal/driver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type Config struct {
Alert string `json:"alert"`
// whether to support overwrite upload
NoOverwriteUpload bool `json:"-"`
// Rename never replaces an existing object at the target name: the server
// either rejects the conflict or keeps both objects. WebDAV MOVE relies on
// this to undo a same-directory rename without keeping a copy of the source.
NoOverwriteRename bool `json:"-"`
ProxyRangeOption bool `json:"-"`
// if the driver returns Link without URL, this should be set to true
NoLinkURL bool `json:"-"`
Expand Down
6 changes: 6 additions & 0 deletions internal/op/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ func GetStorageAndActualPath(rawPath string) (storage driver.Driver, actualPath
return
}

// GetStoragesByPath returns every storage that can serve rawPath, which is
// more than one for balanced mounts. It does not advance the balance counter.
func GetStoragesByPath(rawPath string) []driver.Driver {
return getStoragesByPath(utils.FixAndCleanPath(rawPath))
}

// GetStorageVirtualMountPath returns the deterministic virtual mount path
// without advancing the balanced-storage counter.
func GetStorageVirtualMountPath(rawPath string) (string, error) {
Expand Down
66 changes: 57 additions & 9 deletions server/webdav/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type fileMutation struct {
rollback func() error
finalize func()
backupPaths []string
moved bool
}

type fileBackup struct {
Expand Down Expand Up @@ -213,11 +214,47 @@ func copyMutation(ctx context.Context, dstPath, stagePath string, dstBackup *fil
}
}

func moveWithRollback(ctx context.Context, src, dst, dstDir, srcDir, srcName, dstName string, dstExisted bool) (*fileMutation, error) {
// Keep a source copy so a partially completed cross-storage MOVE can be undone.
srcBackup, err := newFileBackup(ctx, src, true)
if err != nil {
return nil, err
// canRenameInPlace reports whether a same-directory rename of src can skip the
// source backup. The rollback is a reverse rename, which is only safe when the
// driver guarantees that a rename never overwrites an existing object. A
// balanced mount picks a storage per operation, so every one behind the path
// must give that guarantee.
func canRenameInPlace(src string) bool {
storages := op.GetStoragesByPath(src)
for _, s := range storages {
if !s.Config().NoOverwriteRename {
return false
}
}
return len(storages) > 0
}

// undoInPlaceRename rolls back a rename that has no source backup.
func undoInPlaceRename(ctx context.Context, moved bool, dst, srcName string, dstBackup *fileBackup) error {
if moved {
if err := fs.Rename(ctx, dst, srcName); err != nil {
// dst holds the only copy of the source; restoring the old
// destination would delete it.
return errors.WithMessagef(err, "failed to rename %s back to %s", dst, srcName)
}
} else if dstBackup != nil {
// A rename that returned an error may still have been applied, and then
// dst holds the only copy of the source. Nothing here can tell that apart
// from a rename that did not happen, so keep the old destination aside.
return errors.Errorf("rename outcome unknown; the old destination is kept at %s", dstBackup.backupPath)
}
return dstBackup.restore(ctx)
}

func moveWithRollback(ctx context.Context, src, dst, dstDir, srcDir, srcName, dstName string, dstExisted, renameInPlace bool) (*fileMutation, error) {
var srcBackup *fileBackup
var err error
if !renameInPlace {
// Keep a source copy so a partially completed cross-storage MOVE can be undone.
srcBackup, err = newFileBackup(ctx, src, true)
if err != nil {
return nil, err
}
}
var dstBackup *fileBackup
if dstExisted {
Expand All @@ -237,18 +274,26 @@ func moveWithRollback(ctx context.Context, src, dst, dstDir, srcDir, srcName, ds
if err := moveResource(ctx, src, dstDir, srcDir, srcName, dstName); err != nil {
return mutation, err
}
mutation.moved = true
return mutation, nil
}

func moveMutation(ctx context.Context, dst, dstDir, srcName, dstName string, srcBackup, dstBackup *fileBackup) *fileMutation {
paths := []string{srcBackup.backupPath}
var paths []string
if srcBackup != nil {
paths = append(paths, srcBackup.backupPath)
}
if dstBackup != nil {
paths = append(paths, dstBackup.backupPath)
}
return &fileMutation{
var m *fileMutation
m = &fileMutation{
backupPaths: paths,
rollback: func() error {
rollbackCtx := context.WithoutCancel(ctx)
if srcBackup == nil {
return undoInPlaceRename(rollbackCtx, m.moved, dst, srcName, dstBackup)
}
var rollbackErrs []error
candidates := []string{dst}
if srcName != dstName {
Expand All @@ -272,6 +317,7 @@ func moveMutation(ctx context.Context, dst, dstDir, srcName, dstName string, src
dstBackup.discard(ctx)
},
}
return m
}

func runDeadPropsFileMutation(ctx context.Context, update func(*gorm.DB) error, apply func() (*fileMutation, error)) error {
Expand Down Expand Up @@ -326,7 +372,8 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int
if !common.CanWrite(user, srcMeta, srcDir) || !common.CanWrite(user, dstMeta, dstDir) {
return http.StatusForbidden, nil
}
if _, err = fs.Get(ctx, src, &fs.GetArgs{}); err != nil {
srcInfo, err := fs.Get(ctx, src, &fs.GetArgs{})
if err != nil {
if errs.IsObjectNotFound(err) {
return http.StatusNotFound, err
}
Expand All @@ -352,7 +399,8 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int
err = runDeadPropsFileMutation(ctx,
func(tx *gorm.DB) error { return moveDeadProps(tx, src, dst) },
func() (*fileMutation, error) {
return moveWithRollback(ctx, src, dst, dstDir, srcDir, srcName, dstName, dstExisted && overwrite)
renameInPlace := srcDir == dstDir && !srcInfo.IsDir() && canRenameInPlace(src)
return moveWithRollback(ctx, src, dst, dstDir, srcDir, srcName, dstName, dstExisted && overwrite, renameInPlace)
},
)
if err != nil {
Expand Down
Loading