From 68f37fc11fab7dd7fbe1a3561fa9e18c863bf759 Mon Sep 17 00:00:00 2001 From: foxxorcat Date: Thu, 28 Apr 2022 23:15:37 +0800 Subject: [PATCH] refactor(xunlei): optimized code --- drivers/xunlei/driver.go | 72 ++++---- drivers/xunlei/types.go | 29 +++- drivers/xunlei/util.go | 43 ++--- drivers/xunlei/xunlei.go | 354 ++++++++++++++++----------------------- 4 files changed, 230 insertions(+), 268 deletions(-) diff --git a/drivers/xunlei/driver.go b/drivers/xunlei/driver.go index 6d39ad22..de192ea3 100644 --- a/drivers/xunlei/driver.go +++ b/drivers/xunlei/driver.go @@ -7,15 +7,14 @@ import ( "os" "path/filepath" "strconv" - - "github.com/aliyun/aliyun-oss-go-sdk/oss" - "github.com/go-resty/resty/v2" + "time" "github.com/Xhofe/alist/conf" "github.com/Xhofe/alist/drivers/base" "github.com/Xhofe/alist/model" "github.com/Xhofe/alist/utils" - log "github.com/sirupsen/logrus" + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/go-resty/resty/v2" ) type XunLeiCloud struct{} @@ -48,10 +47,9 @@ func (driver XunLeiCloud) Items() []base.Item { Description: "account password", }, { - Name: "root_folder", - Label: "root folder file_id", - Type: base.TypeString, - Required: true, + Name: "root_folder", + Label: "root folder file_id", + Type: base.TypeString, }, } } @@ -60,9 +58,9 @@ func (driver XunLeiCloud) Save(account *model.Account, old *model.Account) error if account == nil { return nil } - state := GetState(account) - if state.isTokensExpires() { - return state.Login(account) + client := GetClient(account) + if client.token == "" { + return client.Login(account) } account.Status = "work" model.SaveAccount(account) @@ -101,7 +99,8 @@ func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.Fi files, _ := cache.([]model.File) return files, nil } - file, err := driver.File(path, account) + + parentFile, err := driver.File(path, account) if err != nil { return nil, err } @@ -109,9 +108,9 @@ func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.Fi files := make([]model.File, 0) for { var fileList FileList - _, err = GetState(account).Request("GET", FILE_API_URL, func(r *resty.Request) { + _, err = GetClient(account).Request("GET", FILE_API_URL, func(r *resty.Request) { r.SetQueryParams(map[string]string{ - "parent_id": file.Id, + "parent_id": parentFile.Id, "page_token": fileList.NextPageToken, "with_audit": "true", "filters": `{"phase": {"eq": "PHASE_TYPE_COMPLETE"}, "trashed":{"eq":false}}`, @@ -162,8 +161,7 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li return nil, base.ErrNotFile } var lFile Files - _, err = GetState(account).Request("GET", FILE_API_URL+"/{id}", func(r *resty.Request) { - r.SetPathParam("id", file.Id) + _, err = GetClient(account).Request("GET", FILE_API_URL+"/"+file.Id, func(r *resty.Request) { r.SetQueryParam("with_audit", "true") r.SetResult(&lFile) }, account) @@ -180,7 +178,6 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li func (driver XunLeiCloud) Path(path string, account *model.Account) (*model.File, []model.File, error) { path = utils.ParsePath(path) - log.Debugf("xunlei path: %s", path) file, err := driver.File(path, account) if err != nil { return nil, nil, err @@ -199,6 +196,17 @@ func (driver XunLeiCloud) Preview(path string, account *model.Account) (interfac return nil, base.ErrNotSupport } +func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + _, err = GetClient(account).Request("PATCH", FILE_API_URL+"/"+srcFile.Id, func(r *resty.Request) { + r.SetBody(&base.Json{"name": filepath.Base(dst)}) + }, account) + return err +} + func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error { dir, name := filepath.Split(path) parentFile, err := driver.File(dir, account) @@ -208,7 +216,7 @@ func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error { if !parentFile.IsDir() { return base.ErrNotFolder } - _, err = GetState(account).Request("POST", FILE_API_URL, func(r *resty.Request) { + _, err = GetClient(account).Request("POST", FILE_API_URL, func(r *resty.Request) { r.SetBody(&base.Json{ "kind": FOLDER, "name": name, @@ -229,7 +237,7 @@ func (driver XunLeiCloud) Move(src string, dst string, account *model.Account) e return err } - _, err = GetState(account).Request("POST", FILE_API_URL+":batchMove", func(r *resty.Request) { + _, err = GetClient(account).Request("POST", FILE_API_URL+":batchMove", func(r *resty.Request) { r.SetBody(&base.Json{ "to": base.Json{"parent_id": dstDirFile.Id}, "ids": []string{srcFile.Id}, @@ -248,7 +256,7 @@ func (driver XunLeiCloud) Copy(src string, dst string, account *model.Account) e if err != nil { return err } - _, err = GetState(account).Request("POST", FILE_API_URL+":batchCopy", func(r *resty.Request) { + _, err = GetClient(account).Request("POST", FILE_API_URL+":batchCopy", func(r *resty.Request) { r.SetBody(&base.Json{ "to": base.Json{"parent_id": dstDirFile.Id}, "ids": []string{srcFile.Id}, @@ -262,8 +270,7 @@ func (driver XunLeiCloud) Delete(path string, account *model.Account) error { if err != nil { return err } - _, err = GetState(account).Request("PATCH", FILE_API_URL+"/{id}/trash", func(r *resty.Request) { - r.SetPathParam("id", srcFile.Id) + _, err = GetClient(account).Request("PATCH", FILE_API_URL+"/"+srcFile.Id+"/trash", func(r *resty.Request) { r.SetBody(&base.Json{}) }, account) return err @@ -294,7 +301,7 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account) tempFile.Close() var resp UploadTaskResponse - _, err = GetState(account).Request("POST", FILE_API_URL, func(r *resty.Request) { + _, err = GetClient(account).Request("POST", FILE_API_URL, func(r *resty.Request) { r.SetBody(&base.Json{ "kind": FILE, "parent_id": parentFile.Id, @@ -319,22 +326,13 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account) if err != nil { return err } - return bucket.UploadFile(param.Key, tempFile.Name(), 1<<22, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration)) + err = bucket.UploadFile(param.Key, tempFile.Name(), 1<<22, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration)) + if err != nil { + return err + } } + time.Sleep(time.Millisecond * 200) return nil } -func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error { - _, dstName := filepath.Split(dst) - srcFile, err := driver.File(src, account) - if err != nil { - return err - } - _, err = GetState(account).Request("PATCH", FILE_API_URL+"/{id}", func(r *resty.Request) { - r.SetPathParam("id", srcFile.Id) - r.SetBody(&base.Json{"name": dstName}) - }, account) - return err -} - var _ base.Driver = (*XunLeiCloud)(nil) diff --git a/drivers/xunlei/types.go b/drivers/xunlei/types.go index c73c292c..cd5008f4 100644 --- a/drivers/xunlei/types.go +++ b/drivers/xunlei/types.go @@ -1,16 +1,24 @@ package xunlei import ( + "fmt" "time" ) type Erron struct { - Error string `json:"error"` ErrorCode int64 `json:"error_code"` + ErrorMsg string `json:"error"` ErrorDescription string `json:"error_description"` // ErrorDetails interface{} `json:"error_details"` } +func (e *Erron) Error() string { + return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription) +} + +/* +* 验证码Token +**/ type CaptchaTokenRequest struct { Action string `json:"action"` CaptchaToken string `json:"captcha_token"` @@ -26,6 +34,9 @@ type CaptchaTokenResponse struct { Url string `json:"url"` } +/* +* 登录 +**/ type TokenResponse struct { TokenType string `json:"token_type"` AccessToken string `json:"access_token"` @@ -36,6 +47,10 @@ type TokenResponse struct { UserID string `json:"user_id"` } +func (t *TokenResponse) Token() string { + return fmt.Sprint(t.TokenType, " ", t.AccessToken) +} + type SignInRequest struct { CaptchaToken string `json:"captcha_token"` @@ -46,6 +61,9 @@ type SignInRequest struct { Password string `json:"password"` } +/* +* 文件 +**/ type FileList struct { Kind string `json:"kind"` NextPageToken string `json:"next_page_token"` @@ -116,6 +134,9 @@ type Files struct { //Collection interface{} `json:"collection"` } +/* +* 上传 +**/ type UploadTaskResponse struct { UploadType string `json:"upload_type"` @@ -152,3 +173,9 @@ type UploadTaskResponse struct { File Files `json:"file"` } + +type Tasks struct { + Tasks []interface{} + NextPageToken string `json:"next_page_token"` + //ExpiresIn int64 `json:"expires_in"` +} diff --git a/drivers/xunlei/util.go b/drivers/xunlei/util.go index 6c5315c9..a2470c3d 100644 --- a/drivers/xunlei/util.go +++ b/drivers/xunlei/util.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "io" + "net" "net/url" "github.com/Xhofe/alist/utils" @@ -57,12 +58,13 @@ const ( UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL" ) +// 验证码签名 func captchaSign(driverID string, time int64) string { str := fmt.Sprint(CLIENT_ID, CLIENT_VERSION, PACKAGE_NAME, driverID, time) for _, algorithm := range Algorithms { - str = utils.GetMD5Encode(fmt.Sprint(str, algorithm)) + str = utils.GetMD5Encode(str + algorithm) } - return fmt.Sprint(ALG_VERSION, ".", str) + return ALG_VERSION + "." + str } func getAction(method string, u string) string { @@ -70,34 +72,27 @@ func getAction(method string, u string) string { return fmt.Sprint(method, ":", c.Path) } +// 计算文件Gcid func getGcid(r io.Reader, size int64) (string, error) { calcBlockSize := func(j int64) int64 { - if j >= 0 && j <= 134217728 { - return 262144 + if j >= 0 && j <= 0x8000000 { + return 0x40000 } - if j <= 134217728 || j > 268435456 { - if j <= 268435456 || j > 536870912 { - return 2097152 + if j <= 0x8000000 || j > 0x10000000 { + if j <= 0x10000000 || j > 0x20000000 { + return 0x200000 } - return 1048576 + return 0x100000 } - return 524288 + return 0x80000 } - /* - calcBlockSize := func(j int64) int64 { - psize := int64(0x40000) - for j/psize > 0x200 { - psize <<= 1 - } - return psize - } - */ hash1 := sha1.New() hash2 := sha1.New() + readSize := calcBlockSize(size) for { hash2.Reset() - if n, err := io.CopyN(hash2, r, calcBlockSize(size)); err != nil && n == 0 { + if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 { if err != io.EOF { return "", err } @@ -107,3 +102,13 @@ func getGcid(r io.Reader, size int64) (string, error) { } return hex.EncodeToString(hash1.Sum(nil)), nil } + +// 获取driverID +func getDriverID(username string) string { + interfaces, _ := net.Interfaces() + str := username + for _, inter := range interfaces { + str += inter.HardwareAddr.String() + } + return utils.GetMD5Encode(str) +} diff --git a/drivers/xunlei/xunlei.go b/drivers/xunlei/xunlei.go index 2e9d4c23..692843ea 100644 --- a/drivers/xunlei/xunlei.go +++ b/drivers/xunlei/xunlei.go @@ -13,281 +13,213 @@ import ( log "github.com/sirupsen/logrus" ) -var xunleiClient = resty.New().SetHeaders(map[string]string{"Accept": "application/json;charset=UTF-8"}).SetTimeout(base.DefaultTimeout) +var xunleiClient = resty.New(). + SetHeaders(map[string]string{ + "Accept": "application/json;charset=UTF-8", + }). + SetTimeout(base.DefaultTimeout) -// 一个账户只允许登陆一次 -var userStateCache = struct { +var userClients sync.Map + +func GetClient(account *model.Account) *Client { + if v, ok := userClients.Load(account.Username); ok { + return v.(*Client) + } + + client := &Client{ + Client: xunleiClient, + driverID: getDriverID(account.Username), + } + userClients.Store(account.Username, client) + return client +} + +type Client struct { + *resty.Client sync.Mutex - States map[string]*State -}{States: make(map[string]*State)} -func GetState(account *model.Account) *State { - userStateCache.Lock() - defer userStateCache.Unlock() - if v, ok := userStateCache.States[account.Username]; ok && v != nil { - return v - } - state := new(State).Init() - userStateCache.States[account.Username] = state - return state + driverID string + captchaToken string + + token string + refreshToken string + userID string } -type State struct { - sync.Mutex - captchaToken string - captchaTokenExpiresTime int64 - - tokenType string - accessToken string - refreshToken string - tokenExpiresTime int64 //Milli - - userID string -} - -func (s *State) init() *State { - s.captchaToken = "" - s.captchaTokenExpiresTime = 0 - s.tokenType = "" - s.accessToken = "" - s.refreshToken = "" - s.tokenExpiresTime = 0 - s.userID = "0" - return s -} - -func (s *State) getToken(account *model.Account) (string, error) { - if s.isTokensExpires() { - if err := s.refreshToken_(account); err != nil { - return "", err - } - } - return fmt.Sprint(s.tokenType, " ", s.accessToken), nil -} - -func (s *State) getCaptchaToken(action string, account *model.Account) (string, error) { - if s.isCaptchaTokenExpires() { - return s.newCaptchaToken(action, nil, account) - } - return s.captchaToken, nil -} - -func (s *State) isCaptchaTokenExpires() bool { - return time.Now().UnixMilli() >= s.captchaTokenExpiresTime || s.captchaToken == "" || s.tokenType == "" -} - -func (s *State) isTokensExpires() bool { - return time.Now().UnixMilli() >= s.tokenExpiresTime || s.accessToken == "" -} - -func (s *State) newCaptchaToken(action string, meta map[string]string, account *model.Account) (string, error) { - ctime := time.Now().UnixMilli() - driverID := utils.GetMD5Encode(account.Username) - creq := CaptchaTokenRequest{ +// 请求验证码token +func (c *Client) requestCaptchaToken(action string, meta map[string]string) error { + req := CaptchaTokenRequest{ Action: action, - CaptchaToken: s.captchaToken, + CaptchaToken: c.captchaToken, ClientID: CLIENT_ID, - DeviceID: driverID, - Meta: map[string]string{ - "captcha_sign": captchaSign(driverID, ctime), - "client_version": CLIENT_VERSION, - "package_name": PACKAGE_NAME, - "timestamp": fmt.Sprint(ctime), - "user_id": s.userID, - }, - } - for k, v := range meta { - creq.Meta[k] = v + DeviceID: c.driverID, + Meta: meta, } var e Erron var resp CaptchaTokenResponse _, err := xunleiClient.R(). - SetBody(&creq). + SetBody(&req). SetError(&e). SetResult(&resp). - SetHeader("X-Device-Id", driverID). - SetQueryParam("client_id", CLIENT_ID). Post(XLUSER_API_URL + "/shield/captcha/init") if err != nil { - return "", err + return err } - if e.ErrorCode != 0 { - return "", fmt.Errorf("%s : %s", e.Error, e.ErrorDescription) + if e.ErrorCode != 0 || e.ErrorMsg != "" { + return &e } + if resp.Url != "" { - return "", fmt.Errorf("需要验证验证码") + return fmt.Errorf("need verify:%s", resp.Url) } - s.captchaTokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000 - s.captchaToken = resp.CaptchaToken - return s.captchaToken, nil + if resp.CaptchaToken == "" { + return fmt.Errorf("empty captchaToken") + } + c.captchaToken = resp.CaptchaToken + return nil } -func (s *State) refreshToken_(account *model.Account) error { - var e Erron - var resp TokenResponse - _, err := xunleiClient.R(). - SetResult(&resp).SetError(&e). - SetBody(&base.Json{ - "grant_type": "refresh_token", - "refresh_token": s.refreshToken, - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - }). - SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).SetQueryParam("client_id", CLIENT_ID). - Post(XLUSER_API_URL + "/auth/token") - if err != nil { - return err - } +// 登录 +func (c *Client) Login(account *model.Account) (err error) { + c.Lock() + defer c.Unlock() - switch e.ErrorCode { - case 4122, 4121: - return s.login(account) - case 0: - s.tokenExpiresTime = (time.Now().UnixMilli() + resp.ExpiresIn*1000) - 30000 - s.tokenType = resp.TokenType - s.accessToken = resp.AccessToken - s.refreshToken = resp.RefreshToken - s.userID = resp.UserID - return nil - default: - return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription) - } -} + defer func() { + if err != nil { + account.Status = err.Error() + } else { + account.Status = "work" + } + model.SaveAccount(account) + }() -func (s *State) login(account *model.Account) error { - s.init() - ctime := time.Now().UnixMilli() url := XLUSER_API_URL + "/auth/signin" - captchaToken, err := s.newCaptchaToken(getAction("POST", url), map[string]string{"username": account.Username}, account) + err = c.requestCaptchaToken(getAction(http.MethodPost, url), map[string]string{"username": account.Username}) if err != nil { return err } - signReq := SignInRequest{ - CaptchaToken: captchaToken, - ClientID: CLIENT_ID, - ClientSecret: CLIENT_SECRET, - Username: account.Username, - Password: account.Password, - } - var e Erron var resp TokenResponse _, err = xunleiClient.R(). SetResult(&resp). SetError(&e). - SetBody(&signReq). - SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)). - SetQueryParam("client_id", CLIENT_ID). + SetBody(&SignInRequest{ + CaptchaToken: c.captchaToken, + ClientID: CLIENT_ID, + ClientSecret: CLIENT_SECRET, + Username: account.Username, + Password: account.Password, + }). Post(url) if err != nil { return err } - defer model.SaveAccount(account) - if e.ErrorCode != 0 { - account.Status = e.Error - return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription) + if e.ErrorCode != 0 || e.ErrorMsg != "" { + return &e } - account.Status = "work" - s.tokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000 - s.tokenType = resp.TokenType - s.accessToken = resp.AccessToken - s.refreshToken = resp.RefreshToken - s.userID = resp.UserID + + if resp.RefreshToken == "" { + return base.ErrEmptyToken + } + + c.token = resp.Token() + c.refreshToken = resp.RefreshToken + c.userID = resp.UserID return nil } -func (s *State) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) { - s.Lock() - token, err := s.getToken(account) - if err != nil { - return nil, err - } +// 刷新验证码token +func (c *Client) RefreshCaptchaToken(action string) error { + c.Lock() + defer c.Unlock() - captchaToken, err := s.getCaptchaToken(getAction(method, url), account) - if err != nil { - return nil, err - } + ctime := time.Now().UnixMilli() + return c.requestCaptchaToken(action, map[string]string{ + "captcha_sign": captchaSign(c.driverID, ctime), + "client_version": CLIENT_VERSION, + "package_name": PACKAGE_NAME, + "timestamp": fmt.Sprint(ctime), + "user_id": c.userID, + }) +} +// 刷新token +func (c *Client) RefreshToken() error { + c.Lock() + defer c.Unlock() + + var e Erron + var resp TokenResponse + _, err := xunleiClient.R(). + SetError(&e). + SetResult(&resp). + SetBody(&base.Json{ + "grant_type": "refresh_token", + "refresh_token": c.refreshToken, + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + }). + Post(XLUSER_API_URL + "/auth/token") + if err != nil { + return err + } + if e.ErrorCode != 0 || e.ErrorMsg != "" { + return &e + } + c.token = resp.TokenType + " " + resp.AccessToken + c.refreshToken = resp.RefreshToken + c.userID = resp.UserID + return nil +} + +func (c *Client) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) { + c.Lock() req := xunleiClient.R(). SetHeaders(map[string]string{ - "X-Device-Id": utils.GetMD5Encode(account.Username), - "Authorization": token, - "X-Captcha-Token": captchaToken, + "X-Device-Id": c.driverID, + "Authorization": c.token, + "X-Captcha-Token": c.captchaToken, }). SetQueryParam("client_id", CLIENT_ID) - - callback(req) - s.Unlock() - - var res *resty.Response - switch method { - case "GET": - res, err = req.Get(url) - case "POST": - res, err = req.Post(url) - case "DELETE": - res, err = req.Delete(url) - case "PATCH": - res, err = req.Patch(url) - case "PUT": - res, err = req.Put(url) - default: - return nil, base.ErrNotSupport + if callback != nil { + callback(req) } + c.Unlock() + res, err := req.Execute(method, url) if err != nil { return nil, err } log.Debug(res.String()) var e Erron - err = utils.Json.Unmarshal(res.Body(), &e) - if err != nil { + if err = utils.Json.Unmarshal(res.Body(), &e); err != nil { return nil, err } + + // 处理错误 switch e.ErrorCode { - case 9: - _, err = s.newCaptchaToken(getAction(method, url), nil, account) - if err != nil { - return nil, err + case 0: + return res, nil + case 4122, 4121: // token过期 + if err = c.RefreshToken(); err == nil { + break } fallthrough - case 4122, 4121: // Authorization expired - return s.Request(method, url, callback, account) - case 0: - if res.StatusCode() == http.StatusOK { - return res, nil + case 16: // 登录失效 + if err = c.Login(account); err != nil { + return nil, err + } + case 9: // 验证码token过期 + if err = c.RefreshCaptchaToken(getAction(method, url)); err != nil { + return nil, err } - return nil, fmt.Errorf(res.String()) default: - return nil, fmt.Errorf("%s : %s", e.Error, e.ErrorDescription) + return nil, &e } -} - -func (s *State) Init() *State { - s.Lock() - defer s.Unlock() - return s.init() -} - -func (s *State) GetCaptchaToken(action string, account *model.Account) (string, error) { - s.Lock() - defer s.Unlock() - return s.getCaptchaToken(action, account) -} - -func (s *State) GetToken(account *model.Account) (string, error) { - s.Lock() - defer s.Unlock() - return s.getToken(account) -} - -func (s *State) Login(account *model.Account) error { - s.Lock() - defer s.Unlock() - return s.login(account) + return c.Request(method, url, callback, account) }