Implement moveto and copyto commands for choosing a destination name on copy/move

Fixes #227
Fixes #476
This commit is contained in:
Nick Craig-Wood
2016-12-03 23:43:52 +00:00
parent 2058652fa4
commit c265f451f2
10 changed files with 332 additions and 26 deletions
+2
View File
@@ -10,6 +10,7 @@ import (
_ "github.com/ncw/rclone/cmd/cleanup"
_ "github.com/ncw/rclone/cmd/config"
_ "github.com/ncw/rclone/cmd/copy"
_ "github.com/ncw/rclone/cmd/copyto"
_ "github.com/ncw/rclone/cmd/dedupe"
_ "github.com/ncw/rclone/cmd/delete"
_ "github.com/ncw/rclone/cmd/genautocomplete"
@@ -23,6 +24,7 @@ import (
_ "github.com/ncw/rclone/cmd/mkdir"
_ "github.com/ncw/rclone/cmd/mount"
_ "github.com/ncw/rclone/cmd/move"
_ "github.com/ncw/rclone/cmd/moveto"
_ "github.com/ncw/rclone/cmd/purge"
_ "github.com/ncw/rclone/cmd/rmdir"
_ "github.com/ncw/rclone/cmd/rmdirs"
+80 -12
View File
@@ -14,6 +14,7 @@ import (
"regexp"
"runtime"
"runtime/pprof"
"strings"
"time"
"github.com/spf13/cobra"
@@ -94,31 +95,50 @@ func ShowVersion() {
fmt.Printf("rclone %s\n", fs.Version)
}
// newFsSrc creates a src Fs from a name
// newFsFile creates a dst Fs from a name but may point to a file.
//
// This can point to a file
func newFsSrc(remote string) fs.Fs {
// It returns a string with the file name if points to a file
func newFsFile(remote string) (fs.Fs, string) {
fsInfo, configName, fsPath, err := fs.ParseRemote(remote)
if err != nil {
fs.Stats.Error()
log.Fatalf("Failed to create file system for %q: %v", remote, err)
}
f, err := fsInfo.NewFs(configName, fsPath)
if err == fs.ErrorIsFile {
switch err {
case fs.ErrorIsFile:
return f, path.Base(fsPath)
case nil:
return f, ""
default:
fs.Stats.Error()
log.Fatalf("Failed to create file system for %q: %v", remote, err)
}
return nil, ""
}
// newFsSrc creates a src Fs from a name
//
// It returns a string with the file name if limiting to one file
//
// This can point to a file
func newFsSrc(remote string) (fs.Fs, string) {
f, fileName := newFsFile(remote)
if fileName != "" {
if !fs.Config.Filter.InActive() {
fs.Stats.Error()
log.Fatalf("Can't limit to single files when using filters: %v", remote)
}
// Limit transfers to this file
err = fs.Config.Filter.AddFile(path.Base(fsPath))
err := fs.Config.Filter.AddFile(fileName)
if err != nil {
fs.Stats.Error()
log.Fatalf("Failed to limit to single file %q: %v", remote, err)
}
// Set --no-traverse as only one file
fs.Config.NoTraverse = true
}
if err != nil {
fs.Stats.Error()
log.Fatalf("Failed to create file system for %q: %v", remote, err)
}
return f
return f, fileName
}
// newFsDst creates a dst Fs from a name
@@ -135,14 +155,62 @@ func newFsDst(remote string) fs.Fs {
// NewFsSrcDst creates a new src and dst fs from the arguments
func NewFsSrcDst(args []string) (fs.Fs, fs.Fs) {
fsrc, fdst := newFsSrc(args[0]), newFsDst(args[1])
fsrc, _ := newFsSrc(args[0])
fdst := newFsDst(args[1])
fs.CalculateModifyWindow(fdst, fsrc)
return fsrc, fdst
}
// RemoteSplit splits a remote into a parent and a leaf
//
// if it returns parent as an empty string then it wasn't possible
func RemoteSplit(remote string) (parent string, leaf string) {
// Split remote on :
i := strings.Index(remote, ":")
remoteName := ""
remotePath := remote
if i >= 0 {
remoteName = remote[:i+1]
remotePath = remote[i+1:]
}
if remotePath == "" {
return "", ""
}
// Construct new remote name without last segment
parent, leaf = path.Split(remotePath)
if leaf == "" {
return "", ""
}
if parent != "/" {
parent = strings.TrimSuffix(parent, "/")
}
parent = remoteName + parent
if parent == "" {
parent = "."
}
return parent, leaf
}
// NewFsSrcDstFiles creates a new src and dst fs from the arguments
// If src is a file then srcFileName and dstFileName will be non-empty
func NewFsSrcDstFiles(args []string) (fsrc fs.Fs, srcFileName string, fdst fs.Fs, dstFileName string) {
fsrc, srcFileName = newFsSrc(args[0])
// If copying a file...
dstRemote := args[1]
if srcFileName != "" {
dstRemote, dstFileName = RemoteSplit(dstRemote)
if dstRemote == "" {
log.Fatalf("Can't find parent directory for %q", args[1])
}
}
fdst = newFsDst(dstRemote)
fs.CalculateModifyWindow(fdst, fsrc)
return
}
// NewFsSrc creates a new src fs from the arguments
func NewFsSrc(args []string) fs.Fs {
fsrc := newFsSrc(args[0])
fsrc, _ := newFsSrc(args[0])
fs.CalculateModifyWindow(fsrc)
return fsrc
}
+28
View File
@@ -0,0 +1,28 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRemoteSplit(t *testing.T) {
for _, test := range []struct {
remote, wantParent, wantLeaf string
}{
{"", "", ""},
{"remote:", "", ""},
{"remote:potato", "remote:", "potato"},
{"remote:potato/sausage", "remote:potato", "sausage"},
{"/", "", ""},
{"/root", "/", "root"},
{"/a/b", "/a", "b"},
{"root", ".", "root"},
{"a/b", "a", "b"},
} {
gotParent, gotLeaf := RemoteSplit(test.remote)
assert.Equal(t, test.wantParent, gotParent, test.remote)
assert.Equal(t, test.wantLeaf, gotLeaf, test.remote)
}
}
+2 -2
View File
@@ -45,12 +45,12 @@ Not to
destpath/sourcepath/one.txt
destpath/sourcepath/two.txt
If you are familiar with ` + "`" + `rsync` + "`" + `, rclone always works as if you had
If you are familiar with ` + "`rsync`" + `, rclone always works as if you had
written a trailing / - meaning "copy the contents of this directory".
This applies to all commands and whether you are talking about the
source or destination.
See the ` + "`" + `--no-traverse` + "`" + ` option for controlling whether rclone lists
See the ` + "`--no-traverse`" + ` option for controlling whether rclone lists
the destination directory or not.
`,
Run: func(command *cobra.Command, args []string) {
+53
View File
@@ -0,0 +1,53 @@
package copyto
import (
"github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/fs"
"github.com/spf13/cobra"
)
func init() {
cmd.Root.AddCommand(commandDefintion)
}
var commandDefintion = &cobra.Command{
Use: "copyto source:path dest:path",
Short: `Copy files from source to dest, skipping already copied`,
Long: `
If source:path is a file or directory then it copies it to a file or
directory named dest:path.
This can be used to upload single files to other than their current
name. If the source is a directory then it acts exactly like the copy
command.
So
rclone copyto src dst
where src and dst are rclone paths, either remote:path or
/path/to/local or C:\windows\path\if\on\windows.
This will:
if src is file
copy it to dst, overwriting an existing file if it exists
if src is directory
copy it to dst, overwriting existing files if they exist
see copy command for full details
This doesn't transfer unchanged files, testing by size and
modification time or MD5SUM. It doesn't delete files from the
destination.
`,
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(2, 2, command, args)
fsrc, srcFileName, fdst, dstFileName := cmd.NewFsSrcDstFiles(args)
cmd.Run(true, command, func() error {
if srcFileName == "" {
return fs.CopyDir(fdst, fsrc)
}
return fs.CopyFile(fdst, fsrc, dstFileName, srcFileName)
})
},
}
+5 -5
View File
@@ -19,14 +19,14 @@ directory. Rclone will error if the source and destination overlap and
the remote does not support a server side directory move operation.
If no filters are in use and if possible this will server side move
` + "`" + `source:path` + "`" + ` into ` + "`" + `dest:path` + "`" + `. After this ` + "`" + `source:path` + "`" + ` will no
` + "`source:path`" + ` into ` + "`dest:path`" + `. After this ` + "`source:path`" + ` will no
longer longer exist.
Otherwise for each file in ` + "`" + `source:path` + "`" + ` selected by the filters (if
any) this will move it into ` + "`" + `dest:path` + "`" + `. If possible a server side
Otherwise for each file in ` + "`source:path`" + ` selected by the filters (if
any) this will move it into ` + "`dest:path`" + `. If possible a server side
move will be used, otherwise it will copy it (server side if possible)
into ` + "`" + `dest:path` + "`" + ` then delete the original (if no errors on copy) in
` + "`" + `source:path` + "`" + `.
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
--dry-run flag.
+57
View File
@@ -0,0 +1,57 @@
package moveto
import (
"github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/fs"
"github.com/spf13/cobra"
)
func init() {
cmd.Root.AddCommand(commandDefintion)
}
var commandDefintion = &cobra.Command{
Use: "moveto source:path dest:path",
Short: `Move file or directory from source to dest.`,
Long: `
If source:path is a file or directory then it moves it to a file or
directory named dest:path.
This can be used to rename files or upload single files to other than
their existing name. If the source is a directory then it acts exacty
like the move command.
So
rclone moveto src dst
where src and dst are rclone paths, either remote:path or
/path/to/local or C:\windows\path\if\on\windows.
This will:
if src is file
move it to dst, overwriting an existing file if it exists
if src is directory
move it to dst, overwriting existing files if they exist
see move command for full details
This doesn't transfer unchanged files, testing by size and
modification time or MD5SUM. src will be deleted on successful
transfer.
**Important**: Since this can cause data loss, test first with the
--dry-run flag.
`,
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(2, 2, command, args)
fsrc, srcFileName, fdst, dstFileName := cmd.NewFsSrcDstFiles(args)
cmd.Run(true, command, func() error {
if srcFileName == "" {
return fs.MoveDir(fdst, fsrc)
}
return fs.MoveFile(fdst, fsrc, dstFileName, srcFileName)
})
},
}