diff --git a/internal/command/rename.go b/internal/command/rename.go index 0780519..5d67901 100644 --- a/internal/command/rename.go +++ b/internal/command/rename.go @@ -15,13 +15,42 @@ package command import ( "fmt" + "github.com/tickstep/aliyunpan-api/aliyunpan" "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" "github.com/tickstep/aliyunpan/internal/config" "github.com/urfave/cli" "path" + "regexp" + "sort" + "strconv" "strings" ) +type ( + fileItem struct { + file *aliyunpan.FileEntity + newFileName string + } + fileArray []*fileItem +) + +func newFileItem(file *aliyunpan.FileEntity) *fileItem { + return &fileItem{ + file: file, + newFileName: "", + } +} + +func (x fileArray) Len() int { + return len(x) +} +func (x fileArray) Less(i, j int) bool { + return x[i].file.FileName < x[j].file.FileName +} +func (x fileArray) Swap(i, j int) { + x[i], x[j] = x[j], x[i] +} + func CmdRename() cli.Command { return cli.Command{ Name: "rename", @@ -30,18 +59,29 @@ func CmdRename() cli.Command { aliyunpan rename <旧文件/目录名> <新文件/目录名>`, Description: ` 示例: + 1. 将文件 1.mp4 重命名为 2.mp4 + aliyunpan rename 1.mp4 2.mp4 - 将文件 1.mp4 重命名为 2.mp4 - aliyunpan rename 1.mp4 2.mp4 + 2. 将文件 /test/1.mp4 重命名为 /test/2.mp4 + 要求必须是同一个文件目录内 + aliyunpan rename /test/1.mp4 /test/2.mp4 - 将文件 /test/1.mp4 重命名为 /test/2.mp4 - 要求必须是同一个文件目录内 - aliyunpan rename /test/1.mp4 /test/2.mp4 + 批量重命名,规则:rename [expression] [replacement] [file] + 其中, + expression - 表达式,即文件名中需要去掉的部分,如果为*代表旧的文件名全部去掉 + replacement - 需要替换的新文件名,如果包含 # 号会按顺序替换成数字编号 + file - 文件匹配模式,指定需要查找的文件匹配字符串,支持通配符,例如:*.mp4 即当前目录下所有的mp4文件 + + 3. 批量重命名,将当前目录下所有.mp4文件中包含的字符串 "我的视频" 全部改成 "视频" + aliyunpan rename 我的视频 视频 *.mp4 + + 4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4... + aliyunpan rename * 视频###.mp4 *.mp4 `, Category: "阿里云盘", Before: ReloadConfigFunc, Action: func(c *cli.Context) error { - if c.NArg() != 2 { + if c.NArg() != 2 && c.NArg() != 3 { cli.ShowCommandHelp(c, c.Command.Name) return nil } @@ -49,7 +89,12 @@ func CmdRename() cli.Command { fmt.Println("未登录账号") return nil } - RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1)) + if c.NArg() == 2 { + RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1)) + } else if c.NArg() == 3 { + // 批量重命名 + RunRenameBatch(parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2)) + } return nil }, Flags: []cli.Flag{ @@ -103,3 +148,119 @@ func RunRename(driveId string, oldName string, newName string) { fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName)) activeUser.DeleteOneCache(path.Dir(newName)) } + +// RunRenameBatch 批量重命名文件 +func RunRenameBatch(driveId string, expression, replacement, filePattern string) { + if len(expression) == 0 { + fmt.Println("旧文件名不能为空") + return + } + if !apiutil.CheckFileNameValid(replacement) { + fmt.Println("新文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars) + return + } + + if len(filePattern) == 0 { + fmt.Println("文件匹配模式不能为空") + return + } + if strings.ContainsAny(filePattern, "/") { + fmt.Println("文件匹配模式不能包含路径分隔符") + return + } + if strings.ContainsAny(filePattern, "\\") { + fmt.Println("文件匹配模式不能包含路径分隔符") + return + } + + activeUser := GetActiveUser() + absolutePath := path.Clean(activeUser.PathJoin(driveId, filePattern)) + fileList, err1 := matchPathByShellPattern(driveId, absolutePath) + if err1 != nil { + fmt.Println("查询文件出错:" + err1.Error()) + return + } + if fileList == nil || len(fileList) == 0 { + fmt.Println("没有找到符合的文件") + return + } + + // 文件列表按照名字排序 + files := fileArray{} + for _, f := range fileList { + files = append(files, newFileItem(f)) + } + if len(files) > 1 { + sort.Sort(files) + } + + // 依次处理 + var index = 1 + for _, file := range files { + replacementStr := replaceNumStr(replacement, index) + index += 1 + if expression == "*" { + // 旧的名称全部替换 + file.newFileName = replacementStr + } else { + // 替换指定的名称片段 + file.newFileName = strings.ReplaceAll(file.file.FileName, expression, replacementStr) + } + } + + // 确认 + fmt.Printf("以下文件将进行对应的重命名\n\n") + idx := 1 + for _, file := range files { + fmt.Printf("%d) %s -> %s\n", idx, file.file.FileName, file.newFileName) + idx += 1 + } + fmt.Printf("\n是否进行批量重命名,该操作不可逆(y/n): ") + confirm := "" + _, err := fmt.Scanln(&confirm) + if err != nil || (confirm != "y" && confirm != "Y") { + fmt.Println("用户取消了操作") + return + } + + // 重命名 + for _, file := range files { + b, e := activeUser.PanClient().FileRename(driveId, file.file.FileId, file.newFileName) + if e != nil { + fmt.Println(e.Err) + return + } + if !b { + fmt.Println("重命名文件失败") + return + } + fmt.Printf("重命名文件成功:%s -> %s\n", file.file.FileName, file.newFileName) + activeUser.DeleteOneCache(path.Dir(file.file.Path)) + } +} + +// replaceNumStr 将#替换成数字编号 +func replaceNumStr(name string, num int) string { + pattern, _ := regexp.Compile("[#]+") + r := pattern.FindAllStringSubmatchIndex(name, -1) + if len(r) == 0 { + return name + } + newName := name + c := r[0] + return replaceNumStr(newName[:c[0]]+generateNumStr(num, c[1]-c[0])+newName[c[1]:], num) +} +func generateNumStr(num, count int) string { + s := strconv.Itoa(num) + return generateZeroStr(count-len(s)) + s +} +func generateZeroStr(count int) string { + if count <= 0 { + return "" + } + s := "" + for i := 0; i < count; i++ { + s += "0" + } + return s +} diff --git a/internal/command/rename_test.go b/internal/command/rename_test.go new file mode 100644 index 0000000..bbe57bc --- /dev/null +++ b/internal/command/rename_test.go @@ -0,0 +1,41 @@ +package command + +import ( + "fmt" + "github.com/tickstep/aliyunpan-api/aliyunpan" + "sort" + "testing" +) + +func TestRenameSort(t *testing.T) { + files := fileArray{} + files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "0我的文件01.txt"})) + files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "4我的文件03.txt"})) + files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "3我的文件02.txt"})) + files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "1我的文件00.txt"})) + + sort.Sort(files) + + for _, f := range files { + fmt.Println(f.file.FileName) + } +} +func TestRenameNum0(t *testing.T) { + fmt.Println(replaceNumStr("我的文件###.txt", 2)) +} +func TestRenameNum1(t *testing.T) { + fmt.Println(replaceNumStr("我的##文件###.txt", 123)) +} +func TestRenameNum2(t *testing.T) { + fmt.Println(replaceNumStr("我的文件###.txt", 1233)) +} +func TestRenameNum3(t *testing.T) { + fmt.Println(replaceNumStr("我的文件[###].txt", 1233)) +} +func TestRenameNum4(t *testing.T) { + fmt.Println(replaceNumStr("我的文件.txt", 1233)) +} + +func TestRenameNum5(t *testing.T) { + fmt.Println(replaceNumStr("", 1233)) +}