diff --git a/cmd/rename/rename.go b/cmd/rename/rename.go new file mode 100644 index 0000000..95d7b0a --- /dev/null +++ b/cmd/rename/rename.go @@ -0,0 +1,43 @@ +package rename + +import ( + "fmt" + "path/filepath" + + "github.com/52funny/pikpakcli/conf" + "github.com/52funny/pikpakcli/internal/pikpak" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var RenameCmd = &cobra.Command{ + Use: "rename ", + Short: "Rename a file or folder on the PikPak drive", + Long: `Rename a file or folder on the PikPak drive. +Example: pikpakcli rename /my-folder/old-name.txt new-name.txt`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password) + if err := p.Login(); err != nil { + logrus.Errorln("Login Failed:", err) + return err + } + + oldPath := args[0] + newName := filepath.Base(args[1]) + + fileStat, err := p.GetFileByPath(oldPath) + if err != nil { + logrus.Errorf("Could not find file or folder at path '%s': %v", oldPath, err) + return err + } + + if err := p.Rename(fileStat.ID, newName); err != nil { + logrus.Errorf("Failed to rename file: %v", err) + return err + } + + fmt.Printf("Successfully renamed '%s' to '%s'\n", oldPath, newName) + return nil + }, +} diff --git a/cmd/root.go b/cmd/root.go index fca8214..9bbb196 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "github.com/52funny/pikpakcli/cmd/list" "github.com/52funny/pikpakcli/cmd/new" "github.com/52funny/pikpakcli/cmd/quota" + "github.com/52funny/pikpakcli/cmd/rename" "github.com/52funny/pikpakcli/cmd/share" "github.com/52funny/pikpakcli/cmd/upload" @@ -53,6 +54,7 @@ func init() { rootCmd.AddCommand(embed.EmbedCmd) rootCmd.AddCommand(quota.QuotaCmd) rootCmd.AddCommand(list.ListCmd) + rootCmd.AddCommand(rename.RenameCmd) } // Execute the command line interface diff --git a/internal/pikpak/file.go b/internal/pikpak/file.go index d5d0c3d..8d168ca 100644 --- a/internal/pikpak/file.go +++ b/internal/pikpak/file.go @@ -1,10 +1,12 @@ package pikpak import ( + "bytes" "encoding/json" "errors" "net/http" "net/url" + "strings" "time" "unsafe" @@ -141,6 +143,34 @@ func (p *PikPak) GetFileStat(parentId string, name string) (FileStat, error) { return FileStat{}, errors.New("file not found") } +func (p *PikPak) GetFileByPath(path string) (FileStat, error) { + if path == "/" || path == "" { + // Cannot rename root + return FileStat{}, errors.New("cannot get info of root directory") + } + // Normalize path + if path[0] == '/' { + path = path[1:] + } + parts := strings.Split(path, "/") + currentParentId := "" + var currentFileStat FileStat + var err error + + for i, part := range parts { + currentFileStat, err = p.GetFileStat(currentParentId, part) + if err != nil { + return FileStat{}, err + } + currentParentId = currentFileStat.ID + if i == len(parts)-1 { + return currentFileStat, nil + } + } + return FileStat{}, errors.New("file not found") +} + + func (p *PikPak) GetFile(fileId string) (File, error) { var fileInfo File query := url.Values{} @@ -173,3 +203,46 @@ func (p *PikPak) GetFile(fileId string) (File, error) { } return fileInfo, err } + +func (p *PikPak) Rename(fileId string, newName string) error { + apiUrl := "https://api-drive.mypikpak.com/drive/v1/files/" + fileId + body := map[string]string{"name": newName} + jsonBody, err := json.Marshal(body) + if err != nil { + return err + } + + // Allow one retry after captcha refresh + for i := 0; i < 2; i++ { + req, err := http.NewRequest("PATCH", apiUrl, bytes.NewBuffer(jsonBody)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Captcha-Token", p.CaptchaToken) + req.Header.Set("X-Device-Id", p.DeviceId) + + bs, err := p.sendRequest(req) + if err != nil { + return err + } + + errorCode := gjson.Get(*(*string)(unsafe.Pointer(&bs)), "error_code").Int() + if errorCode == 0 { + return nil // Success + } + + if errorCode == 9 { + err = p.AuthCaptchaToken("PATCH:/drive/v1/files") + if err != nil { + return err // Failed to refresh token + } + // if token refreshed successfully, loop will retry the request + continue + } + return errors.New(gjson.Get(*(*string)(unsafe.Pointer(&bs)), "error").String()) + } + return errors.New("failed to rename file after captcha refresh") +} +