package thunderx import ( "context" "fmt" "github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/pkg/utils" hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/go-resty/resty/v2" "net/http" "strings" ) type ThunderX struct { *XunLeiXCommon model.Storage Addition identity string } func (x *ThunderX) Config() driver.Config { return config } func (x *ThunderX) GetAddition() driver.Additional { return &x.Addition } func (x *ThunderX) Init(ctx context.Context) (err error) { // 初始化所需参数 if x.XunLeiXCommon == nil { x.XunLeiXCommon = &XunLeiXCommon{ Common: &Common{ client: base.NewRestyClient(), Algorithms: Algorithms, DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password), ClientID: ClientID, ClientSecret: ClientSecret, ClientVersion: ClientVersion, PackageName: PackageName, UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""), DownloadUserAgent: DownloadUserAgent, UseVideoUrl: x.UseVideoUrl, refreshCTokenCk: func(token string) { x.CaptchaToken = token op.MustSaveDriverStorage(x) }, }, refreshTokenFunc: func() error { // 通过RefreshToken刷新 token, err := x.RefreshToken(x.TokenResp.RefreshToken) if err != nil { // 重新登录 token, err = x.Login(x.Username, x.Password) if err != nil { x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) if token.UserID != "" { x.SetUserID(token.UserID) x.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID) } op.MustSaveDriverStorage(x) } } x.SetTokenResp(token) return err }, } } // 自定义验证码token ctoken := strings.TrimSpace(x.CaptchaToken) if ctoken != "" { x.SetCaptchaToken(ctoken) } if x.DeviceID == "" { x.SetDeviceID(utils.GetMD5EncodeStr(x.Username + x.Password)) } x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl x.Addition.RootFolderID = x.RootFolderID // 防止重复登录 identity := x.GetIdentity() if x.identity != identity || !x.IsLogin() { x.identity = identity // 登录 token, err := x.Login(x.Username, x.Password) if err != nil { return err } x.SetTokenResp(token) if token.UserID != "" { x.SetUserID(token.UserID) x.UserAgent = BuildCustomUserAgent(x.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, token.UserID) } } return nil } func (x *ThunderX) Drop(ctx context.Context) error { return nil } type ThunderXExpert struct { *XunLeiXCommon model.Storage ExpertAddition identity string } func (x *ThunderXExpert) Config() driver.Config { return configExpert } func (x *ThunderXExpert) GetAddition() driver.Additional { return &x.ExpertAddition } func (x *ThunderXExpert) Init(ctx context.Context) (err error) { // 防止重复登录 identity := x.GetIdentity() if identity != x.identity || !x.IsLogin() { x.identity = identity x.XunLeiXCommon = &XunLeiXCommon{ Common: &Common{ client: base.NewRestyClient(), DeviceID: func() string { if len(x.DeviceID) != 32 { if x.LoginType == "user" { return utils.GetMD5EncodeStr(x.Username + x.Password) } return utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken) } return x.DeviceID }(), ClientID: x.ClientID, ClientSecret: x.ClientSecret, ClientVersion: x.ClientVersion, PackageName: x.PackageName, UserAgent: func() string { if x.ExpertAddition.UserAgent != "" { return x.ExpertAddition.UserAgent } if x.LoginType == "user" { return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.Username+x.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, "") } return BuildCustomUserAgent(utils.GetMD5EncodeStr(x.ExpertAddition.RefreshToken), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, "") }(), DownloadUserAgent: func() string { if x.ExpertAddition.DownloadUserAgent != "" { return x.ExpertAddition.DownloadUserAgent } return DownloadUserAgent }(), UseVideoUrl: x.UseVideoUrl, refreshCTokenCk: func(token string) { x.CaptchaToken = token op.MustSaveDriverStorage(x) }, }, } if x.ExpertAddition.CaptchaToken != "" { x.SetCaptchaToken(x.ExpertAddition.CaptchaToken) op.MustSaveDriverStorage(x) } if x.Common.DeviceID != "" { x.ExpertAddition.DeviceID = x.Common.DeviceID op.MustSaveDriverStorage(x) } if x.Common.DownloadUserAgent != "" { x.ExpertAddition.DownloadUserAgent = x.Common.DownloadUserAgent op.MustSaveDriverStorage(x) } x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl x.ExpertAddition.RootFolderID = x.RootFolderID // 签名方法 if x.SignType == "captcha_sign" { x.Common.Timestamp = x.Timestamp x.Common.CaptchaSign = x.CaptchaSign } else { x.Common.Algorithms = strings.Split(x.Algorithms, ",") } // 登录方式 if x.LoginType == "refresh_token" { // 通过RefreshToken登录 token, err := x.XunLeiXCommon.RefreshToken(x.ExpertAddition.RefreshToken) if err != nil { return err } x.SetTokenResp(token) // 刷新token方法 x.SetRefreshTokenFunc(func() error { token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken) if err != nil { x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) } x.SetTokenResp(token) op.MustSaveDriverStorage(x) return err }) } else { // 通过用户密码登录 token, err := x.Login(x.Username, x.Password) if err != nil { return err } x.SetTokenResp(token) x.SetRefreshTokenFunc(func() error { token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken) if err != nil { token, err = x.Login(x.Username, x.Password) if err != nil { x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) } } x.SetTokenResp(token) op.MustSaveDriverStorage(x) return err }) } // 更新 UserAgent if x.TokenResp.UserID != "" { x.ExpertAddition.UserAgent = BuildCustomUserAgent(x.ExpertAddition.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, x.TokenResp.UserID) x.SetUserAgent(x.ExpertAddition.UserAgent) op.MustSaveDriverStorage(x) } } else { // 仅修改验证码token if x.CaptchaToken != "" { x.SetCaptchaToken(x.CaptchaToken) } x.XunLeiXCommon.UserAgent = x.ExpertAddition.UserAgent x.XunLeiXCommon.DownloadUserAgent = x.ExpertAddition.UserAgent x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl x.ExpertAddition.RootFolderID = x.RootFolderID } return nil } func (x *ThunderXExpert) Drop(ctx context.Context) error { return nil } func (x *ThunderXExpert) SetTokenResp(token *TokenResp) { x.XunLeiXCommon.SetTokenResp(token) if token != nil { x.ExpertAddition.RefreshToken = token.RefreshToken } } type XunLeiXCommon struct { *Common *TokenResp // 登录信息 refreshTokenFunc func() error } func (xc *XunLeiXCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { return xc.getFiles(ctx, dir.GetID()) } func (xc *XunLeiXCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { var lFile Files _, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) { r.SetContext(ctx) r.SetPathParam("fileID", file.GetID()) //r.SetQueryParam("space", "") }, &lFile) if err != nil { return nil, err } link := &model.Link{ URL: lFile.WebContentLink, Header: http.Header{ "User-Agent": {xc.DownloadUserAgent}, }, } if xc.UseVideoUrl { for _, media := range lFile.Medias { if media.Link.URL != "" { link.URL = media.Link.URL break } } } /* strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink) if len(strs) == 2 { timestamp, err := strconv.ParseInt(strs[1], 10, 64) if err == nil { expired := time.Duration(timestamp-time.Now().Unix()) * time.Second link.Expiration = &expired } } */ return link, nil } func (xc *XunLeiXCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { _, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) { r.SetContext(ctx) r.SetBody(&base.Json{ "kind": FOLDER, "name": dirName, "parent_id": parentDir.GetID(), }) }, nil) return err } func (xc *XunLeiXCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error { _, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) { r.SetContext(ctx) r.SetBody(&base.Json{ "to": base.Json{"parent_id": dstDir.GetID()}, "ids": []string{srcObj.GetID()}, }) }, nil) return err } func (xc *XunLeiXCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error { _, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) { r.SetContext(ctx) r.SetPathParam("fileID", srcObj.GetID()) r.SetBody(&base.Json{"name": newName}) }, nil) return err } func (xc *XunLeiXCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { _, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) { r.SetContext(ctx) r.SetBody(&base.Json{ "to": base.Json{"parent_id": dstDir.GetID()}, "ids": []string{srcObj.GetID()}, }) }, nil) return err } func (xc *XunLeiXCommon) Remove(ctx context.Context, obj model.Obj) error { _, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) { r.SetContext(ctx) r.SetPathParam("fileID", obj.GetID()) r.SetBody("{}") }, nil) return err } func (xc *XunLeiXCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { hi := stream.GetHash() gcid := hi.GetHash(hash_extend.GCID) if len(gcid) < hash_extend.GCID.Width { tFile, err := stream.CacheFullInTempFile() if err != nil { return err } gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize()) if err != nil { return err } } var resp UploadTaskResponse _, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) { r.SetContext(ctx) r.SetBody(&base.Json{ "kind": FILE, "parent_id": dstDir.GetID(), "name": stream.GetName(), "size": stream.GetSize(), "hash": gcid, "upload_type": UPLOAD_TYPE_RESUMABLE, }) }, &resp) if err != nil { return err } param := resp.Resumable.Params if resp.UploadType == UPLOAD_TYPE_RESUMABLE { param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".") s, err := session.NewSession(&aws.Config{ Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken), Region: aws.String("xunlei"), Endpoint: aws.String(param.Endpoint), }) if err != nil { return err } uploader := s3manager.NewUploader(s) if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize { uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1) } _, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{ Bucket: aws.String(param.Bucket), Key: aws.String(param.Key), Expires: aws.Time(param.Expiration), Body: stream, }) return err } return nil } func (xc *XunLeiXCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) { files := make([]model.Obj, 0) var pageToken string for { var fileList FileList _, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) { r.SetContext(ctx) r.SetQueryParams(map[string]string{ "space": "", "__type": "drive", "refresh": "true", "__sync": "true", "parent_id": folderId, "page_token": pageToken, "with_audit": "true", "limit": "100", "filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`, }) }, &fileList) if err != nil { return nil, err } for i := 0; i < len(fileList.Files); i++ { files = append(files, &fileList.Files[i]) } if fileList.NextPageToken == "" { break } pageToken = fileList.NextPageToken } return files, nil } // SetRefreshTokenFunc 设置刷新Token的方法 func (xc *XunLeiXCommon) SetRefreshTokenFunc(fn func() error) { xc.refreshTokenFunc = fn } // SetTokenResp 设置Token func (xc *XunLeiXCommon) SetTokenResp(tr *TokenResp) { xc.TokenResp = tr } // Request 携带Authorization和CaptchaToken的请求 func (xc *XunLeiXCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { data, err := xc.Common.Request(url, method, func(req *resty.Request) { req.SetHeaders(map[string]string{ "Authorization": xc.Token(), "X-Captcha-Token": xc.GetCaptchaToken(), }) if callback != nil { callback(req) } }, resp) errResp, ok := err.(*ErrResp) if !ok { return nil, err } switch errResp.ErrorCode { case 0: return data, nil case 4122, 4121, 10, 16: if xc.refreshTokenFunc != nil { if err = xc.refreshTokenFunc(); err == nil { break } } return nil, err case 9: // 验证码token过期 if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil { return nil, err } default: return nil, err } return xc.Request(url, method, callback, resp) } // RefreshToken 刷新Token func (xc *XunLeiXCommon) RefreshToken(refreshToken string) (*TokenResp, error) { var resp TokenResp _, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) { req.SetBody(&base.Json{ "grant_type": "refresh_token", "refresh_token": refreshToken, "client_id": xc.ClientID, "client_secret": xc.ClientSecret, }) }, &resp) if err != nil { return nil, err } if resp.RefreshToken == "" { return nil, errs.EmptyToken } resp.UserID = resp.Sub return &resp, nil } // Login 登录 func (xc *XunLeiXCommon) Login(username, password string) (*TokenResp, error) { url := XLUSER_API_URL + "/auth/signin" err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username) if err != nil { return nil, err } var resp TokenResp _, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) { req.SetBody(&SignInRequest{ CaptchaToken: xc.GetCaptchaToken(), ClientID: xc.ClientID, ClientSecret: xc.ClientSecret, Username: username, Password: password, }) }, &resp) if err != nil { return nil, err } resp.UserID = resp.Sub return &resp, nil } func (xc *XunLeiXCommon) IsLogin() bool { if xc.TokenResp == nil { return false } _, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil) return err == nil }