From e5190f14ce843c2bed0a49f083207945f98f8ab2 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 15 Sep 2020 13:32:08 +0100 Subject: [PATCH] drive: implement "rclone backend copyid" command for copying files by ID This allows files to be copied by ID from google drive. These can be copied to any rclone remote and if the remote is a google drive then server side copy will be attempted. Fixes #3625 --- backend/drive/drive.go | 69 ++++++++++++++++++++++++++++ backend/drive/drive_internal_test.go | 65 +++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 6 deletions(-) diff --git a/backend/drive/drive.go b/backend/drive/drive.go index 26606f7ca..07a4b92e3 100755 --- a/backend/drive/drive.go +++ b/backend/drive/drive.go @@ -35,6 +35,7 @@ import ( "github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/fserrors" "github.com/rclone/rclone/fs/fshttp" + "github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/hash" "github.com/rclone/rclone/fs/operations" "github.com/rclone/rclone/fs/walk" @@ -2959,6 +2960,38 @@ func (f *Fs) unTrashDir(ctx context.Context, dir string, recurse bool) (r unTras return f.unTrash(ctx, dir, directoryID, true) } +// copy file with id to dest +func (f *Fs) copyID(ctx context.Context, id, dest string) (err error) { + info, err := f.getFile(id, f.fileFields) + if err != nil { + return errors.Wrap(err, "couldn't find id") + } + if info.MimeType == driveFolderType { + return errors.Errorf("can't copy directory use: rclone copy --drive-root-folder-id %s %s %s", id, fs.ConfigString(f), dest) + } + info.Name = f.opt.Enc.ToStandardName(info.Name) + o, err := f.newObjectWithInfo(info.Name, info) + if err != nil { + return err + } + destDir, destLeaf, err := fspath.Split(dest) + if err != nil { + return err + } + if destLeaf == "" { + destLeaf = info.Name + } + dstFs, err := cache.Get(destDir) + if err != nil { + return err + } + _, err = operations.Copy(ctx, dstFs, nil, destLeaf, o) + if err != nil { + return errors.Wrap(err, "copy failed") + } + return nil +} + var commandHelp = []fs.CommandHelp{{ Name: "get", Short: "Get command for fetching the drive config parameters", @@ -3059,6 +3092,29 @@ Result: "Errors": 0 } `, +}, { + Name: "copyid", + Short: "Copy files by ID", + Long: `This command copies files by ID + +Usage: + + rclone backend copyid drive: ID path + rclone backend copyid drive: ID1 path1 ID2 path2 + +It copies the drive file with ID given to the path (an rclone path which +will be passed internally to rclone copyto). The ID and path pairs can be +repeated. + +The path should end with a / to indicate copy the file as named to +this directory. If it doesn't end with a / then the last path +component will be used as the file name. + +If the destination is a drive backend then server side copying will be +attempted if possible. + +Use the -i flag to see what would be copied before copying. +`, }} // Command the backend to run a named command @@ -3130,6 +3186,19 @@ func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[str dir = arg[0] } return f.unTrashDir(ctx, dir, true) + case "copyid": + if len(arg)%2 != 0 { + return nil, errors.New("need an even number of arguments") + } + for len(arg) > 0 { + id, dest := arg[0], arg[1] + arg = arg[2:] + err = f.copyID(ctx, id, dest) + if err != nil { + return nil, errors.Wrapf(err, "failed copying %q to %q", id, dest) + } + } + return nil, nil default: return nil, fs.ErrorCommandNotFound } diff --git a/backend/drive/drive_internal_test.go b/backend/drive/drive_internal_test.go index 334191bae..4a1423eb0 100644 --- a/backend/drive/drive_internal_test.go +++ b/backend/drive/drive_internal_test.go @@ -7,6 +7,8 @@ import ( "io" "io/ioutil" "mime" + "os" + "path" "path/filepath" "strings" "testing" @@ -272,14 +274,15 @@ func (f *Fs) InternalTestDocumentLink(t *testing.T) { } } +const ( + // from fstest/fstests/fstests.go + existingDir = "hello? sausage" + existingFile = `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt` + existingSubDir = "êé" +) + // TestIntegration/FsMkdir/FsPutFiles/Internal/Shortcuts func (f *Fs) InternalTestShortcuts(t *testing.T) { - const ( - // from fstest/fstests/fstests.go - existingDir = "hello? sausage" - existingFile = `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt` - existingSubDir = "êé" - ) ctx := context.Background() srcObj, err := f.NewObject(ctx, existingFile) require.NoError(t, err) @@ -408,6 +411,55 @@ func (f *Fs) InternalTestUnTrash(t *testing.T) { require.NoError(t, f.Purge(ctx, "trashDir")) } +// TestIntegration/FsMkdir/FsPutFiles/Internal/CopyID +func (f *Fs) InternalTestCopyID(t *testing.T) { + ctx := context.Background() + obj, err := f.NewObject(ctx, existingFile) + require.NoError(t, err) + o := obj.(*Object) + + dir, err := ioutil.TempDir("", "rclone-drive-copyid-test") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(dir) + }() + + checkFile := func(name string) { + filePath := filepath.Join(dir, name) + fi, err := os.Stat(filePath) + require.NoError(t, err) + assert.Equal(t, int64(100), fi.Size()) + err = os.Remove(filePath) + require.NoError(t, err) + } + + t.Run("BadID", func(t *testing.T) { + err = f.copyID(ctx, "ID-NOT-FOUND", dir+"/") + require.Error(t, err) + assert.Contains(t, err.Error(), "couldn't find id") + }) + + t.Run("Directory", func(t *testing.T) { + rootID, err := f.dirCache.RootID(ctx, false) + require.NoError(t, err) + err = f.copyID(ctx, rootID, dir+"/") + require.Error(t, err) + assert.Contains(t, err.Error(), "can't copy directory") + }) + + t.Run("WithoutDestName", func(t *testing.T) { + err = f.copyID(ctx, o.id, dir+"/") + require.NoError(t, err) + checkFile(path.Base(existingFile)) + }) + + t.Run("WithDestName", func(t *testing.T) { + err = f.copyID(ctx, o.id, dir+"/potato.txt") + require.NoError(t, err) + checkFile("potato.txt") + }) +} + func (f *Fs) InternalTest(t *testing.T) { // These tests all depend on each other so run them as nested tests t.Run("DocumentImport", func(t *testing.T) { @@ -424,6 +476,7 @@ func (f *Fs) InternalTest(t *testing.T) { }) t.Run("Shortcuts", f.InternalTestShortcuts) t.Run("UnTrash", f.InternalTestUnTrash) + t.Run("CopyID", f.InternalTestCopyID) } var _ fstests.InternalTester = (*Fs)(nil)