Fix move command - stop it running for overlapping fses - fixes #577

* Make move command check for overlapping remotes and refuse to run
  * Do copy/delete rather than all the copies then all the deletes
  * Doesn't purge the source - this was unexpected behaviour see #512 and #416
  * Add -list-retries flag to test suite to control retries

This changes the semantics of `move` slightly.  However it now errs on
the side of not deleting stuff.
This commit is contained in:
Nick Craig-Wood 2016-07-11 11:36:46 +01:00
parent b9479cf7ab
commit a6056408dd
8 changed files with 126 additions and 59 deletions

View File

@ -113,16 +113,18 @@ go there.
### move source:path dest:path ### ### move source:path dest:path ###
Moves the source to the destination. Moves the contents of the source directory to the destination
directory. Rclone will error if the source and destination overlap.
If there are no filters in use this is equivalent to a copy followed If no filters are in use and if possible this will server side move
by a purge, but may use server side operations to speed it up if `source:path` into `dest:path`. After this `source:path` will no
possible. longer longer exist.
If filters are in use then it is equivalent to a copy followed by Otherwise for each file in `source:path` selected by the filters (if
delete, followed by an rmdir (which only removes the directory if any) this will move it into `dest:path`. If possible a server side
empty). The individual file moves will be moved with server side move will be used, otherwise it will copy it (server side if possible)
operations if possible. into `dest:path` then delete the original (if no errors on copy) in
`source:path`.
**Important**: Since this can cause data loss, test first with the **Important**: Since this can cause data loss, test first with the
--dry-run flag. --dry-run flag.

View File

@ -45,6 +45,7 @@ var (
ErrorListOnlyRoot = errors.New("can only list from root") ErrorListOnlyRoot = errors.New("can only list from root")
ErrorIsFile = errors.New("is a file not a directory") ErrorIsFile = errors.New("is a file not a directory")
ErrorNotDeleting = errors.New("not deleting files as there were IO errors") ErrorNotDeleting = errors.New("not deleting files as there were IO errors")
ErrorCantMoveOverlapping = errors.New("can't move files on overlapping remotes")
) )
// RegInfo provides information about a filesystem // RegInfo provides information about a filesystem

View File

@ -452,6 +452,12 @@ func Same(fdst, fsrc Fs) bool {
return fdst.Name() == fsrc.Name() && fdst.Root() == fsrc.Root() return fdst.Name() == fsrc.Name() && fdst.Root() == fsrc.Root()
} }
// Overlapping returns true if fdst and fsrc point to the same
// underlying Fs or they overlap.
func Overlapping(fdst, fsrc Fs) bool {
return fdst.Name() == fsrc.Name() && (strings.HasPrefix(fdst.Root(), fsrc.Root()) || strings.HasPrefix(fsrc.Root(), fdst.Root()))
}
// checkIdentical checks to see if dst and src are identical // checkIdentical checks to see if dst and src are identical
// //
// it returns true if differences were found // it returns true if differences were found

View File

@ -75,6 +75,7 @@ type Run struct {
localName string localName string
flocal fs.Fs flocal fs.Fs
fremote fs.Fs fremote fs.Fs
fremoteName string
cleanRemote func() cleanRemote func()
mkdir map[string]bool // whether the remote has been made yet for the fs name mkdir map[string]bool // whether the remote has been made yet for the fs name
Logf, Fatalf func(text string, args ...interface{}) Logf, Fatalf func(text string, args ...interface{})
@ -108,7 +109,7 @@ func newRun() *Run {
fs.Config.DumpBodies = *DumpBodies fs.Config.DumpBodies = *DumpBodies
fs.Config.LowLevelRetries = *LowLevelRetries fs.Config.LowLevelRetries = *LowLevelRetries
var err error var err error
r.fremote, r.cleanRemote, err = fstest.RandomRemote(*RemoteName, *SubDir) r.fremote, r.fremoteName, r.cleanRemote, err = fstest.RandomRemote(*RemoteName, *SubDir)
if err != nil { if err != nil {
r.Fatalf("Failed to open remote %q: %v", *RemoteName, err) r.Fatalf("Failed to open remote %q: %v", *RemoteName, err)
} }

View File

@ -264,14 +264,10 @@ func (s *syncCopyMove) pairMover(in ObjectPairChan, fdst Fs, wg *sync.WaitGroup)
Log(src, "Not moving as --dry-run") Log(src, "Not moving as --dry-run")
} else if haveMover && src.Fs().Name() == fdst.Name() { } else if haveMover && src.Fs().Name() == fdst.Name() {
// Delete destination if it exists // Delete destination if it exists
if pair.dst != nil { if dst != nil {
err := dst.Remove() s.processError(DeleteFile(src))
if err != nil {
Stats.Error()
ErrorLog(dst, "Couldn't delete: %v", err)
s.processError(err)
}
} }
// Move dst <- src
_, err := fdstMover.Move(src, src.Remote()) _, err := fdstMover.Move(src, src.Remote())
if err != nil { if err != nil {
Stats.Error() Stats.Error()
@ -281,7 +277,15 @@ func (s *syncCopyMove) pairMover(in ObjectPairChan, fdst Fs, wg *sync.WaitGroup)
Debug(src, "Moved") Debug(src, "Moved")
} }
} else { } else {
s.processError(Copy(fdst, pair.dst, src)) // Copy dst <- src
err := Copy(fdst, dst, src)
s.processError(err)
if err != nil {
ErrorLog(src, "Not deleting as copy failed: %v", err)
} else {
// Delete src if no error on copy
s.processError(DeleteFile(src))
}
} }
Stats.DoneTransferring(src.Remote()) Stats.DoneTransferring(src.Remote())
case <-s.abort: case <-s.abort:
@ -491,14 +495,24 @@ func MoveDir(fdst, fsrc Fs) error {
ErrorLog(fdst, "Nothing to do as source and destination are the same") ErrorLog(fdst, "Nothing to do as source and destination are the same")
return nil return nil
} }
// The two remotes mustn't overlap
if Overlapping(fdst, fsrc) {
err := ErrorCantMoveOverlapping
ErrorLog(fdst, "%v", err)
return err
}
// First attempt to use DirMover if exists, same Fs and no filters are active // First attempt to use DirMover if exists, same Fs and no filters are active
if fdstDirMover, ok := fdst.(DirMover); ok && fsrc.Name() == fdst.Name() && Config.Filter.InActive() { if fdstDirMover, ok := fdst.(DirMover); ok && fsrc.Name() == fdst.Name() && Config.Filter.InActive() {
err := fdstDirMover.DirMove(fsrc) if Config.DryRun {
Log(fdst, "Not doing server side directory move as --dry-run")
return nil
}
Debug(fdst, "Using server side directory move") Debug(fdst, "Using server side directory move")
err := fdstDirMover.DirMove(fsrc)
switch err { switch err {
case ErrorCantDirMove, ErrorDirExists: case ErrorCantDirMove, ErrorDirExists:
Debug(fdst, "Server side directory move failed - fallback to copy/delete: %v", err) Debug(fdst, "Server side directory move failed - fallback to file moves: %v", err)
case nil: case nil:
Debug(fdst, "Server side directory move succeeded") Debug(fdst, "Server side directory move succeeded")
return nil return nil
@ -509,22 +523,6 @@ func MoveDir(fdst, fsrc Fs) error {
} }
} }
// Now move the files // Otherwise move the files one by one
err := moveDir(fdst, fsrc) return moveDir(fdst, fsrc)
if err != nil || Stats.Errored() {
ErrorLog(fdst, "Not deleting files as there were IO errors")
return err
}
// If no filters then purge
if Config.Filter.InActive() {
return Purge(fsrc)
}
// Otherwise remove any remaining files obeying filters
err = Delete(fsrc)
if err != nil {
return err
}
// and try to remove the directory if empty - ignoring error
_ = TryRmdir(fsrc)
return nil
} }

View File

@ -100,7 +100,7 @@ func TestServerSideCopy(t *testing.T) {
file1 := r.WriteObject("sub dir/hello world", "hello world", t1) file1 := r.WriteObject("sub dir/hello world", "hello world", t1)
fstest.CheckItems(t, r.fremote, file1) fstest.CheckItems(t, r.fremote, file1)
fremoteCopy, finaliseCopy, err := fstest.RandomRemote(*RemoteName, *SubDir) fremoteCopy, _, finaliseCopy, err := fstest.RandomRemote(*RemoteName, *SubDir)
require.NoError(t, err) require.NoError(t, err)
defer finaliseCopy() defer finaliseCopy()
t.Logf("Server side copy (if possible) %v -> %v", r.fremote, fremoteCopy) t.Logf("Server side copy (if possible) %v -> %v", r.fremote, fremoteCopy)
@ -563,17 +563,12 @@ func TestSyncWithUpdateOlder(t *testing.T) {
} }
// Test a server side move if possible, or the backup path if not // Test a server side move if possible, or the backup path if not
func TestServerSideMove(t *testing.T) { func testServerSideMove(t *testing.T, r *Run, fremoteMove fs.Fs, withFilter bool) {
r := NewRun(t)
defer r.Finalise()
file1 := r.WriteBoth("potato2", "------------------------------------------------------------", t1) file1 := r.WriteBoth("potato2", "------------------------------------------------------------", t1)
file2 := r.WriteBoth("empty space", "", t2) file2 := r.WriteBoth("empty space", "", t2)
fstest.CheckItems(t, r.fremote, file2, file1) fstest.CheckItems(t, r.fremote, file2, file1)
fremoteMove, finaliseMove, err := fstest.RandomRemote(*RemoteName, *SubDir)
require.NoError(t, err)
defer finaliseMove()
t.Logf("Server side move (if possible) %v -> %v", r.fremote, fremoteMove) t.Logf("Server side move (if possible) %v -> %v", r.fremote, fremoteMove)
// Write just one file in the new remote // Write just one file in the new remote
@ -582,17 +577,75 @@ func TestServerSideMove(t *testing.T) {
// Do server side move // Do server side move
fs.Stats.ResetCounters() fs.Stats.ResetCounters()
err = fs.MoveDir(fremoteMove, r.fremote) err := fs.MoveDir(fremoteMove, r.fremote)
require.NoError(t, err) require.NoError(t, err)
fstest.CheckItems(t, r.fremote) fstest.CheckItems(t, r.fremote, file2)
fstest.CheckItems(t, fremoteMove, file2, file1) fstest.CheckItems(t, fremoteMove, file2, file1)
// Purge the original before moving
require.NoError(t, fs.Purge(r.fremote))
// Move it back again, dst does not exist this time // Move it back again, dst does not exist this time
fs.Stats.ResetCounters() fs.Stats.ResetCounters()
err = fs.MoveDir(r.fremote, fremoteMove) err = fs.MoveDir(r.fremote, fremoteMove)
require.NoError(t, err) require.NoError(t, err)
fstest.CheckItems(t, r.fremote, file2, file1) if withFilter {
fstest.CheckItems(t, fremoteMove) fstest.CheckItems(t, r.fremote, file1)
fstest.CheckItems(t, fremoteMove, file2)
} else {
fstest.CheckItems(t, r.fremote, file2, file1)
fstest.CheckItems(t, fremoteMove)
}
}
// Test a server side move if possible, or the backup path if not
func TestServerSideMove(t *testing.T) {
r := NewRun(t)
defer r.Finalise()
fremoteMove, _, finaliseMove, err := fstest.RandomRemote(*RemoteName, *SubDir)
require.NoError(t, err)
defer finaliseMove()
testServerSideMove(t, r, fremoteMove, false)
}
// Test a server side move if possible, or the backup path if not
func TestServerSideMoveWithFilter(t *testing.T) {
r := NewRun(t)
defer r.Finalise()
fs.Config.Filter.MinSize = 40
defer func() {
fs.Config.Filter.MinSize = -1
}()
fremoteMove, _, finaliseMove, err := fstest.RandomRemote(*RemoteName, *SubDir)
require.NoError(t, err)
defer finaliseMove()
testServerSideMove(t, r, fremoteMove, true)
}
// Test a server side move with overlap
func TestServerSideMoveOverlap(t *testing.T) {
r := NewRun(t)
defer r.Finalise()
subRemoteName := r.fremoteName + "/rclone-move-test"
fremoteMove, err := fs.NewFs(subRemoteName)
require.NoError(t, err)
file1 := r.WriteObject("potato2", "------------------------------------------------------------", t1)
fstest.CheckItems(t, r.fremote, file1)
// Subdir move with no filters should return ErrorCantMoveOverlapping
err = fs.MoveDir(fremoteMove, r.fremote)
assert.EqualError(t, err, fs.ErrorCantMoveOverlapping.Error())
// Now try with a filter which should also fail with ErrorCantMoveOverlapping
fs.Config.Filter.MinSize = 40
defer func() {
fs.Config.Filter.MinSize = -1
}()
err = fs.MoveDir(fremoteMove, r.fremote)
assert.EqualError(t, err, fs.ErrorCantMoveOverlapping.Error())
} }

View File

@ -5,6 +5,7 @@ package fstest
import ( import (
"bytes" "bytes"
"flag"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -25,6 +26,7 @@ import (
var ( var (
// MatchTestRemote matches the remote names used for testing // MatchTestRemote matches the remote names used for testing
MatchTestRemote = regexp.MustCompile(`^rclone-test-[abcdefghijklmnopqrstuvwxyz0123456789]{24}$`) MatchTestRemote = regexp.MustCompile(`^rclone-test-[abcdefghijklmnopqrstuvwxyz0123456789]{24}$`)
listRetries = flag.Int("list-retries", 6, "Number or times to retry listing")
) )
// Seed the random number generator // Seed the random number generator
@ -123,9 +125,11 @@ func (is *Items) Find(t *testing.T, obj fs.Object, precision time.Duration) {
i, ok = is.byNameAlt[obj.Remote()] i, ok = is.byNameAlt[obj.Remote()]
assert.True(t, ok, fmt.Sprintf("Unexpected file %q", obj.Remote())) assert.True(t, ok, fmt.Sprintf("Unexpected file %q", obj.Remote()))
} }
delete(is.byName, i.Path) if i != nil {
delete(is.byName, i.WinPath) delete(is.byName, i.Path)
i.Check(t, obj, precision) delete(is.byName, i.WinPath)
i.Check(t, obj, precision)
}
} }
// Done checks all finished // Done checks all finished
@ -134,8 +138,8 @@ func (is *Items) Done(t *testing.T) {
for name := range is.byName { for name := range is.byName {
t.Logf("Not found %q", name) t.Logf("Not found %q", name)
} }
t.Errorf("%d objects not found", len(is.byName))
} }
assert.Equal(t, 0, len(is.byName), fmt.Sprintf("%d objects not found", len(is.byName)))
} }
// CheckListingWithPrecision checks the fs to see if it has the // CheckListingWithPrecision checks the fs to see if it has the
@ -145,7 +149,7 @@ func CheckListingWithPrecision(t *testing.T, f fs.Fs, items []Item, precision ti
oldErrors := fs.Stats.GetErrors() oldErrors := fs.Stats.GetErrors()
var objs []fs.Object var objs []fs.Object
var err error var err error
const retries = 6 var retries = *listRetries
sleep := time.Second / 2 sleep := time.Second / 2
for i := 1; i <= retries; i++ { for i := 1; i <= retries; i++ {
objs, err = fs.NewLister().Start(f, "").GetObjects() objs, err = fs.NewLister().Start(f, "").GetObjects()
@ -257,26 +261,28 @@ func RandomRemoteName(remoteName string) (string, string, error) {
// //
// Call the finalise function returned to Purge the fs at the end (and // Call the finalise function returned to Purge the fs at the end (and
// the parent if necessary) // the parent if necessary)
func RandomRemote(remoteName string, subdir bool) (fs.Fs, func(), error) { //
// Returns the remote, its url, a finaliser and an error
func RandomRemote(remoteName string, subdir bool) (fs.Fs, string, func(), error) {
var err error var err error
var parentRemote fs.Fs var parentRemote fs.Fs
remoteName, _, err = RandomRemoteName(remoteName) remoteName, _, err = RandomRemoteName(remoteName)
if err != nil { if err != nil {
return nil, nil, err return nil, "", nil, err
} }
if subdir { if subdir {
parentRemote, err = fs.NewFs(remoteName) parentRemote, err = fs.NewFs(remoteName)
if err != nil { if err != nil {
return nil, nil, err return nil, "", nil, err
} }
remoteName += "/rclone-test-subdir-" + RandomString(8) remoteName += "/rclone-test-subdir-" + RandomString(8)
} }
remote, err := fs.NewFs(remoteName) remote, err := fs.NewFs(remoteName)
if err != nil { if err != nil {
return nil, nil, err return nil, "", nil, err
} }
finalise := func() { finalise := func() {
@ -289,7 +295,7 @@ func RandomRemote(remoteName string, subdir bool) (fs.Fs, func(), error) {
} }
} }
return remote, finalise, nil return remote, remoteName, finalise, nil
} }
// TestMkdir tests Mkdir works // TestMkdir tests Mkdir works

View File

@ -395,7 +395,7 @@ func TestFsDirMove(t *testing.T) {
require.Equal(t, fs.ErrorDirExists, err) require.Equal(t, fs.ErrorDirExists, err)
// new remote // new remote
newRemote, removeNewRemote, err := fstest.RandomRemote(RemoteName, false) newRemote, _, removeNewRemote, err := fstest.RandomRemote(RemoteName, false)
require.NoError(t, err) require.NoError(t, err)
defer removeNewRemote() defer removeNewRemote()