├── config_example.yml
├── images
└── build.gif
├── .gitignore
├── main.go
├── docs
├── config.md
├── command_zhCN.md
├── command_docker.md
└── command.md
├── internal
├── utils
│ ├── str.go
│ ├── embed.go
│ ├── embed_test.go
│ ├── sync.go
│ └── path.go
└── pikpak
│ ├── quota.go
│ ├── refresh_token.go
│ ├── url.go
│ ├── sha.go
│ ├── download.go
│ ├── captcha_token.go
│ ├── pikpak.go
│ ├── folder.go
│ ├── file.go
│ └── upload.go
├── Dockerfile
├── cmd
├── new
│ ├── new.go
│ ├── folder
│ │ └── folder.go
│ ├── sha
│ │ └── sha.go
│ └── url
│ │ └── url.go
├── quota
│ └── quota.go
├── root.go
├── share
│ └── share.go
├── list
│ └── list.go
├── embed
│ └── embed.go
├── upload
│ └── upload.go
└── download
│ └── download.go
├── .github
└── workflows
│ ├── goreleaser.yml
│ └── dockerhub.yml
├── LICENSE
├── go.mod
├── README_zhCN.md
├── .goreleaser.yaml
├── README.md
├── conf
└── config.go
└── go.sum
/config_example.yml:
--------------------------------------------------------------------------------
1 | proxy:
2 | username: xxx
3 | password: xxx
4 |
--------------------------------------------------------------------------------
/images/build.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/52funny/pikpakcli/HEAD/images/build.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .vscode
3 | .pikpaksync.txt
4 | config.yml
5 | pikpakcli
6 | dist
7 | dist/
8 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/52funny/pikpakcli/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/docs/config.md:
--------------------------------------------------------------------------------
1 | ## Configuration
2 |
3 | ### Using Proxy
4 |
5 | Configuring the `proxy` field in `config.yml`
6 |
7 | ```yml
8 | proxy: http://host:port
9 | ```
10 |
11 | > ⚠️⚠️⚠️ proxy must contain `://`.
12 |
--------------------------------------------------------------------------------
/internal/utils/str.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "unsafe"
5 | )
6 |
7 | func StringToByteSlice(str string) []byte {
8 | return unsafe.Slice(unsafe.StringData(str), len(str))
9 | }
10 |
11 | func ByteSliceToString(bs []byte) string {
12 | return *(*string)(unsafe.Pointer(&bs))
13 | }
14 |
--------------------------------------------------------------------------------
/internal/utils/embed.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "path/filepath"
6 | )
7 |
8 | func GetEmbedBinName(name string) string {
9 | if len(name) == 0 {
10 | return "_embed"
11 | }
12 | base := filepath.Base(name)
13 | ext := filepath.Ext(base)
14 | fmt.Println(base, ext)
15 | if len(ext) > 0 {
16 | base = base[:len(base)-len(ext)]
17 | }
18 | return base + "_embed" + ext
19 | }
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.21-alpine AS builder
2 |
3 | RUN apk add --no-cache git
4 | WORKDIR /src
5 |
6 | COPY go.mod go.sum ./
7 | RUN go mod download
8 |
9 | COPY . .
10 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
11 | go build -ldflags "-s -w" -o /usr/local/bin/pikpakcli ./main.go
12 |
13 | FROM alpine:3.18
14 | RUN apk add --no-cache ca-certificates
15 | COPY --from=builder /usr/local/bin/pikpakcli /usr/local/bin/pikpakcli
16 | WORKDIR /root
17 |
18 | ENTRYPOINT ["/usr/local/bin/pikpakcli"]
19 |
20 |
--------------------------------------------------------------------------------
/internal/utils/embed_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetEmbedBinName(t *testing.T) {
10 | exe := "pikpakcli.exe"
11 | exe2 := ".exe"
12 | assert.Equal(t, "pikpakcli_embed.exe", GetEmbedBinName(exe))
13 | assert.Equal(t, "_embed.exe", GetEmbedBinName(exe2))
14 |
15 | bin := "pikpakcli"
16 | bin2 := ""
17 | assert.Equal(t, "pikpakcli_embed", GetEmbedBinName(bin))
18 | assert.Equal(t, "_embed", GetEmbedBinName(bin2))
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/cmd/new/new.go:
--------------------------------------------------------------------------------
1 | package new
2 |
3 | import (
4 | "github.com/52funny/pikpakcli/cmd/new/folder"
5 | "github.com/52funny/pikpakcli/cmd/new/sha"
6 | "github.com/52funny/pikpakcli/cmd/new/url"
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var NewCommand = &cobra.Command{
11 | Use: "new",
12 | Aliases: []string{"n"},
13 | Short: `New can do something like create folder or other things`,
14 | Run: func(cmd *cobra.Command, args []string) {
15 | cmd.Help()
16 | },
17 | }
18 |
19 | func init() {
20 | NewCommand.AddCommand(folder.NewFolderCommand)
21 | NewCommand.AddCommand(sha.NewShaCommand)
22 | NewCommand.AddCommand(url.NewUrlCommand)
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/goreleaser.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | - name: Run GoReleaser
22 | uses: goreleaser/goreleaser-action@v6
23 | with:
24 | distribution: goreleaser
25 | args: release --clean
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/dockerhub.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker image
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout source
11 | uses: actions/checkout@v2
12 |
13 | - name: Docker meta
14 | id: docker_meta
15 | uses: docker/metadata-action@v5
16 | with:
17 | images: 52funny/pikpakcli
18 |
19 | - name: Set up QEMU
20 | uses: docker/setup-qemu-action@v3
21 |
22 | - name: Set up Docker Buildx
23 | uses: docker/setup-buildx-action@v3
24 |
25 | - name: Login DockerHub
26 | uses: docker/login-action@v3
27 | with:
28 | username: ${{ secrets.DOCKER_USERNAME }}
29 | password: ${{ secrets.DOCKER_PASSWORD }}
30 |
31 | - name: Push Docker Hub
32 | uses: docker/build-push-action@v6
33 | with:
34 | push: true
35 | context: .
36 | platforms: linux/amd64,linux/arm64
37 | file: ./Dockerfile
38 | tags: ${{ steps.docker_meta.outputs.tags }}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 52funny
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/internal/pikpak/quota.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "net/http"
5 |
6 | jsoniter "github.com/json-iterator/go"
7 | )
8 |
9 | type quotaMessage struct {
10 | Kind string `json:"kind"`
11 | Quota Quota `json:"quota"`
12 | ExpiresAt string `json:"expires_at"`
13 | Quotas Quotas `json:"quotas"`
14 | }
15 | type Quota struct {
16 | Kind string `json:"kind"`
17 | Limit string `json:"limit"`
18 | Usage string `json:"usage"`
19 | UsageInTrash string `json:"usage_in_trash"`
20 | PlayTimesLimit string `json:"play_times_limit"`
21 | PlayTimesUsage string `json:"play_times_usage"`
22 | }
23 |
24 | type Quotas struct {
25 | }
26 |
27 | // get cloud quota
28 | func (p *PikPak) GetQuota() (Quota, error) {
29 | req, err := http.NewRequest("GET", "https://api-drive.mypikpak.com/drive/v1/about", nil)
30 | if err != nil {
31 | return Quota{}, err
32 | }
33 | bs, err := p.sendRequest(req)
34 | if err != nil {
35 | return Quota{}, err
36 | }
37 | var quotaMessage quotaMessage
38 | err = jsoniter.Unmarshal(bs, "aMessage)
39 | if err != nil {
40 | return Quota{}, err
41 | }
42 | return quotaMessage.Quota, nil
43 | }
44 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/52funny/pikpakcli
2 |
3 | go 1.21.3
4 |
5 | require (
6 | github.com/52funny/pikpakhash v0.0.0-20231104025731-ef91a56eff9c
7 | github.com/fatih/color v1.15.0
8 | github.com/json-iterator/go v1.1.12
9 | github.com/sirupsen/logrus v1.9.0
10 | github.com/spf13/cobra v1.6.1
11 | github.com/stretchr/testify v1.8.4
12 | github.com/tidwall/gjson v1.14.4
13 | github.com/vbauerster/mpb/v8 v8.7.2
14 | gopkg.in/yaml.v2 v2.4.0
15 | )
16 |
17 | require (
18 | github.com/VividCortex/ewma v1.2.0 // indirect
19 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
20 | github.com/davecgh/go-spew v1.1.1 // indirect
21 | github.com/inconshreveable/mousetrap v1.0.1 // indirect
22 | github.com/mattn/go-colorable v0.1.13 // indirect
23 | github.com/mattn/go-isatty v0.0.17 // indirect
24 | github.com/mattn/go-runewidth v0.0.15 // indirect
25 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
26 | github.com/modern-go/reflect2 v1.0.2 // indirect
27 | github.com/pmezard/go-difflib v1.0.0 // indirect
28 | github.com/rivo/uniseg v0.4.4 // indirect
29 | github.com/spf13/pflag v1.0.5 // indirect
30 | github.com/tidwall/match v1.1.1 // indirect
31 | github.com/tidwall/pretty v1.2.0 // indirect
32 | golang.org/x/sys v0.16.0 // indirect
33 | gopkg.in/yaml.v3 v3.0.1 // indirect
34 | )
35 |
--------------------------------------------------------------------------------
/README_zhCN.md:
--------------------------------------------------------------------------------
1 | # PikPak CLI
2 |
3 | 
4 | 
5 |
6 | PikPakCli 是 PikPak 的命令行工具。
7 |
8 | 
9 |
10 | ## 安装方法
11 |
12 | ### 从源码编译
13 |
14 | 要从源代码构建该工具,请确保您的系统已安装 [Go](https://go.dev/doc/install) 环境。
15 |
16 | 克隆项目
17 |
18 | ```bash
19 | git clone https://github.com/52funny/pikpakcli
20 | ```
21 |
22 | 生成可执行文件
23 |
24 | ```bash
25 | go build
26 | ```
27 |
28 | 运行
29 |
30 | ```bash
31 | ./pikpakcli
32 | ```
33 |
34 | ### 从 Release 下载
35 |
36 | 从 Release 下载你所需要的版本,然后运行。
37 |
38 | ## 配置文件
39 |
40 | 首先将项目中的 `config_example.yml` 配置一下,输入自己的账号密码
41 |
42 | 如果账号是手机号,手机号要以区号开头。如 `+861xxxxxxxxxx`
43 |
44 | 然后将其重命名为 `config.yml`
45 |
46 | 配置文件将会优先从当前目录进行读取 `config.yml`,如果当前目录下不存在 `config.yml` 将会从用户的配置数据的默认根目录进行读取,各个平台的默认根目录如下:
47 |
48 | - Linux: `$HOME/.config/pikpakcli`
49 | - Darwin: `$HOME/Library/Application Support/pikpakcli`
50 | - Windows: `%AppData%/pikpakcli`
51 |
52 | ## 开始
53 |
54 | 之后你就可以运行 `ls` 指令来查看存储在 **PikPak** 上的文件了
55 |
56 | ```bash
57 | ./pikpakcli ls
58 | ```
59 |
60 | ## 用法
61 |
62 | 参阅 [Command](docs/command_zhCN.md) 查看更多的指令
63 |
64 | ## 贡献者
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 |
4 | # The lines below are called `modelines`. See `:help modeline`
5 | # Feel free to remove those if you don't want/need to use them.
6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj
8 |
9 | version: 2
10 |
11 | before:
12 | hooks:
13 | # You may remove this if you don't use go modules.
14 | - go mod tidy
15 | # you may remove this if you don't need go generate
16 | - go generate ./...
17 |
18 | builds:
19 | - env:
20 | - CGO_ENABLED=0
21 | goos:
22 | - linux
23 | - windows
24 | - darwin
25 |
26 | archives:
27 | - format: tar.gz
28 | # this name template makes the OS and Arch compatible with the results of `uname`.
29 | name_template: >-
30 | {{ .ProjectName }}_
31 | {{- title .Os }}_
32 | {{- if eq .Arch "amd64" }}x86_64
33 | {{- else if eq .Arch "386" }}i386
34 | {{- else }}{{ .Arch }}{{ end }}
35 | {{- if .Arm }}v{{ .Arm }}{{ end }}
36 | # use zip for windows archives
37 | format_overrides:
38 | - goos: windows
39 | format: zip
40 | files:
41 | - config_example.yml
42 |
43 | changelog:
44 | sort: asc
45 | filters:
46 | exclude:
47 | - "^docs:"
48 | - "^test:"
49 |
--------------------------------------------------------------------------------
/internal/pikpak/refresh_token.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 |
8 | jsoniter "github.com/json-iterator/go"
9 | "github.com/sirupsen/logrus"
10 | "github.com/tidwall/gjson"
11 | )
12 |
13 | func (p *PikPak) RefreshToken() error {
14 | url := "https://user.mypikpak.com/v1/auth/token"
15 | m := map[string]string{
16 | "client_id": clientID,
17 | "client_secret": clientSecret,
18 | "grant_type": "refresh_token",
19 | "refresh_token": p.refreshToken,
20 | }
21 | bs, err := jsoniter.Marshal(&m)
22 | if err != nil {
23 | return err
24 | }
25 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(bs))
26 | if err != nil {
27 | return err
28 | }
29 | bs, err = p.sendRequest(req)
30 | if err != nil {
31 | return err
32 | }
33 | error_code := gjson.GetBytes(bs, "error_code").Int()
34 | if error_code != 0 {
35 | // refresh token failed
36 | if error_code == 4126 {
37 | // 重新登录
38 | return p.Login()
39 | }
40 | return fmt.Errorf("refresh token error message: %d", gjson.GetBytes(bs, "error").Int())
41 | }
42 | // logrus.Debug("refresh: ", string(bs))
43 | p.JwtToken = gjson.GetBytes(bs, "access_token").String()
44 | p.refreshToken = gjson.GetBytes(bs, "refresh_token").String()
45 | p.RefreshSecond = gjson.GetBytes(bs, "expires_in").Int()
46 | logrus.Debugf("RefreshToken access_token: %s refresh_token: %s\n", p.JwtToken, p.refreshToken)
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/cmd/new/folder/folder.go:
--------------------------------------------------------------------------------
1 | package folder
2 |
3 | import (
4 | "github.com/52funny/pikpakcli/conf"
5 | "github.com/52funny/pikpakcli/internal/pikpak"
6 | "github.com/sirupsen/logrus"
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var NewFolderCommand = &cobra.Command{
11 | Use: "folder",
12 | Short: `Create a folder to pikpak server`,
13 | Run: func(cmd *cobra.Command, args []string) {
14 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
15 | err := p.Login()
16 | if err != nil {
17 | logrus.Errorln("Login Failed:", err)
18 | }
19 | if len(args) > 0 {
20 | handleNewFolder(&p, args)
21 | } else {
22 | logrus.Errorln("Please input the folder name")
23 | }
24 | },
25 | }
26 |
27 | var path string
28 | var parentId string
29 |
30 | func init() {
31 | NewFolderCommand.Flags().StringVarP(&path, "path", "p", "/", "The path of the folder")
32 | NewFolderCommand.Flags().StringVarP(&parentId, "parent-id", "P", "", "The parent id")
33 | }
34 |
35 | // new folder
36 | func handleNewFolder(p *pikpak.PikPak, folders []string) {
37 | var err error
38 | if parentId == "" {
39 | parentId, err = p.GetPathFolderId(path)
40 | if err != nil {
41 | logrus.Errorf("Get parent id failed: %s\n", err)
42 | return
43 | }
44 | }
45 |
46 | for _, folder := range folders {
47 | _, err := p.CreateFolder(parentId, folder)
48 | if err != nil {
49 | logrus.Errorf("Create folder %s failed: %s\n", folder, err)
50 | } else {
51 | logrus.Infof("Create folder %s success\n", folder)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/quota/quota.go:
--------------------------------------------------------------------------------
1 | package quota
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 |
7 | "github.com/52funny/pikpakcli/conf"
8 | "github.com/52funny/pikpakcli/internal/pikpak"
9 | "github.com/sirupsen/logrus"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var human bool
14 |
15 | var QuotaCmd = &cobra.Command{
16 | Use: "quota",
17 | Short: `Get the quota for the pikpak cloud`,
18 | Run: func(cmd *cobra.Command, args []string) {
19 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
20 | err := p.Login()
21 | if err != nil {
22 | logrus.Errorln("Login Failed:", err)
23 | }
24 | q, err := p.GetQuota()
25 | if err != nil {
26 | logrus.Errorln("get cloud quota error:", err)
27 | return
28 | }
29 | fmt.Printf("%-20s%-20s\n", "total", "used")
30 | switch human {
31 | case true:
32 | fmt.Printf("%-20s%-20s\n", displayStorage(q.Limit), displayStorage(q.Usage))
33 | case false:
34 | fmt.Printf("%-20s%-20s\n", q.Limit, q.Usage)
35 | }
36 | },
37 | }
38 |
39 | func init() {
40 | QuotaCmd.Flags().BoolVarP(&human, "human", "H", false, "display human readable format")
41 | }
42 | func displayStorage(s string) string {
43 | size, _ := strconv.ParseFloat(s, 64)
44 | cnt := 0
45 | for size > 1024 {
46 | cnt += 1
47 | if cnt > 5 {
48 | break
49 | }
50 | size /= 1024
51 | }
52 | // res := strconv.Itoa(int(size))
53 | res := strconv.FormatFloat(size, 'g', 2, 64)
54 | switch cnt {
55 | case 0:
56 | res += "B"
57 | case 1:
58 | res += "KB"
59 | case 2:
60 | res += "MB"
61 | case 3:
62 | res += "GB"
63 | case 4:
64 | res += "TB"
65 | case 5:
66 | res += "PB"
67 | }
68 | return res
69 | }
70 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/52funny/pikpakcli/cmd/download"
7 | "github.com/52funny/pikpakcli/cmd/embed"
8 | "github.com/52funny/pikpakcli/cmd/list"
9 | "github.com/52funny/pikpakcli/cmd/new"
10 | "github.com/52funny/pikpakcli/cmd/quota"
11 | "github.com/52funny/pikpakcli/cmd/share"
12 | "github.com/52funny/pikpakcli/cmd/upload"
13 |
14 | "github.com/52funny/pikpakcli/conf"
15 | "github.com/sirupsen/logrus"
16 | "github.com/spf13/cobra"
17 | )
18 |
19 | var rootCmd = &cobra.Command{
20 | Use: "pikpakcli",
21 | Short: "Pikpakcli is a command line interface for Pikpak",
22 | Run: func(cmd *cobra.Command, args []string) {
23 | cmd.Help()
24 | },
25 | PersistentPreRun: func(cmd *cobra.Command, args []string) {
26 | err := conf.InitConfig(configPath)
27 | if err != nil {
28 | logrus.Errorln(err)
29 | os.Exit(1)
30 | }
31 | if debug {
32 | logrus.SetLevel(logrus.DebugLevel)
33 | }
34 | },
35 | }
36 |
37 | // Config path
38 | var configPath string
39 |
40 | // Debug mode
41 | var debug bool
42 |
43 | // Initialize the command line interface
44 | func init() {
45 | // debug
46 | rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "debug mode")
47 | // config
48 | rootCmd.PersistentFlags().StringVar(&configPath, "config", "config.yml", "config file path")
49 | rootCmd.AddCommand(upload.UploadCmd)
50 | rootCmd.AddCommand(download.DownloadCmd)
51 | rootCmd.AddCommand(share.ShareCommand)
52 | rootCmd.AddCommand(new.NewCommand)
53 | rootCmd.AddCommand(embed.EmbedCmd)
54 | rootCmd.AddCommand(quota.QuotaCmd)
55 | rootCmd.AddCommand(list.ListCmd)
56 | }
57 |
58 | // Execute the command line interface
59 | func Execute() {
60 | if err := rootCmd.Execute(); err != nil {
61 | logrus.Errorln(err)
62 | os.Exit(1)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/internal/pikpak/url.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 |
8 | jsoniter "github.com/json-iterator/go"
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | func (p *PikPak) CreateUrlFile(parentId, url string) error {
13 | m := map[string]interface{}{
14 | "kind": "drive#file",
15 | "upload_type": "UPLOAD_TYPE_URL",
16 | "url": map[string]string{
17 | "url": url,
18 | },
19 | }
20 | if parentId != "" {
21 | m["parent_id"] = parentId
22 | }
23 | bs, err := jsoniter.Marshal(&m)
24 | if err != nil {
25 | return err
26 | }
27 | START:
28 | req, err := http.NewRequest("POST", "https://api-drive.mypikpak.com/drive/v1/files", bytes.NewBuffer(bs))
29 | if err != nil {
30 | return err
31 | }
32 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
33 | req.Header.Set("Product_flavor_name", "cha")
34 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
35 | req.Header.Set("X-Client-Version-Code", "10083")
36 | req.Header.Set("X-Peer-Id", p.DeviceId)
37 | req.Header.Set("X-User-Region", "1")
38 | req.Header.Set("X-Alt-Capability", "3")
39 | req.Header.Set("Country", "CN")
40 | bs, err = p.sendRequest(req)
41 | if err != nil {
42 | return err
43 | }
44 | error_code := jsoniter.Get(bs, "error_code").ToInt()
45 | if error_code != 0 {
46 | if error_code == 9 {
47 | err := p.AuthCaptchaToken("POST:/drive/v1/files")
48 | if err != nil {
49 | return err
50 | }
51 | goto START
52 | }
53 | return fmt.Errorf("upload file error: %s", jsoniter.Get(bs, "error").ToString())
54 | }
55 |
56 | task := jsoniter.Get(bs, "task")
57 | logrus.Debug(task.ToString())
58 | // phase := task.Get("phase").ToString()
59 | // if phase == "PHASE_TYPE_COMPLETE" {
60 | // return nil
61 | // } else {
62 | // return fmt.Errorf("create file error: %s", phase)
63 | // }
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/internal/pikpak/sha.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 |
8 | jsoniter "github.com/json-iterator/go"
9 | )
10 |
11 | func (p *PikPak) CreateShaFile(parentId, fileName, size, sha string) error {
12 | m := map[string]interface{}{
13 | "body": map[string]string{
14 | "duration": "",
15 | "width": "",
16 | "height": "",
17 | },
18 | "kind": "drive#file",
19 | "name": fileName,
20 | "size": size,
21 | "hash": sha,
22 | "upload_type": "UPLOAD_TYPE_RESUMABLE",
23 | "objProvider": map[string]string{
24 | "provider": "UPLOAD_TYPE_UNKNOWN",
25 | },
26 | }
27 | if parentId != "" {
28 | m["parent_id"] = parentId
29 | }
30 | bs, err := jsoniter.Marshal(&m)
31 | if err != nil {
32 | return err
33 | }
34 | START:
35 | req, err := http.NewRequest("POST", "https://api-drive.mypikpak.com/drive/v1/files", bytes.NewBuffer(bs))
36 | if err != nil {
37 | return err
38 | }
39 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
40 | req.Header.Set("Product_flavor_name", "cha")
41 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
42 | req.Header.Set("X-Client-Version-Code", "10083")
43 | req.Header.Set("X-Peer-Id", p.DeviceId)
44 | req.Header.Set("X-User-Region", "1")
45 | req.Header.Set("X-Alt-Capability", "3")
46 | req.Header.Set("Country", "CN")
47 | bs, err = p.sendRequest(req)
48 | if err != nil {
49 | return err
50 | }
51 | error_code := jsoniter.Get(bs, "error_code").ToInt()
52 | if error_code != 0 {
53 | if error_code == 9 {
54 | err := p.AuthCaptchaToken("POST:/drive/v1/files")
55 | if err != nil {
56 | return err
57 | }
58 | goto START
59 | }
60 | return fmt.Errorf("upload file error: %s", jsoniter.Get(bs, "error").ToString())
61 | }
62 | file := jsoniter.Get(bs, "file")
63 | phase := file.Get("phase").ToString()
64 | if phase == "PHASE_TYPE_COMPLETE" {
65 | return nil
66 | } else {
67 | return fmt.Errorf("create file error: %s", phase)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/internal/pikpak/download.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "os"
9 | "strconv"
10 |
11 | "github.com/sirupsen/logrus"
12 | "github.com/vbauerster/mpb/v8"
13 | )
14 |
15 | // Download file
16 | func (f *File) Download(path string, bar *mpb.Bar) error {
17 | outFile, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
18 | if err != nil {
19 | return err
20 | }
21 | defer outFile.Close()
22 | info, err := outFile.Stat()
23 | if err != nil {
24 | return err
25 | }
26 | size := info.Size()
27 | resume := size != 0
28 | req, err := http.NewRequest("GET", f.Links.ApplicationOctetStream.URL, nil)
29 | if err != nil {
30 | return err
31 | }
32 | req.Header.Set("User-Agent", userAgent)
33 | if resume {
34 | if bar != nil {
35 | bar.SetCurrent(size)
36 | }
37 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-", size))
38 | }
39 | resp, err := http.DefaultClient.Do(req)
40 | if err != nil {
41 | return err
42 | }
43 | // defer resp.Body.Close()
44 | if resume && resp.StatusCode != http.StatusPartialContent {
45 | logrus.Warnf("Resume file %s failed: Server doesn't support, restarting from the beginning", f.Name)
46 | // try re-opening the file with contents truncated
47 | outFile.Close()
48 | outFile, err = os.Create(path)
49 | if err != nil {
50 | return err
51 | }
52 | }
53 | contentLength, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
54 | if err != nil {
55 | return errors.New("transmute content length to int64 failed")
56 | }
57 | var proxyReader io.ReadCloser
58 | if bar != nil {
59 | proxyReader = bar.ProxyReader(resp.Body)
60 | } else {
61 | proxyReader = resp.Body
62 | }
63 | defer proxyReader.Close()
64 |
65 | // using 128K buffer to accelerate the download
66 | buf := make([]byte, 1024*128)
67 |
68 | written, err := io.CopyBuffer(outFile, proxyReader, buf)
69 | if err != nil {
70 | return err
71 | }
72 | if contentLength != written {
73 | return errors.New("content length not equal to written")
74 | }
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/docs/command_zhCN.md:
--------------------------------------------------------------------------------
1 | # 命令使用方法
2 |
3 | ## 上传
4 |
5 | - 将本地目录下的所有文件上传至 Movies 文件夹内
6 |
7 | ```bash
8 | pikpakcli upload -p Movies .
9 | ```
10 |
11 | - 将本地目录下除了后缀名为`mp3`, `jpg`的文件上传至 Movies 文件夹内
12 |
13 | ```bash
14 | pikpakcli upload -e .mp3,.jpg -p Movies .
15 | ```
16 |
17 | - 指定上传的协程数目(默认为 16)
18 |
19 | ```bash
20 | pikpakcli -c 20 -p Movies .
21 | ```
22 |
23 | - 使用 `-P` 标志设置 Pikpak 云上文件夹的 `id`
24 |
25 | ```bash
26 | pikpakcli upload -P AgmoDVmJPYbHn8ito1 .
27 | ```
28 |
29 | ## 下载
30 |
31 | - 下载特定目录(如:`Movies` )下的所有文件
32 |
33 | ```bash
34 | pikpakcli download -p Movies
35 | ```
36 |
37 | - 下载单个文件
38 |
39 | ```bash
40 | pikpakcli download -p Movies Peppa_Pig.mp4
41 | # or
42 | pikpakcli download Movies/Peppa_Pig.mp4
43 | ```
44 |
45 | - 限制同时下载的文件个数 (默认: 3)
46 |
47 | ```bash
48 | pikpakcli download -c 5 -p Movies
49 | ```
50 |
51 | - 指定下载文件的输出目录
52 |
53 | ```bash
54 | pikpakcli download -p Movies -o Film
55 | ```
56 |
57 | - 使用 `-g` 标志显示下载过程中的状态信息
58 | ```bash
59 | pikpakcli download -p Movies -o Film -g
60 | ```
61 |
62 | ## 分享
63 |
64 | - 分享 Movies 下的所有文件的链接
65 |
66 | ```bash
67 | pikpakcli share -p Movies
68 | ```
69 |
70 | - 分享指定文件的链接
71 |
72 | ```bash
73 | pikpakcli share Movies/Peppa_Pig.mp4
74 | ```
75 |
76 | - 分享链接输出到指定文件
77 |
78 | ```bash
79 | pikpakcli share --out sha.txt -p Movies
80 | ```
81 |
82 | ## 新建
83 |
84 | ### 新建文件夹
85 |
86 | - 在 Movies 下新建文件夹 NewFolder
87 |
88 | ```bash
89 | pikpakcli new folder -p Movies NewFolder
90 | ```
91 |
92 | ### 新建 Sha 文件
93 |
94 | - 在 Movies 下新建 Sha 文件
95 |
96 | ```bash
97 | pikpakcli new sha -p /Movies 'PikPak://美国队长.mkv|22809693754|75BFE33237A0C06C725587F87981C567E4E478C3'
98 | ```
99 |
100 | ### 新建磁力
101 |
102 | - 新建磁力文件
103 |
104 | ```bash
105 | pikpakcli new url 'magnet:?xt=urn:btih:e9c98e3ed488611abc169a81d8a21487fd1d0732'
106 | ```
107 |
108 | ## 配额
109 |
110 | - 获取 PikPak 云盘的空间
111 |
112 | ```bash
113 | pikpakcli quota -H
114 | ```
115 |
116 | ## 获取目录信息
117 |
118 | - 获取根目录下面的所有文件信息
119 |
120 | ```bash
121 | pikpakcli ls -lH -p /
122 | ```
123 |
--------------------------------------------------------------------------------
/internal/utils/sync.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "os"
7 | "slices"
8 | "strings"
9 | "unsafe"
10 |
11 | "github.com/sirupsen/logrus"
12 | )
13 |
14 | var ErrSyncTxtNotEnable = errors.New("sync txt is not enable")
15 |
16 | type SyncTxt struct {
17 | Enable bool
18 | FileName string
19 | alreadySynced []string
20 | f *os.File
21 | }
22 |
23 | func NewSyncTxt(fileName string, enable bool) (sync *SyncTxt, err error) {
24 | var f *os.File = nil
25 | var alreadySynced []string
26 | if enable {
27 | f, err = os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
28 | if err != nil {
29 | return nil, err
30 | }
31 | bs, err := io.ReadAll(f)
32 | if err != nil {
33 | logrus.Error("read file error: ", err)
34 | os.Exit(1)
35 | }
36 | // avoid end with "\n"
37 | alreadySynced = strings.Split(
38 | strings.TrimRight(unsafe.String(unsafe.SliceData(bs), len(bs)), "\n"),
39 | "\n",
40 | )
41 | }
42 | return &SyncTxt{
43 | Enable: enable,
44 | FileName: fileName,
45 | f: f,
46 | alreadySynced: alreadySynced,
47 | }, nil
48 | }
49 |
50 | // impl Writer
51 | func (s *SyncTxt) Write(b []byte) (n int, err error) {
52 | if !s.Enable {
53 | return 0, ErrSyncTxtNotEnable
54 | }
55 | if b[len(b)-1] != '\n' {
56 | b = append(b, '\n')
57 | }
58 | // add to alreadySynced
59 | s.alreadySynced = append(s.alreadySynced, strings.TrimRight(string(b), "\n"))
60 | return s.f.Write(b)
61 | }
62 |
63 | // impl Closer
64 | func (s *SyncTxt) Close() error {
65 | if !s.Enable {
66 | return ErrSyncTxtNotEnable
67 | }
68 | return s.f.Close()
69 | }
70 |
71 | // impl StringWriter
72 | func (s *SyncTxt) WriteString(str string) (n int, err error) {
73 | if !s.Enable {
74 | return 0, ErrSyncTxtNotEnable
75 | }
76 | if str[len(str)-1] != '\n' {
77 | str += "\n"
78 | }
79 | // add to alreadySynced
80 | s.alreadySynced = append(s.alreadySynced, strings.TrimRight(str, "\n"))
81 | return s.f.WriteString(str)
82 | }
83 |
84 | func (s *SyncTxt) UnSync(files []string) []string {
85 | if !s.Enable {
86 | return files
87 | }
88 | res := make([]string, 0)
89 | for _, f := range files {
90 | if slices.Contains(s.alreadySynced, f) {
91 | continue
92 | }
93 | res = append(res, f)
94 | }
95 | return res
96 | }
97 |
--------------------------------------------------------------------------------
/cmd/new/sha/sha.go:
--------------------------------------------------------------------------------
1 | package sha
2 |
3 | import (
4 | "bufio"
5 | "io"
6 | "os"
7 | "strings"
8 |
9 | "github.com/52funny/pikpakcli/conf"
10 | "github.com/52funny/pikpakcli/internal/pikpak"
11 | "github.com/sirupsen/logrus"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | var NewShaCommand = &cobra.Command{
16 | Use: "sha",
17 | Short: `Create a file according to sha`,
18 | Run: func(cmd *cobra.Command, args []string) {
19 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
20 | err := p.Login()
21 | if err != nil {
22 | logrus.Errorln("Login Failed:", err)
23 | }
24 | // input mode
25 | if strings.TrimSpace(input) != "" {
26 | f, err := os.OpenFile(input, os.O_RDONLY, 0666)
27 | if err != nil {
28 | logrus.Errorf("Open file %s failed: %v\n", input, err)
29 | return
30 | }
31 | reader := bufio.NewReader(f)
32 | shas := make([]string, 0)
33 | for {
34 | lineBytes, _, err := reader.ReadLine()
35 | if err == io.EOF {
36 | break
37 | }
38 | shas = append(shas, string(lineBytes))
39 | }
40 | handleNewSha(&p, shas)
41 | return
42 | }
43 |
44 | // args mode
45 | if len(args) > 0 {
46 | handleNewSha(&p, args)
47 | } else {
48 | logrus.Errorln("Please input the folder name")
49 | }
50 | },
51 | }
52 |
53 | var path string
54 |
55 | var parentId string
56 |
57 | var input string
58 |
59 | func init() {
60 | NewShaCommand.Flags().StringVarP(&path, "path", "p", "/", "The path of the folder")
61 | NewShaCommand.Flags().StringVarP(&input, "input", "i", "", "The input of the sha file")
62 | NewShaCommand.Flags().StringVarP(&parentId, "parent-id", "P", "", "The parent id")
63 | }
64 |
65 | // new folder
66 | func handleNewSha(p *pikpak.PikPak, shas []string) {
67 | var err error
68 | if parentId == "" {
69 | parentId, err = p.GetPathFolderId(path)
70 | if err != nil {
71 | logrus.Errorf("Get parent id failed: %s\n", err)
72 | return
73 | }
74 | }
75 |
76 | for _, sha := range shas {
77 | sha = sha[strings.Index(sha, "://")+3:]
78 | shaElements := strings.Split(sha, "|")
79 | if len(shaElements) != 3 {
80 | logrus.Errorln("The sha format is wrong: ", sha)
81 | continue
82 | }
83 | name, size, sha := shaElements[0], shaElements[1], shaElements[2]
84 | err := p.CreateShaFile(parentId, name, size, sha)
85 | if err != nil {
86 | logrus.Errorln("Create sha file failed: ", err)
87 | continue
88 | }
89 | logrus.Infoln("Create sha file success: ", name)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/docs/command_docker.md:
--------------------------------------------------------------------------------
1 | # Docker Command Usage
2 |
3 | For docker users, the most different part is linking the configuration file (i.e., `config.yml`) and folder you want to operate (e.g., `download` or `upload`) into the container.
4 |
5 | ## Upload
6 |
7 | - Uploads all files in the local directory (e.g., `/path/to/upload`) to the `Movies` folder.
8 |
9 | ```bash
10 | # original cli: pikpakcli upload -p Movies .
11 | # Docker cli
12 | docker run --rm -v /path/to/config.yml:/root/.config/pikpakcli/config.yml -v /path/to/upload/:/upload pikpakcli:latest upload -p Movies /upload
13 | ```
14 |
15 | - Upload files in local directory except for `mp3`, `jpg` to Movies folder.
16 |
17 | ```bash
18 | # original cli: pikpakcli upload -e .mp3,.jpg -p Movies .
19 | # Docker cli
20 | docker run --rm -v /path/to/config.yml:/root/.config/pikpakcli/config.yml -v /path/to/upload/:/upload pikpakcli:latest upload -e .mp3,.jpg -p Movies /upload
21 | ```
22 |
23 | ## Download
24 |
25 | - Download all files in a specific directory (e.g. `Movies`).
26 |
27 | ```bash
28 | # original cli: pikpakcli download -p Movies
29 | # Docker cli
30 | # the option -o is used to specify the folder in container to save downloaded files
31 | docker run --rm -v /path/to/config.yml:/root/.config/pikpakcli/config.yml -v /path/to/download/:/download pikpakcli:latest download -p Movies -o /download
32 | ```
33 |
34 | - Downloading a single file (e.g., `Movies/Peppa_Pig.mp4`).
35 |
36 | ```bash
37 | # original cli: pikpakcli download -p Movies Peppa_Pig.mp4
38 | # Docker cli
39 | docker run --rm -v /path/to/config.yml:/root/.config/pikpakcli/config.yml -v /path/to/download/:/download pikpakcli:latest download -p Movies Peppa_Pig.mp4 -o /download
40 | ```
41 |
42 |
43 | > Other download commands are omitted here, please refer to the original cli commands in [Command Usage](docs/command.md).
44 |
45 |
46 | ## Wrapper Script
47 |
48 | We provide a wrapper script `docker_cli.sh` to simplify the docker command usage. You can run the script directly after setting up the `config.yml` file in the current directory. The script will create two folders `pikpak_downloads` and `pikpak_uploads` in the current directory for download and upload operations respectively.
49 |
50 | ```bash
51 | # Make the script executable
52 | chmod +x docker_cli.sh
53 | # Run the script for upload
54 | ./docker_cli.sh upload -p Movies ./pikpak_uploads
55 | # Run the script for download
56 | ./docker_cli.sh download -p Movies -o ./pikpak_downloads
57 | ```
--------------------------------------------------------------------------------
/internal/utils/path.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io/fs"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 | "slices"
9 | "strings"
10 | )
11 |
12 | func Contains(alreadySyncFiles []string, f string) bool {
13 | return slices.Contains(alreadySyncFiles, f)
14 | }
15 |
16 | func SplitSeparator(path string) []string {
17 | if path == "" {
18 | return []string{}
19 | }
20 | return strings.Split(path, string(filepath.Separator))
21 | }
22 |
23 | func Slash(path string) string {
24 | // clean path
25 | path = filepath.Clean(path)
26 | if path == "" {
27 | return ""
28 | }
29 | if path[0] == filepath.Separator {
30 | return path[1:]
31 | }
32 | return path
33 | }
34 |
35 | // 获取目录文件夹下的所有文件路径名
36 | func GetUploadFilePath(basePath string, defaultRegexp []*regexp.Regexp) ([]string, error) {
37 | rawPath := make([]string, 0)
38 | err := filepath.WalkDir(basePath, func(path string, d fs.DirEntry, err error) error {
39 | if err != nil {
40 | return err
41 | }
42 | // match regexp
43 | // if matched, then skip
44 | // else append
45 | matchRegexp := func(name string) bool {
46 | for _, r := range defaultRegexp {
47 | if r.MatchString(name) {
48 | return true
49 | }
50 | }
51 | return false
52 | }
53 |
54 | if matchRegexp(d.Name()) {
55 | if d.IsDir() {
56 | return filepath.SkipDir
57 | }
58 | return nil
59 | }
60 |
61 | // skip dir
62 | if d.IsDir() {
63 | return nil
64 | }
65 | // get relative path
66 | refPath, err := filepath.Rel(basePath, path)
67 | if err != nil {
68 | return err
69 | }
70 | // append to rawPath
71 | rawPath = append(rawPath, refPath)
72 | return nil
73 | })
74 | return rawPath, err
75 | }
76 |
77 | // 检查路径是否存在
78 | func Exists(path string) (bool, error) {
79 | _, err := os.Stat(path)
80 | if err == nil {
81 | return true, nil
82 | }
83 | if os.IsNotExist(err) {
84 | return false, nil
85 | }
86 | return false, err
87 | }
88 |
89 | // 不存在目录就创建
90 | func CreateDirIfNotExist(path string) error {
91 | exist, err := Exists(path)
92 | if err != nil {
93 | return err
94 | }
95 | if !exist {
96 | err := os.MkdirAll(path, os.ModePerm)
97 | if err != nil {
98 | return err
99 | }
100 | }
101 | return nil
102 | }
103 |
104 | // 创建空文件
105 | func TouchFile(path string) error {
106 | exist, err := Exists(path)
107 | if err != nil {
108 | return err
109 | }
110 | if !exist {
111 | f, err := os.Create(path)
112 | if err != nil {
113 | return err
114 | }
115 | f.Close()
116 | }
117 | return nil
118 | }
119 |
--------------------------------------------------------------------------------
/internal/pikpak/captcha_token.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "crypto/md5"
6 | "fmt"
7 | "net/http"
8 | "time"
9 |
10 | jsoniter "github.com/json-iterator/go"
11 | "github.com/sirupsen/logrus"
12 | )
13 |
14 | const package_name = `com.pikcloud.pikpak`
15 | const client_version = `1.21.0`
16 | const md5_obj = `[{"alg":"md5","salt":""},{"alg":"md5","salt":"E32cSkYXC2bciKJGxRsE8ZgwmH\/YwkvpD6\/O9guSOa2irCwciH4xPHaH"},{"alg":"md5","salt":"QtqgfMgHP2TFl"},{"alg":"md5","salt":"zOKgHT56L7nIzFzDpUGhpWFrgP53m3G6ML"},{"alg":"md5","salt":"S"},{"alg":"md5","salt":"THxpsktzfFXizUv7DK1y\/N7NZ1WhayViluBEvAJJ8bA1Wr6"},{"alg":"md5","salt":"y9PXH3xGUhG\/zQI8CaapRw2LhldCaFM9CRlKpZXJvj+pifu"},{"alg":"md5","salt":"+RaaG7T8FRTI4cP019N5y9ofLyHE9ySFUr"},{"alg":"md5","salt":"6Pf1l8UTeuzYldGtb\/d"}]`
17 |
18 | type md5Obj struct {
19 | Alg string `json:"alg"`
20 | Salt string `json:"salt"`
21 | }
22 |
23 | var md5Arr []md5Obj
24 |
25 | func init() {
26 | err := jsoniter.Unmarshal([]byte(md5_obj), &md5Arr)
27 | if err != nil {
28 | logrus.Warn(err)
29 | }
30 | }
31 |
32 | func (p *PikPak) AuthCaptchaToken(action string) error {
33 | m := make(map[string]interface{})
34 | m["action"] = action
35 | m["captcha_token"] = p.CaptchaToken
36 | m["client_id"] = clientID
37 | m["device_id"] = p.DeviceId
38 | ts := fmt.Sprintf("%d", time.Now().UnixMilli())
39 | str := clientID + client_version + package_name + p.DeviceId + ts
40 |
41 | for i := 0; i < len(md5Arr); i++ {
42 | alg := md5Arr[i].Alg
43 | salt := md5Arr[i].Salt
44 | if alg == "md5" {
45 | str = fmt.Sprintf("%x", md5.Sum([]byte(str+salt)))
46 | }
47 | }
48 | // logrus.Debug("captcha_sign: ", "1."+str)
49 | m["meta"] = map[string]string{
50 | "captcha_sign": "1." + str,
51 | "user_id": p.Sub,
52 | "package_name": package_name,
53 | "client_version": client_version,
54 | "timestamp": ts,
55 | }
56 | m["redirect_uri"] = "ttps://api.mypikpak.com/v1/auth/callback"
57 | bs, err := jsoniter.Marshal(m)
58 | if err != nil {
59 | return err
60 | }
61 | req, err := http.NewRequest("POST", "https://user.mypikpak.com/v1/shield/captcha/init?client_id="+clientID, bytes.NewBuffer(bs))
62 | if err != nil {
63 | return err
64 | }
65 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
66 | bs, err = p.sendRequest(req)
67 | if err != nil {
68 | return err
69 | }
70 | error_code := jsoniter.Get(bs, "error_code").ToInt()
71 | if error_code != 0 {
72 | return fmt.Errorf("auth captcha token error: %s", jsoniter.Get(bs, "error").ToString())
73 | }
74 | p.CaptchaToken = jsoniter.Get(bs, "captcha_token").ToString()
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PikPak CLI
2 |
3 | 
4 | 
5 |
6 | English | [简体中文](https://github.com/52funny/pikpakcli/blob/master/README_zhCN.md)
7 |
8 | PikPakCli is a command line tool for Pikpak Cloud.
9 |
10 | 
11 |
12 | ## Installation
13 |
14 | ### Compiling from source code
15 |
16 | To build the tool from the source code, ensure you have [Go](https://go.dev/doc/install) installed on your system.
17 |
18 | Clone the project:
19 |
20 | ```bash
21 | git clone https://github.com/52funny/pikpakcli
22 | ```
23 |
24 | Build the project:
25 |
26 | ```bash
27 | go build
28 | ```
29 |
30 | Run the tool:
31 |
32 | ```
33 | ./pikpakcli
34 | ```
35 |
36 | ### Build with Docker
37 | You can also run `pikpakcli` using Docker.
38 | Pull the Docker image:
39 |
40 | ```bash
41 | docker pull 52funny/pikpakcli:master
42 | ```
43 |
44 | Run the tool:
45 | ```bash
46 | docker run --rm 52funny/pikpakcli:master --help
47 | ```
48 |
49 | ### Download from Release
50 |
51 | Download the executable file you need from the [Releases](https://github.com/52funny/pikpakcli/releases) page, then run it.
52 |
53 | ## Configuration
54 |
55 | First, configure the `config_example.yml` file in the project, entering your account details.
56 |
57 | If your account uses a phone number, it must be preceded by the country code, like `+861xxxxxxxxxx`.
58 |
59 | Then, rename it to `config.yml`.
60 |
61 | The configuration file will first be read from the current directory (`config.yml`). If it doesn't exist there, it will be read from the user's default configuration directory. The default root directories for each platform are:
62 |
63 | - Linux: `$HOME/.config/pikpakcli`
64 | - Darwin: `$HOME/Library/Application Support/pikpakcli`
65 | - Windows: `%AppData%/pikpakcli`
66 |
67 | > **For Docker Users:** You need to mount the configuration file into the Docker container. For example, if your `config.yml` is located at `/path/to/your/config.yml`, you can run the Docker container like this:
68 |
69 | ```bash
70 | docker run -v /path/to/your/config.yml:/root/.config/pikpakcli/config.yml pikpakcli:latest ls
71 | # if your config.yml is in the project directory, you can just run:
72 | docker run -v $PWD/config.yml:/root/.config/pikpakcli/config.yml pikpakcli:latest ls
73 | ```
74 |
75 | ## Get started
76 |
77 | After that you can run the `ls` command to see the files stored on **PikPak**.
78 |
79 | ```bash
80 | ./pikpakcli ls
81 | ```
82 |
83 | ## Usage
84 |
85 | See [Command](docs/command.md) for more commands information.
86 |
87 | ## Contributors
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/docs/command.md:
--------------------------------------------------------------------------------
1 | # Command Usage
2 |
3 | > For docker users, please refer to the [Docker Command Usage](docs/command_docker.md).
4 |
5 | ## Upload
6 |
7 | - Uploads all files in the local directory to the Movies folder.
8 |
9 | ```bash
10 | pikpakcli upload -p Movies .
11 | ```
12 |
13 | - Upload files in local directory except for `mp3`, `jpg` to Movies folder.
14 |
15 | ```bash
16 | pikpakcli upload -e .mp3,.jpg -p Movies .
17 | ```
18 |
19 | - Select the number of concurrent tasks for the upload (default is 16).
20 |
21 | ```bash
22 | pikpakcli -c 20 -p Movies .
23 | ```
24 |
25 | - Use the `-P` flag to set the `id` of the folder on the Pikpak cloud.
26 |
27 | ```bash
28 | pikpakcli upload -P AgmoDVmJPYbHn8ito1 .
29 | ```
30 |
31 | ## Download
32 |
33 | - Download all files in a specific directory (e.g. `Movies`).
34 |
35 | ```bash
36 | pikpakcli download -p Movies
37 | ```
38 |
39 | - Downloading a single file.
40 |
41 | ```bash
42 | pikpakcli download -p Movies Peppa_Pig.mp4
43 | # or
44 | pikpakcli download Movies/Peppa_Pig.mp4
45 | ```
46 |
47 | - Limit the number of files that can be downloaded at the same time (default: 1)
48 |
49 | ```bash
50 | pikpakcli download -c 5 -p Movies
51 | ```
52 |
53 | - Specify the output directory of the downloaded file.
54 |
55 | ```bash
56 | pikpakcli download -p Movies -o Film
57 | ```
58 |
59 | - Use the `-g` flag to display status information during the download process.
60 |
61 | ```bash
62 | pikpakcli download -p Movies -o Film -g
63 | ```
64 |
65 | ## Share
66 |
67 | - Share links to all files under Movies.
68 |
69 | ```bash
70 | pikpakcli share -p Movies
71 | ```
72 |
73 | - Share the link to the specified file.
74 |
75 | ```bash
76 | pikpakcli share Movies/Peppa_Pig.mp4
77 | ```
78 |
79 | - Share link output to a specified file.
80 |
81 | ```bash
82 | pikpakcli share --out sha.txt -p Movies
83 | ```
84 |
85 | ## New
86 |
87 | ### New Folder
88 |
89 | - Create a new folder NewFolder under Movies
90 |
91 | ```bash
92 | pikpakcli new folder -p Movies NewFolder
93 | ```
94 |
95 | ### New Sha File
96 |
97 | - Create a new Sha file under Movies.
98 |
99 | ```bash
100 | pikpakcli new sha -p /Movies 'PikPak://美国队长.mkv|22809693754|75BFE33237A0C06C725587F87981C567E4E478C3'
101 | ```
102 |
103 | ### New Magnet File
104 |
105 | - Create new magnet file.
106 |
107 | ```bash
108 | pikpakcli new url 'magnet:?xt=urn:btih:e9c98e3ed488611abc169a81d8a21487fd1d0732'
109 | ```
110 |
111 | ## Quota
112 |
113 | - Get space on your PikPak cloud drive.
114 |
115 | ```bash
116 | pikpakcli quota -H
117 | ```
118 |
119 | ## Ls
120 |
121 | - Get information about all files in the root directory.
122 |
123 | ```bash
124 | pikpakcli ls -lH -p /
125 | ```
126 |
--------------------------------------------------------------------------------
/cmd/share/share.go:
--------------------------------------------------------------------------------
1 | package share
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | "github.com/52funny/pikpakcli/conf"
9 | "github.com/52funny/pikpakcli/internal/pikpak"
10 | "github.com/sirupsen/logrus"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var ShareCommand = &cobra.Command{
15 | Use: "share",
16 | Aliases: []string{"d"},
17 | Short: `Share file links on the pikpak server`,
18 | Run: func(cmd *cobra.Command, args []string) {
19 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
20 | err := p.Login()
21 | if err != nil {
22 | logrus.Errorln("Login Failed:", err)
23 | }
24 | // Output file handle
25 | var f = os.Stdout
26 | if strings.TrimSpace(output) != "" {
27 | file, err := os.Create(output)
28 | if err != nil {
29 | logrus.Errorln("Create file failed:", err)
30 | return
31 | }
32 | defer file.Close()
33 | f = file
34 | }
35 |
36 | if len(args) > 0 {
37 | shareFiles(&p, args, f)
38 | } else {
39 | shareFolder(&p, f)
40 | }
41 | },
42 | }
43 |
44 | // Specifies the folder of the pikpak server
45 | // default is the root folder
46 | var folder string
47 |
48 | // Specifies the file to write
49 | // default is the stdout
50 | var output string
51 |
52 | var parentId string
53 |
54 | func init() {
55 | ShareCommand.Flags().StringVarP(&folder, "path", "p", "/", "specific the folder of the pikpak server")
56 | ShareCommand.Flags().StringVarP(&output, "output", "o", "", "specific the file to write")
57 | ShareCommand.Flags().StringVarP(&parentId, "parent-id", "P", "", "parent folder id")
58 | }
59 |
60 | // Share folder
61 | func shareFolder(p *pikpak.PikPak, f *os.File) {
62 | var err error
63 | if parentId == "" {
64 | parentId, err = p.GetDeepFolderId("", folder)
65 | if err != nil {
66 | logrus.Errorln("Get parent id failed:", err)
67 | return
68 | }
69 | }
70 | fileStat, err := p.GetFolderFileStatList(parentId)
71 | if err != nil {
72 | logrus.Errorln("Get folder file stat list failed:", err)
73 | return
74 | }
75 | for _, stat := range fileStat {
76 | // logrus.Debug(stat)
77 | if stat.Kind == "drive#file" {
78 | fmt.Fprintf(f, "PikPak://%s|%s|%s\n", stat.Name, stat.Size, stat.Hash)
79 | }
80 | }
81 | }
82 |
83 | // Share files
84 | func shareFiles(p *pikpak.PikPak, args []string, f *os.File) {
85 | var err error
86 | if parentId == "" {
87 | parentId, err = p.GetPathFolderId(folder)
88 | if err != nil {
89 | logrus.Errorln("get parent id failed:", err)
90 | return
91 | }
92 | }
93 | for _, path := range args {
94 | stat, err := p.GetFileStat(parentId, path)
95 | if err != nil {
96 | logrus.Errorln(path, "get file stat error:", err)
97 | continue
98 | }
99 | fmt.Fprintf(f, "PikPak://%s|%s|%s\n", stat.Name, stat.Size, stat.Hash)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/cmd/new/url/url.go:
--------------------------------------------------------------------------------
1 | package url
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io"
7 | "os"
8 | "strings"
9 |
10 | "github.com/52funny/pikpakcli/conf"
11 | "github.com/52funny/pikpakcli/internal/pikpak"
12 | "github.com/sirupsen/logrus"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | var NewUrlCommand = &cobra.Command{
17 | Use: "url",
18 | Short: `Create a file according to url`,
19 | Run: func(cmd *cobra.Command, args []string) {
20 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
21 | err := p.Login()
22 | if err != nil {
23 | logrus.Errorln("Login Failed:", err)
24 | }
25 | if cli {
26 | handleCli(&p)
27 | return
28 | }
29 | // input mode
30 | if strings.TrimSpace(input) != "" {
31 | f, err := os.OpenFile(input, os.O_RDONLY, 0666)
32 | if err != nil {
33 | logrus.Errorf("Open file %s failed: %s", input, err)
34 | return
35 | }
36 | reader := bufio.NewReader(f)
37 | shas := make([]string, 0)
38 | for {
39 | lineBytes, _, err := reader.ReadLine()
40 | if err == io.EOF {
41 | break
42 | }
43 | shas = append(shas, string(lineBytes))
44 | }
45 | handleNewUrl(&p, shas)
46 | return
47 | }
48 |
49 | // args mode
50 | if len(args) > 0 {
51 | handleNewUrl(&p, args)
52 | } else {
53 | logrus.Errorln("Please input the folder name")
54 | }
55 | },
56 | }
57 |
58 | var path string
59 |
60 | var parentId string
61 |
62 | var input string
63 |
64 | var cli bool
65 |
66 | func init() {
67 | NewUrlCommand.Flags().StringVarP(&path, "path", "p", "/", "The path of the folder")
68 | NewUrlCommand.Flags().StringVarP(&parentId, "parent-id", "P", "", "The parent id")
69 | NewUrlCommand.Flags().StringVarP(&input, "input", "i", "", "The input of the sha file")
70 | NewUrlCommand.Flags().BoolVarP(&cli, "cli", "c", false, "The cli mode")
71 | }
72 |
73 | // new folder
74 | func handleNewUrl(p *pikpak.PikPak, shas []string) {
75 | var err error
76 | if parentId == "" {
77 | parentId, err = p.GetPathFolderId(path)
78 | if err != nil {
79 | logrus.Errorf("Get parent id failed: %s\n", err)
80 | return
81 | }
82 | }
83 | for _, url := range shas {
84 | err := p.CreateUrlFile(parentId, url)
85 | if err != nil {
86 | logrus.Errorln("Create url file failed: ", err)
87 | continue
88 | }
89 | logrus.Infoln("Create url file success: ", url)
90 | }
91 | }
92 |
93 | func handleCli(p *pikpak.PikPak) {
94 | var err error
95 | if parentId == "" {
96 | parentId, err = p.GetPathFolderId(path)
97 | if err != nil {
98 | logrus.Errorf("Get parent id failed: %s\n", err)
99 | return
100 | }
101 | }
102 |
103 | reader := bufio.NewReader(os.Stdin)
104 | for {
105 | fmt.Print("> ")
106 | lineBytes, _, err := reader.ReadLine()
107 | if err == io.EOF {
108 | break
109 | }
110 | url := string(lineBytes)
111 | err = p.CreateUrlFile(parentId, url)
112 | if err != nil {
113 | logrus.Errorln("Create url file failed: ", err)
114 | continue
115 | }
116 | logrus.Infoln("Create url file success: ", url)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/cmd/list/list.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 |
7 | "github.com/52funny/pikpakcli/conf"
8 | "github.com/52funny/pikpakcli/internal/pikpak"
9 | "github.com/fatih/color"
10 | "github.com/sirupsen/logrus"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var long bool
15 | var human bool
16 | var path string
17 | var parentId string
18 |
19 | var ListCmd = &cobra.Command{
20 | Use: "ls",
21 | Short: `Get the directory information under the specified folder`,
22 | Run: func(cmd *cobra.Command, args []string) {
23 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
24 | err := p.Login()
25 | if err != nil {
26 | logrus.Errorln("Login Failed:", err)
27 | }
28 | handle(&p, args)
29 | },
30 | }
31 |
32 | func init() {
33 | ListCmd.Flags().BoolVarP(&human, "human", "H", false, "display human readable format")
34 | ListCmd.Flags().BoolVarP(&long, "long", "l", false, "display long format")
35 | ListCmd.Flags().StringVarP(&path, "path", "p", "/", "display the specified path")
36 | ListCmd.Flags().StringVarP(&parentId, "parent-id", "P", "", "display the specified parent id")
37 | }
38 |
39 | func handle(p *pikpak.PikPak, args []string) {
40 | var err error
41 | if parentId == "" {
42 | parentId, err = p.GetPathFolderId(path)
43 | if err != nil {
44 | logrus.Errorln("get path folder id error:", err)
45 | return
46 | }
47 | }
48 | files, err := p.GetFolderFileStatList(parentId)
49 | if err != nil {
50 | logrus.Errorln("get folder file stat list error:", err)
51 | return
52 | }
53 | for _, file := range files {
54 | if long {
55 | if human {
56 | display(3, &file)
57 | } else {
58 | display(2, &file)
59 | }
60 | } else {
61 | display(0, &file)
62 | }
63 | }
64 | }
65 |
66 | // lH
67 | // mode 0: normal print
68 | // mode 2: long format
69 | // mode 3: long format and human readable
70 |
71 | func display(mode int, file *pikpak.FileStat) {
72 | switch mode {
73 | case 0:
74 | if file.Kind == "drive#folder" {
75 | fmt.Printf("%-20s\n", color.GreenString(file.Name))
76 | } else {
77 | fmt.Printf("%-20s\n", file.Name)
78 | }
79 | case 2:
80 | if file.Kind == "drive#folder" {
81 | fmt.Printf("%-26s %-6s %-14s %s\n", file.ID, file.Size, file.CreatedTime.Format("2006-01-02 15:04:05"), color.GreenString(file.Name))
82 | } else {
83 | fmt.Printf("%-26s %-6s %-14s %s\n", file.ID, file.Size, file.CreatedTime.Format("2006-01-02 15:04:05"), file.Name)
84 | }
85 | case 3:
86 | if file.Kind == "drive#folder" {
87 | fmt.Printf("%-26s %-6s %-14s %s\n", file.ID, displayStorage(file.Size), file.CreatedTime.Format("2006-01-02 15:04:05"), color.GreenString(file.Name))
88 | } else {
89 | fmt.Printf("%-26s %-6s %-14s %s\n", file.ID, displayStorage(file.Size), file.CreatedTime.Format("2006-01-02 15:04:05"), file.Name)
90 | }
91 | }
92 | }
93 |
94 | func displayStorage(s string) string {
95 | size, _ := strconv.ParseUint(s, 10, 64)
96 | cnt := 0
97 | for size > 1024 {
98 | cnt += 1
99 | if cnt > 5 {
100 | break
101 | }
102 | size /= 1024
103 | }
104 | res := strconv.Itoa(int(size))
105 | switch cnt {
106 | case 0:
107 | res += "B"
108 | case 1:
109 | res += "KB"
110 | case 2:
111 | res += "MB"
112 | case 3:
113 | res += "GB"
114 | case 4:
115 | res += "TB"
116 | case 5:
117 | res += "PB"
118 | }
119 | return res
120 | }
121 |
--------------------------------------------------------------------------------
/conf/config.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | "io"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 |
12 | "gopkg.in/yaml.v2"
13 | )
14 |
15 | type ConfigType struct {
16 | Proxy string
17 | Username string
18 | Password string
19 | }
20 |
21 | var Config ConfigType
22 |
23 | // UseProxy returns whether the proxy is used
24 | func (c *ConfigType) UseProxy() bool {
25 | return len(c.Proxy) != 0
26 | }
27 |
28 | // Initializing configuration information
29 | func InitConfig(path string) error {
30 | // Firstly, read the config info from executable file
31 | if readFromBinary() == nil {
32 | return nil
33 | }
34 |
35 | // Secondly, it reads config.yml from the given path.
36 | // If there is no config.yml in the given path, it reads it from the default config path.
37 | _, err := os.Stat(path)
38 | switch os.IsNotExist(err) {
39 | case true:
40 | if err := readFromConfigDir(); err != nil {
41 | return err
42 | }
43 | case false:
44 | if err := readFromPath(path); err != nil {
45 | return err
46 | }
47 | }
48 |
49 | // Not empty
50 | // Must contains '://'
51 | if len(Config.Proxy) != 0 && !strings.Contains(Config.Proxy, "://") {
52 | return fmt.Errorf("proxy should contains ://")
53 | }
54 | return nil
55 | }
56 |
57 | // Read config from binary in the end
58 | // config_bytes: n bytes
59 | // end_magic: 10 bytes
60 | // size: 4 bytes
61 | // -----------------------------------
62 | // | config_bytes | size | end_magic |
63 | // -----------------------------------
64 | func readFromBinary() error {
65 | f, err := os.Open(os.Args[0])
66 | if err != nil {
67 | return err
68 | }
69 | defer f.Close()
70 | stat, err := f.Stat()
71 | if err != nil {
72 | return err
73 | }
74 |
75 | var end_magic = make([]byte, 10)
76 | n, err := f.ReadAt(end_magic, stat.Size()-10)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | if n != 10 {
82 | return fmt.Errorf("read end_magic err: %d", n)
83 | }
84 |
85 | // Not have `config.yml` in the end
86 | if !bytes.Equal(end_magic, []byte("config.yml")) {
87 | return fmt.Errorf("not a pikpakcli binary")
88 | }
89 |
90 | var size = make([]byte, 4)
91 | n, err = f.ReadAt(size, stat.Size()-14)
92 |
93 | if err != nil {
94 | return err
95 | }
96 |
97 | if n != 4 {
98 | return fmt.Errorf("read size err: %d", n)
99 | }
100 |
101 | configSize := int64(binary.LittleEndian.Uint32(size))
102 | configBuf := make([]byte, configSize)
103 |
104 | n, err = f.ReadAt(configBuf, stat.Size()-14-configSize)
105 |
106 | if err != nil || n != int(configSize) {
107 | return err
108 | }
109 |
110 | if n != int(configSize) {
111 | return fmt.Errorf("read config size err: %d", n)
112 | }
113 |
114 | // Unmarshal config
115 | return yaml.Unmarshal(configBuf, &Config)
116 | }
117 |
118 | // Read configuration file from the given path
119 | func readFromPath(path string) error {
120 | f, err := os.Open(path)
121 | if err != nil {
122 | return err
123 | }
124 | defer f.Close()
125 | bs, err := io.ReadAll(f)
126 | if err != nil {
127 | return err
128 | }
129 | err = yaml.Unmarshal(bs, &Config)
130 | if err != nil {
131 | return err
132 | }
133 | return nil
134 | }
135 |
136 | // Read configuration file from config path
137 | func readFromConfigDir() error {
138 | configDir, err := os.UserConfigDir()
139 | if err != nil {
140 | return err
141 | }
142 | path := filepath.Join(configDir, "pikpakcli", "config.yml")
143 | f, err := os.Open(path)
144 | if err != nil {
145 | return err
146 | }
147 | defer f.Close()
148 | bs, err := io.ReadAll(f)
149 | if err != nil {
150 | return err
151 | }
152 | err = yaml.Unmarshal(bs, &Config)
153 | if err != nil {
154 | return err
155 | }
156 | return nil
157 | }
158 |
--------------------------------------------------------------------------------
/internal/pikpak/pikpak.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "crypto/md5"
6 | "encoding/hex"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "net/url"
11 |
12 | "github.com/52funny/pikpakcli/conf"
13 | jsoniter "github.com/json-iterator/go"
14 | "github.com/sirupsen/logrus"
15 | )
16 |
17 | const userAgent = `ANDROID-com.pikcloud.pikpak/1.21.0`
18 | const clientID = `YNxT9w7GMdWvEOKa`
19 | const clientSecret = `dbw2OtmVEeuUvIptb1Coyg`
20 |
21 | type PikPak struct {
22 | Account string `json:"account"`
23 | Password string `json:"password"`
24 | JwtToken string `json:"token"`
25 | refreshToken string
26 | CaptchaToken string `json:"captchaToken"`
27 | Sub string `json:"userId"`
28 | DeviceId string `json:"deviceId"`
29 | RefreshSecond int64 `json:"refreshSecond"`
30 | client *http.Client
31 | }
32 |
33 | func NewPikPak(account, password string) PikPak {
34 | client := &http.Client{
35 | Transport: &http.Transport{
36 | Proxy: http.ProxyFromEnvironment,
37 | },
38 | }
39 | if conf.Config.UseProxy() {
40 | url, err := url.Parse(conf.Config.Proxy)
41 | if err != nil {
42 | logrus.Errorln("url parse proxy error", err)
43 | }
44 | p := http.ProxyURL(url)
45 | client.Transport = &http.Transport{
46 | Proxy: p,
47 | }
48 | http.DefaultClient.Transport = &http.Transport{
49 | Proxy: p,
50 | }
51 | }
52 | n := md5.Sum([]byte(account))
53 | return PikPak{
54 | Account: account,
55 | Password: password,
56 | DeviceId: hex.EncodeToString(n[:]),
57 | client: client,
58 | }
59 | }
60 |
61 | func (p *PikPak) Login() error {
62 | captchaToken, err := p.getCaptchaToken()
63 | if err != nil {
64 | return err
65 | }
66 | m := make(map[string]string)
67 | m["client_id"] = clientID
68 | m["client_secret"] = clientSecret
69 | m["grant_type"] = "password"
70 | m["username"] = p.Account
71 | m["password"] = p.Password
72 | m["captcha_token"] = captchaToken
73 | bs, err := jsoniter.Marshal(&m)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | req, err := http.NewRequest("POST", "https://user.mypikpak.com/v1/auth/signin", bytes.NewBuffer(bs))
79 | if err != nil {
80 | return err
81 | }
82 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
83 | bs, err = p.sendRequest(req)
84 | if err != nil {
85 | return err
86 | }
87 |
88 | error_code := jsoniter.Get(bs, "error_code").ToInt()
89 |
90 | if error_code != 0 {
91 | return fmt.Errorf("login error: %v", jsoniter.Get(bs, "error").ToString())
92 | }
93 |
94 | p.JwtToken = jsoniter.Get(bs, "access_token").ToString()
95 | p.refreshToken = jsoniter.Get(bs, "refresh_token").ToString()
96 | p.Sub = jsoniter.Get(bs, "sub").ToString()
97 | p.RefreshSecond = jsoniter.Get(bs, "expires_in").ToInt64()
98 | return nil
99 | }
100 |
101 | func (p *PikPak) getCaptchaToken() (string, error) {
102 | m := make(map[string]any)
103 | m["client_id"] = clientID
104 | m["device_id"] = p.DeviceId
105 | m["action"] = "POST:https://user.mypikpak.com/v1/auth/signin"
106 | m["meta"] = map[string]string{
107 | "username": p.Account,
108 | }
109 | body, err := jsoniter.Marshal(&m)
110 | if err != nil {
111 | return "", err
112 | }
113 | req, err := http.NewRequest("POST", "https://user.mypikpak.com/v1/shield/captcha/init", bytes.NewBuffer(body))
114 | if err != nil {
115 | return "", err
116 | }
117 | req.Header.Add("Content-Type", "application/json")
118 | bs, err := p.sendRequest(req)
119 | if err != nil {
120 | return "", err
121 | }
122 | error_code := jsoniter.Get(bs, "error_code").ToInt()
123 | if error_code != 0 {
124 | return "", fmt.Errorf("get captcha error: %v", jsoniter.Get(bs, "error").ToString())
125 | }
126 | return jsoniter.Get(bs, "captcha_token").ToString(), nil
127 | }
128 |
129 | func (p *PikPak) sendRequest(req *http.Request) ([]byte, error) {
130 | p.setHeader(req)
131 | resp, err := p.client.Do(req)
132 | if err != nil {
133 | return nil, err
134 | }
135 | bs, err := io.ReadAll(resp.Body)
136 | defer resp.Body.Close()
137 | if err != nil {
138 | return nil, err
139 | }
140 | return bs, nil
141 | }
142 |
143 | func (p *PikPak) setHeader(req *http.Request) {
144 | if p.JwtToken != "" {
145 | req.Header.Set("Authorization", "Bearer "+p.JwtToken)
146 | }
147 | req.Header.Set("User-Agent", userAgent)
148 | req.Header.Set("X-Device-Id", p.DeviceId)
149 | }
150 |
--------------------------------------------------------------------------------
/cmd/embed/embed.go:
--------------------------------------------------------------------------------
1 | package embed
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "github.com/52funny/pikpakcli/internal/utils"
10 | "github.com/sirupsen/logrus"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | const magic = "config.yml"
15 |
16 | var EmbedCmd = &cobra.Command{
17 | Use: "embed",
18 | Short: `Embed config file`,
19 | Run: func(cmd *cobra.Command, args []string) {
20 | if len(args) <= 0 {
21 | logrus.Errorln("Please specify the config file path")
22 | os.Exit(1)
23 | }
24 | ok, err := checkEmbed()
25 | if err != nil {
26 | logrus.Errorln("check magic error", err)
27 | os.Exit(1)
28 | }
29 |
30 | if update && ok {
31 | err = updateEmbed(args[0], os.Args[0])
32 | if err != nil {
33 | logrus.Errorln(err)
34 | os.Exit(1)
35 | }
36 | logrus.Infoln("Update embed config file success")
37 | os.Exit(0)
38 | }
39 |
40 | if ok {
41 | logrus.Warnln("config file has been embedded")
42 | os.Exit(1)
43 | }
44 | copyBin, err := copyBin(os.Args[0])
45 | if err != nil {
46 | logrus.Errorln("create copy binary file error:", err.Error())
47 | os.Exit(1)
48 | }
49 | err = embed(args[0], copyBin)
50 | if err != nil {
51 | logrus.Errorln(err)
52 | os.Exit(1)
53 | }
54 | logrus.Infoln("Embed config file success")
55 | },
56 | }
57 |
58 | var update bool
59 |
60 | func init() {
61 | EmbedCmd.Flags().BoolVarP(&update, "update", "u", false, "update embed config")
62 | }
63 |
64 | func checkEmbed() (bool, error) {
65 | f, err := os.Open(os.Args[0])
66 | if err != nil {
67 | return false, err
68 | }
69 | defer f.Close()
70 | fStat, _ := f.Stat()
71 | magicBuf := make([]byte, len(magic))
72 | n, err := f.ReadAt(magicBuf, fStat.Size()-int64(len(magic)))
73 | if err != nil {
74 | return false, err
75 | }
76 | if n != len(magic) {
77 | return false, fmt.Errorf("read magic size error")
78 | }
79 | return string(magicBuf) == magic, nil
80 | }
81 |
82 | // embed config file to binary
83 | func embed(configPath string, binFile *os.File) error {
84 | f, err := os.Open(configPath)
85 | if err != nil {
86 | return fmt.Errorf("open config file error: %s", err.Error())
87 | }
88 | defer f.Close()
89 | fStat, _ := f.Stat()
90 | bs, err := io.ReadAll(f)
91 | if err != nil {
92 | return fmt.Errorf("read config file error: %s", err.Error())
93 | }
94 | sizeBytes := make([]byte, 4)
95 | binary.LittleEndian.PutUint32(sizeBytes, uint32(fStat.Size()))
96 | bs = append(bs, sizeBytes...)
97 | bs = append(bs, []byte("config.yml")...)
98 | // binFile, err := os.OpenFile(binPath, os.O_WRONLY|os.O_APPEND, 0666)
99 | // if err != nil {
100 | // return fmt.Errorf("open binary file error: %s", err.Error())
101 | // }
102 | defer binFile.Close()
103 | n, err := binFile.Write(bs)
104 | if err != nil || n != len(bs) {
105 | return fmt.Errorf("write to binary error: %s", err.Error())
106 | }
107 | return nil
108 | }
109 |
110 | // first remove embed config
111 | // second embed config
112 | func updateEmbed(configPath string, BinPath string) error {
113 | copyBin, err := copyBin(BinPath)
114 | if err != nil {
115 | return err
116 | }
117 | binStat, _ := copyBin.Stat()
118 | var size = make([]byte, 4)
119 | n, err := copyBin.ReadAt(size, binStat.Size()-14)
120 |
121 | if err != nil {
122 | return err
123 | }
124 |
125 | if n != 4 {
126 | return fmt.Errorf("read size err: %d", n)
127 | }
128 |
129 | configSize := int64(binary.LittleEndian.Uint32(size))
130 | copyBin.Seek(binStat.Size()-14-configSize, 0)
131 | err = copyBin.Truncate(binStat.Size() - 14 - configSize)
132 | if err != nil {
133 | return err
134 | }
135 |
136 | return embed(configPath, copyBin)
137 | }
138 |
139 | func copyBin(path string) (*os.File, error) {
140 | binFile, err := os.Open(path)
141 | if err != nil {
142 | return nil, fmt.Errorf("open binary file error: %s", err.Error())
143 | }
144 | copyBinName := utils.GetEmbedBinName(os.Args[0])
145 | binSt, _ := binFile.Stat()
146 | copyBin, err := os.OpenFile(copyBinName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, binSt.Mode())
147 | if err != nil {
148 | return nil, fmt.Errorf("open copy binary file error: %s", err.Error())
149 | }
150 | io.Copy(copyBin, binFile)
151 | binFile.Close()
152 | return copyBin, nil
153 | }
154 |
155 | // delete some bytes in the end of file
156 | func deleteBytes(f *os.File, n int64) error {
157 | fStat, _ := f.Stat()
158 | // read the last n bytes
159 | bs := make([]byte, n)
160 | _, err := f.ReadAt(bs, fStat.Size()-n)
161 | if err != nil {
162 | return err
163 | }
164 | // truncate file
165 | err = f.Truncate(fStat.Size() - n)
166 | if err != nil {
167 | return err
168 | }
169 | return nil
170 | }
171 |
--------------------------------------------------------------------------------
/internal/pikpak/folder.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 |
9 | "github.com/52funny/pikpakcli/internal/utils"
10 | jsoniter "github.com/json-iterator/go"
11 | "github.com/sirupsen/logrus"
12 | "github.com/tidwall/gjson"
13 | )
14 |
15 | // 获取文件夹 id
16 | // dir 可以包括 /.
17 | // 若以 / 开头,函数会去除 /, 且会从 parent 目录开始查找
18 | func (p *PikPak) GetDeepFolderId(parentId string, dirPath string) (string, error) {
19 | dirPath = utils.Slash(dirPath)
20 | if dirPath == "" {
21 | return parentId, nil
22 | }
23 |
24 | dirS := utils.SplitSeparator(dirPath)
25 |
26 | for _, dir := range dirS {
27 | id, err := p.GetFolderId(parentId, dir)
28 | if err != nil {
29 | return "", err
30 | }
31 | parentId = id
32 | }
33 | return parentId, nil
34 | }
35 |
36 | func (p *PikPak) GetPathFolderId(dirPath string) (string, error) {
37 | return p.GetDeepFolderId("", dirPath)
38 | }
39 |
40 | // 获取文件夹 id
41 | // dir 不能包括 /
42 | func (p *PikPak) GetFolderId(parentId string, dir string) (string, error) {
43 | // slash the dir path
44 | dir = utils.Slash(dir)
45 |
46 | value := url.Values{}
47 | value.Add("parent_id", parentId)
48 | value.Add("page_token", "")
49 | value.Add("with_audit", "false")
50 | value.Add("thumbnail_size", "SIZE_LARGE")
51 | value.Add("limit", "500")
52 | for {
53 | req, err := http.NewRequest("GET", fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files?"+value.Encode()), nil)
54 | if err != nil {
55 | return "", err
56 | }
57 | req.Header.Set("Country", "CN")
58 | req.Header.Set("X-Peer-Id", p.DeviceId)
59 | req.Header.Set("X-User-Region", "1")
60 | req.Header.Set("X-Alt-Capability", "3")
61 | req.Header.Set("X-Client-Version-Code", "10083")
62 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
63 | bs, err := p.sendRequest(req)
64 | if err != nil {
65 | return "", err
66 | }
67 | files := gjson.GetBytes(bs, "files").Array()
68 |
69 | for _, file := range files {
70 | kind := file.Get("kind").String()
71 | name := file.Get("name").String()
72 | trashed := file.Get("trashed").Bool()
73 | if kind == "drive#folder" && name == dir && !trashed {
74 | return file.Get("id").String(), nil
75 | }
76 | }
77 | nextToken := gjson.GetBytes(bs, "next_page_token").String()
78 | if nextToken == "" {
79 | break
80 | }
81 | value.Set("page_token", nextToken)
82 | }
83 | return "", ErrNotFoundFolder
84 | }
85 |
86 | func (p *PikPak) GetDeepFolderOrCreateId(parentId string, dirPath string) (string, error) {
87 | dirPath = utils.Slash(dirPath)
88 | if dirPath == "" || dirPath == "." {
89 | return parentId, nil
90 | }
91 |
92 | dirS := utils.SplitSeparator(dirPath)
93 |
94 | for _, dir := range dirS {
95 | id, err := p.GetFolderId(parentId, dir)
96 | if err != nil {
97 | logrus.Warn("dir ", err)
98 | if err == ErrNotFoundFolder {
99 | createId, err := p.CreateFolder(parentId, dir)
100 | if err != nil {
101 | return "", err
102 | } else {
103 | logrus.Info("create dir: ", dir)
104 | parentId = createId
105 | }
106 | } else {
107 | return "", err
108 | }
109 | } else {
110 | parentId = id
111 | }
112 | }
113 | return parentId, nil
114 | }
115 |
116 | // Create new folder in parent folder
117 | // parentId is parent folder id
118 | func (p *PikPak) CreateFolder(parentId, dir string) (string, error) {
119 | m := map[string]interface{}{
120 | "kind": "drive#folder",
121 | "parent_id": parentId,
122 | "name": dir,
123 | }
124 | bs, err := jsoniter.Marshal(&m)
125 | if err != nil {
126 | return "", err
127 | }
128 | START:
129 | req, err := http.NewRequest("POST", "https://api-drive.mypikpak.com/drive/v1/files", bytes.NewBuffer(bs))
130 | if err != nil {
131 | return "", err
132 | }
133 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
134 | req.Header.Set("Product_flavor_name", "cha")
135 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
136 | req.Header.Set("X-Client-Version-Code", "10083")
137 | req.Header.Set("X-Peer-Id", p.DeviceId)
138 | req.Header.Set("X-User-Region", "1")
139 | req.Header.Set("X-Alt-Capability", "3")
140 | req.Header.Set("Country", "CN")
141 | bs, err = p.sendRequest(req)
142 | if err != nil {
143 | return "", err
144 | }
145 | error_code := gjson.GetBytes(bs, "error_code").Int()
146 | if error_code != 0 {
147 | if error_code == 9 {
148 | err := p.AuthCaptchaToken("POST:/drive/v1/files")
149 | if err != nil {
150 | return "", err
151 | }
152 | goto START
153 | }
154 | return "", fmt.Errorf("create folder error: %s", jsoniter.Get(bs, "error").ToString())
155 | }
156 | id := gjson.GetBytes(bs, "file.id").String()
157 | return id, nil
158 | }
159 |
--------------------------------------------------------------------------------
/cmd/upload/upload.go:
--------------------------------------------------------------------------------
1 | package upload
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "regexp"
7 | "strings"
8 | "time"
9 |
10 | "github.com/52funny/pikpakcli/conf"
11 | "github.com/52funny/pikpakcli/internal/pikpak"
12 | "github.com/52funny/pikpakcli/internal/utils"
13 | "github.com/sirupsen/logrus"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | var UploadCmd = &cobra.Command{
18 | Use: "upload",
19 | Aliases: []string{"u"},
20 | Short: `Upload file to pikpak server`,
21 | Run: func(cmd *cobra.Command, args []string) {
22 | pikpak.Concurrent = uploadConcurrency
23 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
24 | err := p.Login()
25 | if err != nil {
26 | logrus.Error(err)
27 | }
28 | err = p.AuthCaptchaToken("POST:/drive/v1/files")
29 | if err != nil {
30 | logrus.Error(err)
31 | }
32 |
33 | go func() {
34 | ticker := time.NewTicker(time.Second * 7200 * 3 / 4)
35 | defer ticker.Stop()
36 | for range ticker.C {
37 | err := p.RefreshToken()
38 | if err != nil {
39 | logrus.Warn(err)
40 | continue
41 | }
42 | }
43 | }()
44 | for _, v := range args {
45 | stat, err := os.Stat(v)
46 | if err != nil {
47 | logrus.Errorf("Get file %s stat failed: %s", v, err)
48 | continue
49 | }
50 | if stat.IsDir() {
51 | handleUploadFolder(&p, v)
52 | } else {
53 | handleUploadFile(&p, v)
54 | }
55 | }
56 | },
57 | }
58 |
59 | // Specifies the folder of the pikpak server
60 | var uploadFolder string
61 |
62 | // Specifies the file to upload
63 | var uploadConcurrency int64
64 |
65 | // Sync mode
66 | var sync bool
67 |
68 | // Parent path id
69 | var parentId string
70 |
71 | // Init upload command
72 | func init() {
73 | UploadCmd.Flags().StringVarP(&uploadFolder, "path", "p", "/", "specific the folder of the pikpak server")
74 | UploadCmd.Flags().Int64VarP(&uploadConcurrency, "concurrency", "c", 1<<4, "specific the concurrency of the upload")
75 | UploadCmd.Flags().StringSliceVarP(&exclude, "exn", "e", []string{}, "specific the exclude file or folder")
76 | UploadCmd.Flags().BoolVarP(&sync, "sync", "s", false, "sync mode")
77 | UploadCmd.Flags().StringVarP(&parentId, "parent-id", "P", "", "parent folder id")
78 | }
79 |
80 | // Exclude string list
81 | var exclude []string
82 |
83 | var defaultExcludeRegexp []*regexp.Regexp = []*regexp.Regexp{
84 | // exclude the hidden file
85 | regexp.MustCompile(`^\..+`),
86 | }
87 |
88 | // Dispose the exclude file or folder
89 | func disposeExclude() {
90 | for _, v := range exclude {
91 | defaultExcludeRegexp = append(defaultExcludeRegexp, regexp.MustCompile(v))
92 | }
93 | }
94 |
95 | func handleUploadFile(p *pikpak.PikPak, path string) {
96 | var err error
97 | if parentId == "" {
98 | parentId, err = p.GetDeepFolderOrCreateId("", uploadFolder)
99 | if err != nil {
100 | logrus.Errorf("Get folder %s id failed: %s", uploadFolder, err)
101 | return
102 | }
103 | }
104 | err = p.UploadFile(parentId, path)
105 | if err != nil {
106 | logrus.Errorf("Upload file %s failed: %s\n", path, err)
107 | return
108 | }
109 | logrus.Infof("Upload file %s success!\n", path)
110 | }
111 |
112 | // upload files logic
113 | func handleUploadFolder(p *pikpak.PikPak, path string) {
114 | basePath := filepath.Base(filepath.ToSlash(path))
115 | uploadFilePath, err := utils.GetUploadFilePath(path, defaultExcludeRegexp)
116 | if err != nil {
117 | logrus.Errorln(err)
118 | return
119 | }
120 |
121 | syncTxt, err := utils.NewSyncTxt(".pikpaksync.txt", sync)
122 | if err != nil {
123 | logrus.Errorln(err)
124 | return
125 | }
126 | defer syncTxt.Close()
127 |
128 | uploadFilePath = syncTxt.UnSync(uploadFilePath)
129 |
130 | logrus.Info("upload file list:")
131 | for _, f := range uploadFilePath {
132 | logrus.Infoln(filepath.Join(basePath, f))
133 | }
134 |
135 | if parentId == "" {
136 | parentId, err = p.GetDeepFolderOrCreateId("", uploadFolder)
137 | if err != nil {
138 | logrus.Errorf("get folder %s id error: %s", uploadFolder, err)
139 | return
140 | }
141 | }
142 |
143 | logrus.Debug("upload folder: ", uploadFolder, " parentId: ", parentId)
144 |
145 | parentId, err = p.GetDeepFolderOrCreateId(parentId, basePath)
146 | if err != nil {
147 | logrus.Errorf("get base_upload_path %s id error: %s", basePath, err)
148 | return
149 | }
150 | parentIdMap := make(map[string]string)
151 | for _, v := range uploadFilePath {
152 | if strings.Contains(v, "/") || strings.Contains(v, "\\") {
153 | var id string
154 | base := filepath.Dir(v)
155 |
156 | // Avoid secondary query ids
157 | if mId, ok := parentIdMap[base]; !ok {
158 | id, err = p.GetDeepFolderOrCreateId(parentId, base)
159 | if err != nil {
160 | logrus.Error(err)
161 | }
162 | parentIdMap[base] = id
163 | } else {
164 | id = mId
165 | }
166 |
167 | err = p.UploadFile(id, filepath.Join(path, v))
168 | if err != nil {
169 | logrus.Error(err)
170 | }
171 | syncTxt.WriteString(v + "\n")
172 | logrus.Infof("%s upload success!\n", v)
173 | } else {
174 | err = p.UploadFile(parentId, filepath.Join(path, v))
175 | if err != nil {
176 | logrus.Error(err)
177 | }
178 | syncTxt.WriteString(v + "\n")
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/52funny/pikpakhash v0.0.0-20231104025731-ef91a56eff9c h1:ecJG8tmvgH6exVE4+I3rFPPA1Mk3/lNb8VZ6A7dtcyI=
2 | github.com/52funny/pikpakhash v0.0.0-20231104025731-ef91a56eff9c/go.mod h1:YA/IS8XUrMTcrY+J4yOJ3CDgoyQ28NOOo4GnzOL6bTI=
3 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
4 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
5 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
6 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
12 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
13 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
14 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
15 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
16 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
17 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
18 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
19 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
20 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
21 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
22 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
23 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
24 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
25 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
27 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
28 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
32 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
33 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
34 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
35 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
36 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
37 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
38 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
39 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
40 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
41 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
42 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
43 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
44 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
45 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
46 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
47 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
48 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
49 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
50 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
51 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
52 | github.com/vbauerster/mpb/v8 v8.7.2 h1:SMJtxhNho1MV3OuFgS1DAzhANN1Ejc5Ct+0iSaIkB14=
53 | github.com/vbauerster/mpb/v8 v8.7.2/go.mod h1:ZFnrjzspgDHoxYLGvxIruiNk73GNTPG4YHgVNpR10VY=
54 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
55 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
56 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
57 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
60 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
61 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
63 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
65 |
--------------------------------------------------------------------------------
/internal/pikpak/file.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "net/http"
7 | "net/url"
8 | "time"
9 | "unsafe"
10 |
11 | "github.com/tidwall/gjson"
12 | )
13 |
14 | type FileStat struct {
15 | Kind string `json:"kind"`
16 | ID string `json:"id"`
17 | ParentID string `json:"parent_id"`
18 | Name string `json:"name"`
19 | UserID string `json:"user_id"`
20 | Size string `json:"size"`
21 | FileExtension string `json:"file_extension"`
22 | MimeType string `json:"mime_type"`
23 | CreatedTime time.Time `json:"created_time"`
24 | ModifiedTime time.Time `json:"modified_time"`
25 | IconLink string `json:"icon_link"`
26 | ThumbnailLink string `json:"thumbnail_link"`
27 | Md5Checksum string `json:"md5_checksum"`
28 | Hash string `json:"hash"`
29 | Phase string `json:"phase"`
30 | }
31 | type File struct {
32 | FileStat
33 | Revision string `json:"revision"`
34 | Starred bool `json:"starred"`
35 | WebContentLink string `json:"web_content_link"`
36 | Links struct {
37 | ApplicationOctetStream struct {
38 | URL string `json:"url"`
39 | Token string `json:"token"`
40 | Expire time.Time `json:"expire"`
41 | } `json:"application/octet-stream"`
42 | } `json:"links"`
43 | Audit struct {
44 | Status string `json:"status"`
45 | Message string `json:"message"`
46 | Title string `json:"title"`
47 | } `json:"audit"`
48 | Medias []struct {
49 | MediaID string `json:"media_id"`
50 | MediaName string `json:"media_name"`
51 | Video interface{} `json:"video"`
52 | Link struct {
53 | URL string `json:"url"`
54 | Token string `json:"token"`
55 | Expire time.Time `json:"expire"`
56 | } `json:"link"`
57 | NeedMoreQuota bool `json:"need_more_quota"`
58 | VipTypes []interface{} `json:"vip_types"`
59 | RedirectLink string `json:"redirect_link"`
60 | IconLink string `json:"icon_link"`
61 | IsDefault bool `json:"is_default"`
62 | Priority int `json:"priority"`
63 | IsOrigin bool `json:"is_origin"`
64 | ResolutionName string `json:"resolution_name"`
65 | IsVisible bool `json:"is_visible"`
66 | Category string `json:"category"`
67 | } `json:"medias"`
68 | Trashed bool `json:"trashed"`
69 | DeleteTime string `json:"delete_time"`
70 | OriginalURL string `json:"original_url"`
71 | Params struct {
72 | Platform string `json:"platform"`
73 | PlatformIcon string `json:"platform_icon"`
74 | } `json:"params"`
75 | OriginalFileIndex int `json:"original_file_index"`
76 | Space string `json:"space"`
77 | Apps []interface{} `json:"apps"`
78 | Writable bool `json:"writable"`
79 | FolderType string `json:"folder_type"`
80 | Collection interface{} `json:"collection"`
81 | }
82 |
83 | type fileListResult struct {
84 | NextPageToken string `json:"next_page_token"`
85 | Files []FileStat `json:"files"`
86 | }
87 |
88 | func (p *PikPak) GetFolderFileStatList(parentId string) ([]FileStat, error) {
89 | filters := `{"trashed":{"eq":false}}`
90 | query := url.Values{}
91 | query.Add("thumbnail_size", "SIZE_MEDIUM")
92 | query.Add("limit", "500")
93 | query.Add("parent_id", parentId)
94 | query.Add("with_audit", "false")
95 | query.Add("filters", filters)
96 | fileList := make([]FileStat, 0)
97 |
98 | for {
99 | req, err := http.NewRequest("GET", "https://api-drive.mypikpak.com/drive/v1/files?"+query.Encode(), nil)
100 | if err != nil {
101 | return fileList, err
102 | }
103 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
104 | req.Header.Set("Content-Type", "application/json")
105 | bs, err := p.sendRequest(req)
106 | if err != nil {
107 | return fileList, err
108 | }
109 | error_code := gjson.Get(*(*string)(unsafe.Pointer(&bs)), "error_code").Int()
110 | if error_code == 9 {
111 | err = p.AuthCaptchaToken("GET:/drive/v1/files")
112 | if err != nil {
113 | return fileList, err
114 | }
115 | }
116 | var result fileListResult
117 | err = json.Unmarshal(bs, &result)
118 | if err != nil {
119 | return fileList, err
120 | }
121 | fileList = append(fileList, result.Files...)
122 | if result.NextPageToken == "" {
123 | break
124 | }
125 | query.Set("page_token", result.NextPageToken)
126 | }
127 | return fileList, nil
128 | }
129 |
130 | // Find FileState similar to name in the parentId directory
131 | func (p *PikPak) GetFileStat(parentId string, name string) (FileStat, error) {
132 | stats, err := p.GetFolderFileStatList(parentId)
133 | if err != nil {
134 | return FileStat{}, err
135 | }
136 | for _, stat := range stats {
137 | if stat.Name == name {
138 | return stat, nil
139 | }
140 | }
141 | return FileStat{}, errors.New("file not found")
142 | }
143 |
144 | func (p *PikPak) GetFile(fileId string) (File, error) {
145 | var fileInfo File
146 | query := url.Values{}
147 | query.Add("thumbnail_size", "SIZE_MEDIUM")
148 | req, err := http.NewRequest("GET", "https://api-drive.mypikpak.com/drive/v1/files/"+fileId+"?"+query.Encode(), nil)
149 | if err != nil {
150 | return fileInfo, nil
151 | }
152 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
153 | req.Header.Set("X-Device-Id", p.DeviceId)
154 | bs, err := p.sendRequest(req)
155 | if err != nil {
156 | return fileInfo, nil
157 | }
158 |
159 | error_code := gjson.Get(*(*string)(unsafe.Pointer(&bs)), "error_code").Int()
160 | if error_code != 0 {
161 | if error_code == 9 {
162 | err = p.AuthCaptchaToken("GET:/drive/v1/files")
163 | if err != nil {
164 | return fileInfo, err
165 | }
166 | }
167 | err = errors.New(gjson.Get(*(*string)(unsafe.Pointer(&bs)), "error").String() + ":" + fileId)
168 | return fileInfo, err
169 | }
170 | err = json.Unmarshal(bs, &fileInfo)
171 | if err != nil {
172 | return fileInfo, err
173 | }
174 | return fileInfo, err
175 | }
176 |
--------------------------------------------------------------------------------
/cmd/download/download.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "strconv"
7 | "unicode/utf8"
8 |
9 | "github.com/52funny/pikpakcli/conf"
10 | "github.com/52funny/pikpakcli/internal/pikpak"
11 | "github.com/52funny/pikpakcli/internal/utils"
12 | "github.com/sirupsen/logrus"
13 | "github.com/spf13/cobra"
14 | "github.com/vbauerster/mpb/v8"
15 | "github.com/vbauerster/mpb/v8/decor"
16 | )
17 |
18 | var DownloadCmd = &cobra.Command{
19 | Use: "download",
20 | Aliases: []string{"d"},
21 | Short: `Download file from pikpak server`,
22 | Run: func(cmd *cobra.Command, args []string) {
23 | p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password)
24 | err := p.Login()
25 | if err != nil {
26 | logrus.Errorln("Login Failed:", err)
27 | }
28 | if len(args) > 0 {
29 | downloadFile(&p, args)
30 | } else {
31 | downloadFolder(&p)
32 | }
33 | },
34 | }
35 |
36 | // Number of simultaneous downloads
37 | //
38 | // default 1
39 | var count int
40 |
41 | // Specifies the folder of the pikpak server
42 | //
43 | // default server root directory (.)
44 | var folder string
45 |
46 | // parent path id
47 | var parentId string
48 |
49 | // Output directory
50 | //
51 | // default current directory (.)
52 | var output string
53 |
54 | // Progress bar
55 | //
56 | // default false
57 | var progress bool
58 |
59 | type warpFile struct {
60 | f *pikpak.File
61 | output string
62 | }
63 |
64 | type warpStat struct {
65 | s pikpak.FileStat
66 | output string
67 | }
68 |
69 | func init() {
70 | DownloadCmd.Flags().IntVarP(&count, "count", "c", 1, "number of simultaneous downloads")
71 | DownloadCmd.Flags().StringVarP(&output, "output", "o", ".", "output directory")
72 | DownloadCmd.Flags().StringVarP(&folder, "path", "p", "/", "specific the folder of the pikpak server\nonly support download folder")
73 | DownloadCmd.Flags().StringVarP(&parentId, "parent-id", "P", "", "the parent path id")
74 | DownloadCmd.Flags().BoolVarP(&progress, "progress", "g", false, "show download progress")
75 | }
76 |
77 | // Downloads all files in the specified directory
78 | func downloadFolder(p *pikpak.PikPak) {
79 | base := filepath.Base(folder)
80 | var err error
81 | if parentId == "" {
82 | parentId, err = p.GetPathFolderId(folder)
83 | if err != nil {
84 | logrus.Errorln("Get Parent Folder Id Failed:", err)
85 | return
86 | }
87 |
88 | }
89 | collectStat := make([]warpStat, 0)
90 | recursive(p, &collectStat, parentId, filepath.Join(output, base))
91 |
92 | statCh := make(chan warpStat, len(collectStat))
93 | statDone := make(chan struct{})
94 |
95 | fileCh := make(chan warpFile, len(collectStat))
96 | fileDone := make(chan struct{})
97 |
98 | for i := 0; i < 4; i += 1 {
99 | go func(fileCh chan<- warpFile, statCh <-chan warpStat, statDone chan<- struct{}) {
100 | for {
101 | stat, ok := <-statCh
102 | if !ok {
103 | break
104 | }
105 | file, err := p.GetFile(stat.s.ID)
106 | if err != nil {
107 | logrus.Errorln("Get File Failed:", err)
108 | }
109 | fileCh <- warpFile{
110 | f: &file,
111 | output: stat.output,
112 | }
113 | statDone <- struct{}{}
114 | }
115 | }(fileCh, statCh, statDone)
116 | }
117 |
118 | if progress {
119 | pb := mpb.New(mpb.WithAutoRefresh())
120 | for i := 0; i < count; i++ {
121 | // if progress is true then show progress bar
122 | go download(fileCh, fileDone, pb)
123 | }
124 | } else {
125 | go download(fileCh, fileDone, nil)
126 | }
127 |
128 | for i := 0; i < len(collectStat); i += 1 {
129 | err := utils.CreateDirIfNotExist(collectStat[i].output)
130 | if err != nil {
131 | logrus.Errorln("Create output directory failed:", err)
132 | return
133 | }
134 | statCh <- collectStat[i]
135 | }
136 | close(statCh)
137 |
138 | for i := 0; i < len(collectStat); i += 1 {
139 | <-statDone
140 | }
141 | close(statDone)
142 |
143 | for i := 0; i < len(collectStat); i += 1 {
144 | <-fileDone
145 | }
146 | }
147 |
148 | func recursive(p *pikpak.PikPak, collectWarpFile *[]warpStat, parentId string, parentPath string) {
149 | statList, err := p.GetFolderFileStatList(parentId)
150 | if err != nil {
151 | logrus.Errorln("Get Folder File Stat List Failed:", err)
152 | return
153 | }
154 | for _, r := range statList {
155 | if r.Kind == "drive#folder" {
156 | recursive(p, collectWarpFile, r.ID, filepath.Join(parentPath, r.Name))
157 | } else {
158 | // file, _ := p.GetFile(r.ID)
159 | *collectWarpFile = append(*collectWarpFile, warpStat{
160 | s: r,
161 | output: parentPath,
162 | })
163 | // fmt.Println(r.Name, r.Size, r.Kind, parentPath)
164 | }
165 | }
166 | }
167 |
168 | func downloadFile(p *pikpak.PikPak, args []string) {
169 | var err error
170 | if parentId == "" {
171 | parentId, err = p.GetPathFolderId(folder)
172 | if err != nil {
173 | logrus.Errorln("get folder failed:", err)
174 | return
175 | }
176 | }
177 |
178 | // if output not exists then create.
179 | if err := utils.CreateDirIfNotExist(output); err != nil {
180 | logrus.Errorln("Create output directory failed:", err)
181 | return
182 | }
183 |
184 | sendCh := make(chan warpFile, 1)
185 | receiveCh := make(chan struct{}, len(args))
186 |
187 | if progress {
188 | pb := mpb.New(mpb.WithAutoRefresh())
189 | for i := 0; i < count; i++ {
190 | // if progress is true then show progress bar
191 | go download(sendCh, receiveCh, pb)
192 | }
193 | } else {
194 | go download(sendCh, receiveCh, nil)
195 | }
196 |
197 | for i := 0; i < count; i++ {
198 | // if progress is true then show progress bar
199 | switch progress {
200 | case true:
201 | go download(sendCh, receiveCh, mpb.New(mpb.WithAutoRefresh()))
202 | case false:
203 | go download(sendCh, receiveCh, nil)
204 | }
205 | }
206 | for _, path := range args {
207 | stat, err := p.GetFileStat(parentId, path)
208 | if err != nil {
209 | logrus.Errorln(path, "get parent id failed:", err)
210 | continue
211 | }
212 |
213 | file, err := p.GetFile(stat.ID)
214 | if err != nil {
215 | logrus.Errorln(path, "get file failed", err)
216 | continue
217 | }
218 | sendCh <- warpFile{
219 | f: &file,
220 | output: output,
221 | }
222 | }
223 | close(sendCh)
224 | for i := 0; i < len(args); i++ {
225 | <-receiveCh
226 | }
227 | close(receiveCh)
228 | }
229 |
230 | func download(inCh <-chan warpFile, out chan<- struct{}, pb *mpb.Progress) {
231 | for {
232 | warp, ok := <-inCh
233 | if !ok {
234 | break
235 | }
236 |
237 | path := filepath.Join(warp.output, warp.f.Name)
238 | exist, err := utils.Exists(path)
239 | if err != nil {
240 | // logrus.Errorln("Access", path, "Failed:", err)
241 | out <- struct{}{}
242 | continue
243 | }
244 | flag := path + ".pikpakclidownload"
245 | hasFlag, err := utils.Exists(flag)
246 | if err != nil {
247 | // logrus.Errorln("Access", flag, "Failed:", err)
248 | out <- struct{}{}
249 | continue
250 | }
251 | if exist && !hasFlag {
252 | // logrus.Infoln("Skip downloaded file", warp.f.Name)
253 | out <- struct{}{}
254 | continue
255 | }
256 | err = utils.TouchFile(flag)
257 | if err != nil {
258 | // logrus.Errorln("Create flag file", flag, "Failed:", err)
259 | out <- struct{}{}
260 | continue
261 | }
262 |
263 | siz, err := strconv.ParseInt(warp.f.Size, 10, 64)
264 | if err != nil {
265 | // logrus.Errorln("Parse File size", warp.f.Size, "Failed:", err)
266 | out <- struct{}{}
267 | continue
268 | }
269 |
270 | var bar *mpb.Bar = nil
271 |
272 | // This is simple way to display part names
273 | // It may be replaced one day.
274 | trimeName := func(name string) string {
275 | // Fixed value, which is an unwise approach.
276 | maxLen := 30
277 | if utf8.RuneCountInString(name)+3 > maxLen {
278 | return name[:maxLen-3] + "..."
279 | }
280 | return name
281 | }
282 |
283 | if pb != nil {
284 | bar = pb.AddBar(siz,
285 | mpb.PrependDecorators(
286 | decor.Name(trimeName(warp.f.Name)),
287 | decor.Percentage(decor.WCSyncSpace),
288 | ),
289 | mpb.AppendDecorators(
290 | decor.EwmaETA(decor.ET_STYLE_GO, 30),
291 | decor.Name(" ] "),
292 | decor.EwmaSpeed(decor.SizeB1024(0), "% .2f", 60),
293 | ),
294 | )
295 | }
296 |
297 | // start downloading
298 | err = warp.f.Download(path, bar)
299 | // if hasn't error then remove flag file
300 | if err == nil {
301 | if pb == nil {
302 | logrus.Infoln("Download", warp.f.Name, "Success")
303 | }
304 | os.Remove(flag)
305 | } else {
306 | if pb == nil {
307 | logrus.Errorln("Download", warp.f.Name, "Failed:", err)
308 | }
309 | }
310 | if bar != nil {
311 | bar.Abort(true)
312 | }
313 | out <- struct{}{}
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/internal/pikpak/upload.go:
--------------------------------------------------------------------------------
1 | package pikpak
2 |
3 | import (
4 | "bytes"
5 | "crypto/hmac"
6 | "crypto/sha1"
7 | "encoding/base64"
8 | "encoding/xml"
9 | "errors"
10 | "fmt"
11 | "io"
12 | "math"
13 | "net/http"
14 | "net/url"
15 | "os"
16 | "path/filepath"
17 | "sort"
18 | "strconv"
19 | "strings"
20 | "sync"
21 | "time"
22 |
23 | "github.com/52funny/pikpakhash"
24 | jsoniter "github.com/json-iterator/go"
25 | "github.com/sirupsen/logrus"
26 | )
27 |
28 | type OssArgs struct {
29 | Bucket string `json:"bucket"`
30 | AccessKeyId string `json:"access_key_id"`
31 | AccessKeySecret string `json:"access_key_secret"`
32 | EndPoint string `json:"endpoint"`
33 | Key string `json:"key"`
34 | SecurityToken string `json:"security_token"`
35 | }
36 |
37 | // 256k
38 | var defaultChunkSize int64 = 1 << 18
39 | var Concurrent int64 = 1 << 4
40 |
41 | var ErrNotFoundFolder = errors.New("not found pikpak folder")
42 |
43 | func (p *PikPak) UploadFile(parentId, path string) error {
44 | fileName := filepath.Base(path)
45 | fileState, err := os.Stat(path)
46 | if err != nil {
47 | return err
48 | }
49 | fileSize := fileState.Size()
50 | ph := pikpakhash.Default()
51 | hash, err := ph.HashFromPath(path)
52 | if err != nil {
53 | return err
54 | }
55 | m := map[string]interface{}{
56 | "body": map[string]string{
57 | "duration": "",
58 | "width": "",
59 | "height": "",
60 | },
61 | "kind": "drive#file",
62 | "name": fileName,
63 | "size": fmt.Sprintf("%d", fileSize),
64 | "hash": hash,
65 | "upload_type": "UPLOAD_TYPE_RESUMABLE",
66 | "objProvider": map[string]string{
67 | "provider": "UPLOAD_TYPE_UNKNOWN",
68 | },
69 | }
70 | if parentId != "" {
71 | m["parent_id"] = parentId
72 | }
73 | bs, err := jsoniter.Marshal(&m)
74 | if err != nil {
75 | return err
76 | }
77 | START:
78 | req, err := http.NewRequest("POST", "https://api-drive.mypikpak.com/drive/v1/files", bytes.NewBuffer(bs))
79 | if err != nil {
80 | return err
81 | }
82 | req.Header.Set("Content-Type", "application/json; charset=utf-8")
83 | req.Header.Set("Product_flavor_name", "cha")
84 | req.Header.Set("X-Captcha-Token", p.CaptchaToken)
85 | req.Header.Set("X-Client-Version-Code", "10083")
86 | req.Header.Set("X-Peer-Id", p.DeviceId)
87 | req.Header.Set("X-User-Region", "1")
88 | req.Header.Set("X-Alt-Capability", "3")
89 | req.Header.Set("Country", "CN")
90 | bs, err = p.sendRequest(req)
91 | if err != nil {
92 | return err
93 | }
94 | error_code := jsoniter.Get(bs, "error_code").ToInt()
95 | if error_code != 0 {
96 | if error_code == 9 {
97 | err = p.AuthCaptchaToken("POST:/drive/v1/files")
98 | if err != nil {
99 | return err
100 | }
101 | goto START
102 | }
103 | return fmt.Errorf("upload file error: %s", jsoniter.Get(bs, "error").ToString())
104 | }
105 | // logrus.Debug(string(bs))
106 | file := jsoniter.Get(bs, "file")
107 | phase := file.Get("phase").ToString()
108 | logrus.Debug("path: ", path, " phase: ", phase)
109 |
110 | switch phase {
111 | case "PHASE_TYPE_COMPLETE":
112 | logrus.Debug(path, " upload file complete")
113 | return nil
114 | case "PHASE_TYPE_PENDING":
115 | // break switch
116 | break
117 | }
118 | params := jsoniter.Get(bs, "resumable").Get("params")
119 |
120 | accessKeyId := params.Get("access_key_id").ToString()
121 | accessKeySecret := params.Get("access_key_secret").ToString()
122 | bucket := params.Get("bucket").ToString()
123 | endpoint := params.Get("endpoint").ToString()
124 | key := params.Get("key").ToString()
125 | securityToken := params.Get("security_token").ToString()
126 | ossArgs := OssArgs{
127 | Bucket: bucket,
128 | AccessKeyId: accessKeyId,
129 | AccessKeySecret: accessKeySecret,
130 | EndPoint: endpoint,
131 | Key: key,
132 | SecurityToken: securityToken,
133 | }
134 |
135 | uploadId := p.beforeUpload(ossArgs)
136 |
137 | f, err := os.Open(path)
138 | if err != nil {
139 | return err
140 | }
141 | defer f.Close()
142 |
143 | wait := new(sync.WaitGroup)
144 | in_wait := new(sync.WaitGroup)
145 | ch := make(chan Part, Concurrent)
146 |
147 | var chunkSize = int64(math.Ceil(float64(fileSize) / 10000))
148 | if chunkSize < defaultChunkSize {
149 | chunkSize = defaultChunkSize
150 | }
151 |
152 | for i := int64(0); i < Concurrent; i++ {
153 | wait.Add(1)
154 | go uploadChunk(wait, ch, f, chunkSize, fileSize, i, ossArgs, uploadId)
155 | }
156 | donePartSlice := make([]Part, 0)
157 | in_wait.Add(1)
158 | go func() {
159 | defer in_wait.Done()
160 | for p := range ch {
161 | donePartSlice = append(donePartSlice, p)
162 | }
163 | }()
164 | wait.Wait()
165 | close(ch)
166 | in_wait.Wait()
167 | sort.Slice(donePartSlice, func(i, j int) bool {
168 | iNum, _ := strconv.Atoi(donePartSlice[i].PartNumber)
169 | jNum, _ := strconv.Atoi(donePartSlice[j].PartNumber)
170 | return iNum < jNum
171 | })
172 | args := CompleteMultipartUpload{
173 | Part: donePartSlice,
174 | }
175 | err = p.afterUpload(&args, ossArgs, uploadId)
176 | if err != nil {
177 | return err
178 | }
179 | return nil
180 | }
181 |
182 | func uploadChunk(wait *sync.WaitGroup, ch chan Part, f *os.File, ChunkSize, fileSize int64, part int64, ossArgs OssArgs, uploadId string) {
183 | defer wait.Done()
184 | if part*ChunkSize >= fileSize {
185 | return
186 | }
187 | buf := make([]byte, ChunkSize)
188 | var offset = part * ChunkSize
189 | for offset < fileSize {
190 | n, _ := f.ReadAt(buf, offset)
191 | // if err != nil {
192 | // // logrus.Error(err)
193 | // }
194 | if n > 0 {
195 | value := url.Values{}
196 | value.Add("uploadId", uploadId)
197 | value.Add("partNumber", fmt.Sprintf("%d", part+1))
198 | req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s?%s",
199 | ossArgs.EndPoint,
200 | ossArgs.Key,
201 | value.Encode()), bytes.NewBuffer(buf[:n]))
202 | if err != nil {
203 | continue
204 | }
205 |
206 | now := time.Now().UTC()
207 | req.Header.Set("Content-Type", "application/octet-stream")
208 | req.Header.Set("X-OSS-Security-Token", ossArgs.SecurityToken)
209 | req.Header.Set("Date", now.Format(http.TimeFormat))
210 | req.Header.Set("Authorization", "OSS "+ossArgs.AccessKeyId+":"+hmacAuthorization(req, nil, now, ossArgs))
211 | resp, err := http.DefaultClient.Do(req)
212 | if err != nil {
213 | continue
214 | }
215 | // bs, _ := io.ReadAll(resp.Body)
216 | eTag := strings.Trim(resp.Header.Get("ETag"), "\"")
217 | p := Part{
218 | PartNumber: fmt.Sprintf("%d", part+1),
219 | ETag: eTag,
220 | }
221 | ch <- p
222 | resp.Body.Close()
223 | }
224 | part = part + Concurrent
225 | offset = part * ChunkSize
226 | }
227 | }
228 |
229 | type header struct {
230 | key string
231 | val string
232 | }
233 |
234 | type CompleteMultipartUpload struct {
235 | Part []Part `xml:"Part"`
236 | }
237 | type Part struct {
238 | PartNumber string `xml:"PartNumber"`
239 | ETag string `xml:"ETag"`
240 | }
241 |
242 | func hmacAuthorization(req *http.Request, body []byte, time time.Time, ossArgs OssArgs) string {
243 | date := time.UTC().Format(http.TimeFormat)
244 | stringBuilder := new(strings.Builder)
245 | stringBuilder.WriteString(req.Method + "\n")
246 | if body == nil {
247 | stringBuilder.WriteString("\n")
248 | } else {
249 | // digest := md5.New()
250 | // digest.Write(body)
251 | // sign := base64.StdEncoding.EncodeToString(digest.Sum(nil))
252 | // stringBuilder.WriteString(sign + "\n")
253 | stringBuilder.WriteString("\n")
254 | }
255 | stringBuilder.WriteString(req.Header.Get("Content-Type") + "\n")
256 | stringBuilder.WriteString(date + "\n")
257 |
258 | headerSlice := make([]header, 0)
259 | for k, v := range req.Header {
260 | headerK := strings.ToLower(k)
261 | if strings.Contains(headerK, "x-oss-") && len(v) > 0 {
262 | headerSlice = append(headerSlice, header{headerK, v[0]})
263 | }
264 | }
265 |
266 | // 从小到大排序
267 | sort.Slice(headerSlice, func(i, j int) bool {
268 | return headerSlice[i].key < headerSlice[j].key
269 | })
270 | for _, hd := range headerSlice {
271 | stringBuilder.WriteString(hd.key + ":" + hd.val + "\n")
272 | }
273 |
274 | stringBuilder.WriteString("/" + ossArgs.Bucket + req.URL.Path + "?" + req.URL.RawQuery)
275 |
276 | h := hmac.New(sha1.New, []byte(ossArgs.AccessKeySecret))
277 | h.Write([]byte(stringBuilder.String()))
278 | return base64.StdEncoding.EncodeToString(h.Sum(nil))
279 | }
280 |
281 | func (p *PikPak) beforeUpload(ossArgs OssArgs) string {
282 | req, err := http.NewRequest("POST", "https://"+ossArgs.EndPoint+"/"+ossArgs.Key+"?uploads", nil)
283 | if err != nil {
284 | return ""
285 | }
286 | time := time.Now().UTC()
287 | req.Header.Set("Date", time.Format(http.TimeFormat))
288 | req.Header.Set("Content-Type", "application/octet-stream")
289 | req.Header.Set("User-Agent", "aliyun-sdk-android/2.9.5(Linux/Android 11/ONEPLUS%20A6000;RKQ1.201217.002)")
290 | req.Header.Set("X-Oss-Security-Token", ossArgs.SecurityToken)
291 | req.Header.Set("Authorization",
292 | fmt.Sprintf("%s %s:%s",
293 | "OSS",
294 | ossArgs.AccessKeyId,
295 | hmacAuthorization(req, nil, time, ossArgs),
296 | ))
297 |
298 | resp, err := http.DefaultClient.Do(req)
299 | if err != nil {
300 | return ""
301 | }
302 | defer resp.Body.Close()
303 | bs, err := io.ReadAll(resp.Body)
304 | if err != nil {
305 | return ""
306 | }
307 | type InitiateMultipartUploadResult struct {
308 | Bucket string `xml:"Bucket"`
309 | Key string `xml:"Key"`
310 | UploadId string `xml:"UploadId"`
311 | }
312 | res := new(InitiateMultipartUploadResult)
313 |
314 | err = xml.Unmarshal(bs, res)
315 | if err != nil {
316 | return ""
317 | }
318 | return res.UploadId
319 | }
320 |
321 | func (p *PikPak) afterUpload(args *CompleteMultipartUpload, ossArgs OssArgs, uploadId string) error {
322 | bs, err := xml.Marshal(args)
323 | if err != nil {
324 | return err
325 | }
326 | req, err := http.NewRequest("POST", "https://"+ossArgs.EndPoint+"/"+ossArgs.Key+"?uploadId="+uploadId, bytes.NewBuffer(bs))
327 | if err != nil {
328 | return err
329 | }
330 | time := time.Now().UTC()
331 | req.Header.Set("Date", time.Format(http.TimeFormat))
332 | req.Header.Set("Content-Type", "application/octet-stream")
333 | req.Header.Set("User-Agent", "aliyun-sdk-android/2.9.5(Linux/Android 11/ONEPLUS%20A6000;RKQ1.201217.002)")
334 | req.Header.Set("X-Oss-Security-Token", ossArgs.SecurityToken)
335 | req.Header.Set("Authorization",
336 | fmt.Sprintf("%s %s:%s",
337 | "OSS",
338 | ossArgs.AccessKeyId,
339 | hmacAuthorization(req, nil, time, ossArgs),
340 | ))
341 |
342 | resp, err := http.DefaultClient.Do(req)
343 | if err != nil {
344 | return err
345 | }
346 | defer resp.Body.Close()
347 | _, err = io.ReadAll(resp.Body)
348 | if err != nil {
349 | return err
350 | }
351 | return nil
352 | }
353 |
--------------------------------------------------------------------------------