mirror of https://github.com/Xhofe/alist
586 lines
15 KiB
Go
586 lines
15 KiB
Go
package thunder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
type Thunder struct {
|
|
*XunLeiCommon
|
|
model.Storage
|
|
Addition
|
|
|
|
identity string
|
|
}
|
|
|
|
func (x *Thunder) Config() driver.Config {
|
|
return config
|
|
}
|
|
|
|
func (x *Thunder) GetAddition() driver.Additional {
|
|
return &x.Addition
|
|
}
|
|
|
|
func (x *Thunder) Init(ctx context.Context) (err error) {
|
|
// 初始化所需参数
|
|
if x.XunLeiCommon == nil {
|
|
x.XunLeiCommon = &XunLeiCommon{
|
|
Common: &Common{
|
|
client: base.NewRestyClient(),
|
|
Algorithms: []string{
|
|
"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR",
|
|
"GzhNkZ8pOBsCY+7",
|
|
"v+l0ImTpG7c7/",
|
|
"e5ztohgVXNP",
|
|
"t",
|
|
"EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO",
|
|
"o7dvYgbRMOpHXxCs",
|
|
"6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+",
|
|
"kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb",
|
|
"j",
|
|
"4scKJNdd7F27Hv7tbt",
|
|
},
|
|
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
|
|
ClientID: "Xp6vsxz_7IYVw2BB",
|
|
ClientSecret: "Xp6vsy4tN9toTVdMSpomVdXpRmES",
|
|
ClientVersion: "7.51.0.8196",
|
|
PackageName: "com.xunlei.downloadprovider",
|
|
UserAgent: "ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
|
|
DownloadUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
|
|
|
|
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()))
|
|
op.MustSaveDriverStorage(x)
|
|
}
|
|
}
|
|
x.SetTokenResp(token)
|
|
return err
|
|
},
|
|
}
|
|
}
|
|
|
|
// 自定义验证码token
|
|
ctoekn := strings.TrimSpace(x.CaptchaToken)
|
|
if ctoekn != "" {
|
|
x.SetCaptchaToken(ctoekn)
|
|
}
|
|
|
|
// 防止重复登录
|
|
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)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (x *Thunder) Drop(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
type ThunderExpert struct {
|
|
*XunLeiCommon
|
|
model.Storage
|
|
ExpertAddition
|
|
|
|
identity string
|
|
}
|
|
|
|
func (x *ThunderExpert) Config() driver.Config {
|
|
return configExpert
|
|
}
|
|
|
|
func (x *ThunderExpert) GetAddition() driver.Additional {
|
|
return &x.ExpertAddition
|
|
}
|
|
|
|
func (x *ThunderExpert) Init(ctx context.Context) (err error) {
|
|
// 防止重复登录
|
|
identity := x.GetIdentity()
|
|
if identity != x.identity || !x.IsLogin() {
|
|
x.identity = identity
|
|
x.XunLeiCommon = &XunLeiCommon{
|
|
Common: &Common{
|
|
client: base.NewRestyClient(),
|
|
|
|
DeviceID: func() string {
|
|
if len(x.DeviceID) != 32 {
|
|
return utils.GetMD5EncodeStr(x.DeviceID)
|
|
}
|
|
return x.DeviceID
|
|
}(),
|
|
ClientID: x.ClientID,
|
|
ClientSecret: x.ClientSecret,
|
|
ClientVersion: x.ClientVersion,
|
|
PackageName: x.PackageName,
|
|
UserAgent: x.UserAgent,
|
|
DownloadUserAgent: x.DownloadUserAgent,
|
|
UseVideoUrl: x.UseVideoUrl,
|
|
|
|
refreshCTokenCk: func(token string) {
|
|
x.CaptchaToken = token
|
|
op.MustSaveDriverStorage(x)
|
|
},
|
|
},
|
|
}
|
|
|
|
if x.CaptchaToken != "" {
|
|
x.SetCaptchaToken(x.CaptchaToken)
|
|
}
|
|
|
|
// 签名方法
|
|
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.XunLeiCommon.RefreshToken(x.ExpertAddition.RefreshToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
x.SetTokenResp(token)
|
|
|
|
// 刷新token方法
|
|
x.SetRefreshTokenFunc(func() error {
|
|
token, err := x.XunLeiCommon.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.XunLeiCommon.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
|
|
})
|
|
}
|
|
} else {
|
|
// 仅修改验证码token
|
|
if x.CaptchaToken != "" {
|
|
x.SetCaptchaToken(x.CaptchaToken)
|
|
}
|
|
x.XunLeiCommon.UserAgent = x.UserAgent
|
|
x.XunLeiCommon.DownloadUserAgent = x.DownloadUserAgent
|
|
x.XunLeiCommon.UseVideoUrl = x.UseVideoUrl
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (x *ThunderExpert) Drop(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (x *ThunderExpert) SetTokenResp(token *TokenResp) {
|
|
x.XunLeiCommon.SetTokenResp(token)
|
|
if token != nil {
|
|
x.ExpertAddition.RefreshToken = token.RefreshToken
|
|
}
|
|
}
|
|
|
|
type XunLeiCommon struct {
|
|
*Common
|
|
*TokenResp // 登录信息
|
|
|
|
refreshTokenFunc func() error
|
|
}
|
|
|
|
func (xc *XunLeiCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
return xc.getFiles(ctx, dir.GetID())
|
|
}
|
|
|
|
func (xc *XunLeiCommon) 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 *XunLeiCommon) 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 *XunLeiCommon) 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 *XunLeiCommon) 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 *XunLeiCommon) 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 *XunLeiCommon) 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 *XunLeiCommon) 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 *XunLeiCommon) 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
|
|
}
|
|
|
|
// 设置刷新Token的方法
|
|
func (xc *XunLeiCommon) SetRefreshTokenFunc(fn func() error) {
|
|
xc.refreshTokenFunc = fn
|
|
}
|
|
|
|
// 设置Token
|
|
func (xc *XunLeiCommon) SetTokenResp(tr *TokenResp) {
|
|
xc.TokenResp = tr
|
|
}
|
|
|
|
// 携带Authorization和CaptchaToken的请求
|
|
func (xc *XunLeiCommon) 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)
|
|
}
|
|
|
|
// 刷新Token
|
|
func (xc *XunLeiCommon) 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
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// 登录
|
|
func (xc *XunLeiCommon) 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
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
func (xc *XunLeiCommon) IsLogin() bool {
|
|
if xc.TokenResp == nil {
|
|
return false
|
|
}
|
|
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
|
return err == nil
|
|
}
|
|
|
|
// 离线下载文件
|
|
func (xc *XunLeiCommon) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
|
|
var resp OfflineDownloadResp
|
|
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
|
r.SetContext(ctx)
|
|
r.SetBody(&base.Json{
|
|
"kind": FILE,
|
|
"name": fileName,
|
|
"parent_id": parentDir.GetID(),
|
|
"upload_type": UPLOAD_TYPE_URL,
|
|
"url": base.Json{
|
|
"url": fileUrl,
|
|
},
|
|
})
|
|
}, &resp)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &resp.Task, err
|
|
}
|
|
|
|
/*
|
|
获取离线下载任务列表
|
|
*/
|
|
func (xc *XunLeiCommon) OfflineList(ctx context.Context, nextPageToken string) ([]OfflineTask, error) {
|
|
res := make([]OfflineTask, 0)
|
|
|
|
var resp OfflineListResp
|
|
_, err := xc.Request(TASK_API_URL, http.MethodGet, func(req *resty.Request) {
|
|
req.SetContext(ctx).
|
|
SetQueryParams(map[string]string{
|
|
"type": "offline",
|
|
"limit": "10000",
|
|
"page_token": nextPageToken,
|
|
})
|
|
}, &resp)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get offline list: %w", err)
|
|
}
|
|
res = append(res, resp.Tasks...)
|
|
return res, nil
|
|
}
|
|
|
|
func (xc *XunLeiCommon) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
|
|
_, err := xc.Request(TASK_API_URL, http.MethodDelete, func(req *resty.Request) {
|
|
req.SetContext(ctx).
|
|
SetQueryParams(map[string]string{
|
|
"task_ids": strings.Join(taskIDs, ","),
|
|
"delete_files": strconv.FormatBool(deleteFiles),
|
|
})
|
|
}, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete tasks %v: %w", taskIDs, err)
|
|
}
|
|
return nil
|
|
}
|