From c64c003257c267edd0f314c1d8efbfac870e96d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E5=87=89?= <927625802@qq.com> Date: Thu, 16 Dec 2021 22:50:23 +0800 Subject: [PATCH] :sparkler: feat: pikpak support --- README.md | 2 + README_cn.md | 2 + drivers/all.go | 1 + drivers/base/driver.go | 1 + drivers/base/types.go | 8 ++ drivers/pikpak/driver.go | 240 +++++++++++++++++++++++++++++++++++++++ drivers/pikpak/pikpak.go | 178 +++++++++++++++++++++++++++++ server/webdav/file.go | 1 + 8 files changed, 433 insertions(+) create mode 100644 drivers/pikpak/driver.go create mode 100644 drivers/pikpak/pikpak.go diff --git a/README.md b/README.md index 6de3395e..006447fb 100755 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ English | [中文](./README_cn.md) - [x] [123pan](https://www.123pan.com/) - [x] [lanzou](https://pc.woozooo.com/) - [x] [Alist](https://github.com/Xhofe/alist) + - [x] FTP + - [x] [PikPak](https://www.mypikpak.com/) - [x] File preview (PDF, markdown, code, plain text, ...) - [x] Image preview in gallery mode - [x] Video and audio preview (mp4, mp3, ...) diff --git a/README_cn.md b/README_cn.md index 8b693466..df36005d 100644 --- a/README_cn.md +++ b/README_cn.md @@ -26,6 +26,8 @@ - [x] [123云盘](https://www.123pan.com/) - [x] [蓝奏云](https://pc.woozooo.com/) - [x] [Alist](https://github.com/Xhofe/alist) + - [x] FTP + - [x] [PikPak](https://www.mypikpak.com/) - [x] 文件预览(PDF、markdown、代码、纯文本……) - [x] 画廊模式下的图像预览 - [x] 视频和音频预览(mp4、mp3 等) diff --git a/drivers/all.go b/drivers/all.go index 744919cc..66ffb214 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -10,4 +10,5 @@ import ( _ "github.com/Xhofe/alist/drivers/lanzou" _ "github.com/Xhofe/alist/drivers/native" _ "github.com/Xhofe/alist/drivers/onedrive" + _ "github.com/Xhofe/alist/drivers/pikpak" ) diff --git a/drivers/base/driver.go b/drivers/base/driver.go index c3ef458a..cd4270aa 100644 --- a/drivers/base/driver.go +++ b/drivers/base/driver.go @@ -106,4 +106,5 @@ func init() { }), ) NoRedirectClient.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") + RestyClient.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") } diff --git a/drivers/base/types.go b/drivers/base/types.go index 567c0586..102d8fff 100644 --- a/drivers/base/types.go +++ b/drivers/base/types.go @@ -19,6 +19,14 @@ const ( TypeNumber = "number" ) +const ( + Get = iota + Post + Put + Delete + Patch +) + type Json map[string]interface{} type TokenResp struct { diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go new file mode 100644 index 00000000..76ddffa1 --- /dev/null +++ b/drivers/pikpak/driver.go @@ -0,0 +1,240 @@ +package pikpak + +import ( + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "path/filepath" +) + +type PikPak struct{} + +func (driver PikPak) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "PikPak", + } +} + +func (driver PikPak) Items() []base.Item { + return []base.Item{ + { + Name: "username", + Label: "username", + Type: base.TypeString, + Required: true, + }, + { + Name: "password", + Label: "password", + Type: base.TypeString, + Required: true, + }, + { + Name: "root_folder", + Label: "root folder id", + Type: base.TypeString, + Required: false, + }, + } +} + +func (driver PikPak) Save(account *model.Account, old *model.Account) error { + err := driver.Login(account) + _ = model.SaveAccount(account) + return err +} + +func (driver PikPak) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: account.RootFolder, + Name: account.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: account.UpdatedAt, + }, nil + } + dir, name := filepath.Split(path) + files, err := driver.Files(dir, account) + if err != nil { + return nil, err + } + for _, file := range files { + if file.Name == name { + return &file, nil + } + } + return nil, base.ErrPathNotFound +} + +func (driver PikPak) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + var files []model.File + cache, err := base.GetCache(path, account) + if err == nil { + files, _ = cache.([]model.File) + } else { + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + rawFiles, err := driver.GetFiles(file.Id, account) + if err != nil { + return nil, err + } + files = make([]model.File, 0) + for _, file := range rawFiles { + files = append(files, *driver.FormatFile(&file)) + } + if len(files) > 0 { + _ = base.SetCache(path, files, account) + } + } + return files, nil +} + +func (driver PikPak) Link(path string, account *model.Account) (*base.Link, error) { + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + var resp File + _, err = driver.Request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.Id), + base.Get, nil, nil, &resp, account) + if err != nil { + return nil, err + } + return &base.Link{ + Url: resp.WebContentLink, + }, nil +} + +func (driver PikPak) Path(path string, account *model.Account) (*model.File, []model.File, error) { + path = utils.ParsePath(path) + log.Debugf("pikpak path: %s", path) + file, err := driver.File(path, account) + if err != nil { + return nil, nil, err + } + if !file.IsDir() { + link, err := driver.Link(path, account) + if err != nil { + return nil, nil, err + } + file.Url = link.Url + return file, nil, nil + } + files, err := driver.Files(path, account) + if err != nil { + return nil, nil, err + } + return nil, files, nil +} + +func (driver PikPak) Proxy(c *gin.Context, account *model.Account) { + +} + +func (driver PikPak) Preview(path string, account *model.Account) (interface{}, error) { + return nil, base.ErrNotSupport +} + +func (driver PikPak) MakeDir(path string, account *model.Account) error { + path = utils.ParsePath(path) + dir, name := filepath.Split(path) + parentFile, err := driver.File(dir, account) + if err != nil { + return err + } + if !parentFile.IsDir() { + return base.ErrNotFolder + } + _, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Post, nil, &base.Json{ + "kind": "drive#folder", + "parent_id": parentFile.Id, + "name": name, + }, nil, account) + if err == nil { + _ = base.DeleteCache(dir, account) + } + return err +} + +func (driver PikPak) Move(src string, dst string, account *model.Account) error { + srcDir, _ := filepath.Split(src) + dstDir, dstName := filepath.Split(dst) + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + // rename + if srcDir == dstDir { + _, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files/"+srcFile.Id, base.Patch, nil, &base.Json{ + "name": dstName, + }, nil, account) + } else { + // move + dstDirFile, err := driver.File(dstDir, account) + if err != nil { + return err + } + _, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchMove", base.Post, nil, &base.Json{ + "ids": []string{srcFile.Id}, + "to": base.Json{ + "parent_id": dstDirFile.Id, + }, + }, nil, account) + } + if err == nil { + _ = base.DeleteCache(srcDir, account) + _ = base.DeleteCache(dstDir, account) + } + return err +} + +func (driver PikPak) Copy(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + dstDirFile, err := driver.File(utils.Dir(dst), account) + if err != nil { + return err + } + _, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchCopy", base.Post, nil, &base.Json{ + "ids": []string{srcFile.Id}, + "to": base.Json{ + "parent_id": dstDirFile.Id, + }, + }, nil, account) + if err == nil { + _ = base.DeleteCache(utils.Dir(dst), account) + } + return err +} + +func (driver PikPak) Delete(path string, account *model.Account) error { + file, err := driver.File(path, account) + if err != nil { + return err + } + _, err = driver.Request("https://api-drive.mypikpak.com/drive/v1/files:batchTrash", base.Post, nil, &base.Json{ + "ids": []string{file.Id}, + }, nil, account) + if err == nil { + _ = base.DeleteCache(utils.Dir(path), account) + } + return err +} + +func (driver PikPak) Upload(file *model.FileStream, account *model.Account) error { + return base.ErrNotImplement +} + +var _ base.Driver = (*PikPak)(nil) diff --git a/drivers/pikpak/pikpak.go b/drivers/pikpak/pikpak.go new file mode 100644 index 00000000..abe86b2d --- /dev/null +++ b/drivers/pikpak/pikpak.go @@ -0,0 +1,178 @@ +package pikpak + +import ( + "errors" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" + "path/filepath" + "strconv" + "time" +) + +type RespErr struct { + ErrorCode int `json:"error_code"` + Error string `json:"error"` +} + +func (driver PikPak) Login(account *model.Account) error { + var e RespErr + res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{ + "captcha_token": "", + "client_id": "YNxT9w7GMdWvEOKa", + "client_secret": "dbw2OtmVEeuUvIptb1Coyg", + "username": account.Username, + "password": account.Password, + }).Post("https://user.mypikpak.com/v1/auth/signin") + if err != nil { + account.Status = err.Error() + return err + } + if e.ErrorCode != 0 { + account.Status = e.Error + return errors.New(e.Error) + } + data := res.Body() + account.Status = "work" + account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString() + account.AccessToken = jsoniter.Get(data, "access_token").ToString() + return nil +} + +func (driver PikPak) RefreshToken(account *model.Account) error { + var e RespErr + res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{ + "client_id": "YNxT9w7GMdWvEOKa", + "client_secret": "dbw2OtmVEeuUvIptb1Coyg", + "grant_type": "refresh_token", + "refresh_token": account.RefreshToken, + }).Post("https://user.mypikpak.com/v1/auth/token") + if err != nil { + account.Status = err.Error() + return err + } + if e.ErrorCode != 0 { + if e.ErrorCode == 4126 { + // refresh_token 失效,重新登陆 + return driver.Login(account) + } + } + data := res.Body() + account.Status = "work" + account.RefreshToken = jsoniter.Get(data, "refresh_token").ToString() + account.AccessToken = jsoniter.Get(data, "access_token").ToString() + return nil +} + +func (driver PikPak) Request(url string, method int, query map[string]string, data *base.Json, resp interface{}, account *model.Account) ([]byte, error) { + req := base.RestyClient.R() + req.SetHeader("Authorization", "Bearer "+account.AccessToken) + if query != nil { + req.SetQueryParams(query) + } + if data != nil { + req.SetBody(data) + } + if resp != nil { + req.SetResult(resp) + } + var e RespErr + req.SetError(&e) + var res *resty.Response + var err error + switch method { + case base.Get: + res, err = req.Get(url) + case base.Post: + res, err = req.Post(url) + case base.Patch: + res, err = req.Patch(url) + default: + return nil, base.ErrNotSupport + } + if err != nil { + return nil, err + } + if e.ErrorCode != 0 { + if e.ErrorCode == 16 { + // login / refresh token + err = driver.RefreshToken(account) + if err != nil { + return nil, err + } + _ = model.SaveAccount(account) + return driver.Request(url, method, query, data, resp, account) + } else { + return nil, errors.New(e.Error) + } + } + return res.Body(), nil +} + +type File struct { + Id string `json:"id"` + Kind string `json:"kind"` + Name string `json:"name"` + ModifiedTime *time.Time `json:"modified_time"` + Size string `json:"size"` + ThumbnailLink string `json:"thumbnail_link"` + WebContentLink string `json:"web_content_link"` +} + +func (driver PikPak) FormatFile(file *File) *model.File { + size, _ := strconv.ParseInt(file.Size, 10, 64) + f := &model.File{ + Id: file.Id, + Name: file.Name, + Size: size, + Driver: driver.Config().Name, + UpdatedAt: file.ModifiedTime, + Thumbnail: file.ThumbnailLink, + } + if file.Kind == "drive#folder" { + f.Type = conf.FOLDER + } else { + f.Type = utils.GetFileType(filepath.Ext(file.Name)) + } + return f +} + +type Files struct { + Files []File `json:"files"` + NextPageToken string `json:"next_page_token"` +} + +func (driver PikPak) GetFiles(id string, account *model.Account) ([]File, error) { + res := make([]File, 0) + pageToken := "first" + for pageToken != "" { + if pageToken == "first" { + pageToken = "" + } + query := map[string]string{ + "parent_id": id, + "thumbnail_size": "SIZE_LARGE", + "with_audit": "true", + "limit": "100", + "filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`, + "page_token": pageToken, + } + var resp Files + _, err := driver.Request("https://api-drive.mypikpak.com/drive/v1/files", base.Get, query, nil, &resp, account) + if err != nil { + return nil, err + } + log.Debugf("%+v", resp) + pageToken = resp.NextPageToken + res = append(res, resp.Files...) + } + return res, nil +} + +func init() { + base.RegisterDriver(&PikPak{}) +} diff --git a/server/webdav/file.go b/server/webdav/file.go index 885bee99..1a790cf2 100644 --- a/server/webdav/file.go +++ b/server/webdav/file.go @@ -220,6 +220,7 @@ func moveFiles(ctx context.Context, fs *FileSystem, src string, dst string, over } err = driver.Move(srcPath,dstPath,srcAccount) if err != nil { + log.Debug(err) return http.StatusInternalServerError, err } return http.StatusNoContent, nil