diff --git a/README.md b/README.md index 0b3293b..dfd3aed 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ docker run --rm upx upx -v | [get](#get) | 下载一个文件或目录 | | [put](#put) | 上传一个文件或目录 | | [upload](#upload) | 上传多个文件或目录或 http(s) 文件, 支持 Glob 模式过滤上传文件| +| [mv](#mv) | 在同一 bucket 内移动文件| +| [cp](#cp) | 在同一 bucket 内复制文件 | | [rm](#rm) | 删除目录或文件 | | [sync](#sync) | 目录增量同步,类似 rsync | | [auth](#auth) | 生成包含空间名操作员密码信息的 auth 字符串 | @@ -442,6 +444,64 @@ upx rm -d /www upx rm /aaa.png ``` +## mv + +> 在 `bucket` 内部移动文件 + +| args | 说明 | +| --------- | ---- | +| source-file | 需要移动的源文件 | +| dest-file | 需要移动到的目标文件 | + +| options | 说明 | +| --------- | ---- | +| -f | 允许覆盖目标文件 | + +#### 语法 +```bash +upx mv [options] +``` + +#### 示例 +移动文件 +```bash +upx mv /aaa.mp4 /abc/aaa.mp4 +``` + +移动文件,如果目标存在则强制覆盖 +```bash +upx mv -f /aaa.mp4 /abc/aaa.mp4 +``` + +## cp + +> 在 `bucket` 内部拷贝文件 + +| args | 说明 | +| --------- | ---- | +| source-file | 需要复制的源文件 | +| dest-file | 需要复制到的目标文件 | + +| options | 说明 | +| --------- | ---- | +| -f | 允许覆盖目标文件 | + +#### 语法 +```bash +upx mv [options] +``` + +#### 示例 +移动文件 +```bash +upx cp /aaa.mp4 /abc/aaa.mp4 +``` + +复制文件,如果目标存在则强制覆盖 +```bash +upx cp -f /aaa.mp4 /abc/aaa.mp4 +``` + ## sync > sync 本地路径 存储路径 diff --git a/commands.go b/commands.go index acb271c..13cf522 100644 --- a/commands.go +++ b/commands.go @@ -561,3 +561,45 @@ func NewUpgradeCommand() cli.Command { }, } } + +func NewCopyCommand() cli.Command { + return cli.Command{ + Name: "cp", + Usage: "copy files inside cloud storage", + ArgsUsage: "[remote-source-path] [remote-target-path]", + Before: CreateInitCheckFunc(LOGIN, CHECK), + Action: func(c *cli.Context) error { + if c.NArg() != 2 { + PrintErrorAndExit("invalid command args") + } + if err := session.Copy(c.Args()[0], c.Args()[1], c.Bool("f")); err != nil { + PrintErrorAndExit(err.Error()) + } + return nil + }, + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: "Force overwrite existing files"}, + }, + } +} + +func NewMoveCommand() cli.Command { + return cli.Command{ + Name: "mv", + Usage: "move files inside cloud storage", + ArgsUsage: "[remote-source-path] [remote-target-path]", + Before: CreateInitCheckFunc(LOGIN, CHECK), + Action: func(c *cli.Context) error { + if c.NArg() != 2 { + PrintErrorAndExit("invalid command args") + } + if err := session.Move(c.Args()[0], c.Args()[1], c.Bool("f")); err != nil { + PrintErrorAndExit(err.Error()) + } + return nil + }, + Flags: []cli.Flag{ + cli.BoolFlag{Name: "f", Usage: "Force overwrite existing files"}, + }, + } +} diff --git a/copy_test.go b/copy_test.go new file mode 100644 index 0000000..60c3390 --- /dev/null +++ b/copy_test.go @@ -0,0 +1,106 @@ +package upx + +import ( + "fmt" + "io/ioutil" + "path" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCopy(t *testing.T) { + SetUp() + defer TearDown() + + upRootPath := path.Join(ROOT, "copy") + Upx("mkdir", upRootPath) + + localRootPath, err := ioutil.TempDir("", "test") + assert.NoError(t, err) + localRootName := filepath.Base(localRootPath) + + CreateFile(path.Join(localRootPath, "FILE1")) + CreateFile(path.Join(localRootPath, "FILE2")) + + // 上传文件 + _, err = Upx("put", localRootPath, upRootPath) + assert.NoError(t, err) + + files, err := Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 2) + assert.ElementsMatch( + t, + files, + []string{"FILE1", "FILE2"}, + ) + + time.Sleep(time.Second) + + // 正常复制文件 + _, err = Upx( + "cp", + path.Join(upRootPath, localRootName, "FILE1"), + path.Join(upRootPath, localRootName, "FILE3"), + ) + assert.NoError(t, err) + + files, err = Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 3) + assert.ElementsMatch( + t, + files, + []string{"FILE1", "FILE2", "FILE3"}, + ) + + time.Sleep(time.Second) + + // 目标文件已存在 + _, err = Upx( + "cp", + path.Join(upRootPath, localRootName, "FILE1"), + path.Join(upRootPath, localRootName, "FILE2"), + ) + assert.Error(t, err) + assert.Equal( + t, + err.Error(), + fmt.Sprintf( + "target path %s already exists use -f to force overwrite\n", + path.Join(upRootPath, localRootName, "FILE2"), + ), + ) + + files, err = Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 3) + assert.ElementsMatch( + t, + files, + []string{"FILE1", "FILE2", "FILE3"}, + ) + + time.Sleep(time.Second) + + // 目标文件已存在, 强制覆盖 + _, err = Upx( + "cp", + "-f", + path.Join(upRootPath, localRootName, "FILE1"), + path.Join(upRootPath, localRootName, "FILE2"), + ) + assert.NoError(t, err) + + files, err = Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 3) + assert.ElementsMatch( + t, + files, + []string{"FILE1", "FILE2", "FILE3"}, + ) +} diff --git a/move_test.go b/move_test.go new file mode 100644 index 0000000..1ef0517 --- /dev/null +++ b/move_test.go @@ -0,0 +1,104 @@ +package upx + +import ( + "fmt" + "io/ioutil" + "path" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMove(t *testing.T) { + SetUp() + defer TearDown() + + upRootPath := path.Join(ROOT, "move") + Upx("mkdir", upRootPath) + + localRootPath, err := ioutil.TempDir("", "test") + assert.NoError(t, err) + localRootName := filepath.Base(localRootPath) + + CreateFile(path.Join(localRootPath, "FILE1")) + CreateFile(path.Join(localRootPath, "FILE2")) + + // 上传文件 + Upx("put", localRootPath, upRootPath) + files, err := Ls(path.Join(upRootPath, localRootName)) + + assert.NoError(t, err) + assert.Len(t, files, 2) + assert.ElementsMatch( + t, + files, + []string{"FILE1", "FILE2"}, + ) + + time.Sleep(time.Second) + + // 正常移动文件 + _, err = Upx( + "mv", + path.Join(upRootPath, localRootName, "FILE1"), + path.Join(upRootPath, localRootName, "FILE3"), + ) + assert.NoError(t, err) + + files, err = Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 2) + assert.ElementsMatch( + t, + files, + []string{"FILE2", "FILE3"}, + ) + + time.Sleep(time.Second) + + // 目标文件已存在 + _, err = Upx( + "mv", + path.Join(upRootPath, localRootName, "FILE2"), + path.Join(upRootPath, localRootName, "FILE3"), + ) + assert.Equal( + t, + err.Error(), + fmt.Sprintf( + "target path %s already exists use -f to force overwrite\n", + path.Join(upRootPath, localRootName, "FILE3"), + ), + ) + + files, err = Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 2) + assert.ElementsMatch( + t, + files, + []string{"FILE2", "FILE3"}, + ) + + time.Sleep(time.Second) + + // 目标文件已存在, 强制覆盖 + _, err = Upx( + "mv", + "-f", + path.Join(upRootPath, localRootName, "FILE2"), + path.Join(upRootPath, localRootName, "FILE3"), + ) + assert.NoError(t, err) + + files, err = Ls(path.Join(upRootPath, localRootName)) + assert.NoError(t, err) + assert.Len(t, files, 1) + assert.ElementsMatch( + t, + files, + []string{"FILE3"}, + ) +} diff --git a/putiginore_test.go b/putiginore_test.go index 512c4c1..b071a0f 100644 --- a/putiginore_test.go +++ b/putiginore_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -54,6 +55,8 @@ func TestPutIgnore(t *testing.T) { []string{"FILE1", "FILE2"}, ) + time.Sleep(time.Second) + // 上传隐藏的文件夹, 无all,上传失效 Upx( "put", @@ -70,6 +73,8 @@ func TestPutIgnore(t *testing.T) { []string{"FILE1", "FILE2"}, ) + time.Sleep(time.Second) + // 上传隐藏的文件夹, 有all,上传成功 Upx( "put", @@ -86,6 +91,8 @@ func TestPutIgnore(t *testing.T) { []string{"FILE1", "FILE2", ".FILES"}, ) + time.Sleep(time.Second) + // 上传所有文件 Upx("put", "-all", localRootPath, upRootPath) files, err = Ls(path.Join(upRootPath, localRootName)) diff --git a/session.go b/session.go index bb64dfd..411618c 100644 --- a/session.go +++ b/session.go @@ -1230,3 +1230,93 @@ func (sess *Session) Purge(urls []string, file string) { PrintErrorAndExit("purge error: %v", err) } } + +func (sess *Session) Copy(srcPath, destPath string, force bool) error { + return sess.copyMove(srcPath, destPath, "copy", force) +} + +func (sess *Session) Move(srcPath, destPath string, force bool) error { + return sess.copyMove(srcPath, destPath, "move", force) +} + +// 移动或者复制 +// method: "move" | "copy" +// force: 是否覆盖目标文件 +func (sess *Session) copyMove(srcPath, destPath, method string, force bool) error { + // 将源文件路径转化为绝对路径 + srcPath = sess.AbsPath(srcPath) + + // 检测源文件 + sourceFileInfo, err := sess.updriver.GetInfo(srcPath) + if err != nil { + if upyun.IsNotExist(err) { + return fmt.Errorf("source file %s is not exist", srcPath) + } + return err + } + if sourceFileInfo.IsDir { + return fmt.Errorf("not support copy dir, %s is dir", srcPath) + } + + // 将目标路径转化为绝对路径 + destPath = sess.AbsPath(destPath) + + destFileInfo, err := sess.updriver.GetInfo(destPath) + // 如果返回的错误不是文件不存在错误,则返回错误 + if err != nil && !upyun.IsNotExist(err) { + return err + } + // 如果没有错误,表示文件存在,则检测文件类型,并判断是否允许覆盖 + if err == nil { + if !destFileInfo.IsDir { + // 如果目标文件是文件类型,则需要使用强制覆盖 + if !force { + return fmt.Errorf( + "target path %s already exists use -f to force overwrite", + destPath, + ) + } + } else { + // 补全文件名后,再次检测文件存不存在 + destPath = path.Join(destPath, path.Base(srcPath)) + destFileInfo, err := sess.updriver.GetInfo(destPath) + if err == nil { + if destFileInfo.IsDir { + return fmt.Errorf( + "target file %s already exists and is dir", + destPath, + ) + } + if !force { + return fmt.Errorf( + "target file %s already exists use -f to force overwrite", + destPath, + ) + } + } + } + } + + if srcPath == destPath { + return fmt.Errorf( + "source and target are the same %s => %s", + srcPath, + destPath, + ) + } + + switch method { + case "copy": + return sess.updriver.Copy(&upyun.CopyObjectConfig{ + SrcPath: srcPath, + DestPath: destPath, + }) + case "move": + return sess.updriver.Move(&upyun.MoveObjectConfig{ + SrcPath: srcPath, + DestPath: destPath, + }) + default: + return fmt.Errorf("not support method") + } +} diff --git a/upx.go b/upx.go index bf3b177..6fb43ee 100644 --- a/upx.go +++ b/upx.go @@ -58,6 +58,8 @@ func CreateUpxApp() *cli.App { NewGetDBCommand(), NewCleanDBCommand(), NewUpgradeCommand(), + NewCopyCommand(), + NewMoveCommand(), } return app }