mirror of
https://github.com/AlistGo/alist.git
synced 2025-04-21 20:18:47 +08:00
* feat: add supports for thunderX driver * fix: Fix the bug where UserID is not passed correctly * feat: add support for thunder_browser driver
This commit is contained in:
parent
163af0515f
commit
639b7817bf
@ -46,6 +46,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
||||
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
||||
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
||||
_ "github.com/alist-org/alist/v3/drivers/thunder_browser"
|
||||
_ "github.com/alist-org/alist/v3/drivers/thunderx"
|
||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
||||
|
813
drivers/thunder_browser/driver.go
Normal file
813
drivers/thunder_browser/driver.go
Normal file
@ -0,0 +1,813 @@
|
||||
package thunder_browser
|
||||
|
||||
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"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ThunderBrowser struct {
|
||||
*XunLeiBrowserCommon
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) GetAddition() driver.Additional {
|
||||
return &x.Addition
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) Init(ctx context.Context) (err error) {
|
||||
|
||||
spaceTokenFunc := func() error {
|
||||
// 如果用户未设置 "超级保险柜" 密码 则直接返回
|
||||
if x.SafePassword == "" {
|
||||
return nil
|
||||
}
|
||||
// 通过 GetSafeAccessToken 获取
|
||||
token, err := x.GetSafeAccessToken(x.SafePassword)
|
||||
x.SetSpaceTokenResp(token)
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化所需参数
|
||||
if x.XunLeiBrowserCommon == nil {
|
||||
x.XunLeiBrowserCommon = &XunLeiBrowserCommon{
|
||||
Common: &Common{
|
||||
client: base.NewRestyClient(),
|
||||
Algorithms: []string{
|
||||
"x+I5XiTByg",
|
||||
"6QU1x5DqGAV3JKg6h",
|
||||
"VI1vL1WXr7st0es",
|
||||
"n+/3yhlrnKs4ewhLgZhZ5ITpt554",
|
||||
"UOip2PE7BLIEov/ZX6VOnsz",
|
||||
"Q70h9lpViNCOC8sGVkar9o22LhBTjfP",
|
||||
"IVHFuB1JcMlaZHnW",
|
||||
"bKE",
|
||||
"HZRbwxOiQx+diNopi6Nu",
|
||||
"fwyasXgYL3rP314331b",
|
||||
"LWxXAiSW4",
|
||||
"UlWIjv1HGrC6Ngmt4Nohx",
|
||||
"FOa+Lc0bxTDpTwIh2",
|
||||
"0+RY",
|
||||
"xmRVMqokHHpvsiH0",
|
||||
},
|
||||
DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
|
||||
ClientID: "ZUBzD9J_XPXfn7f7",
|
||||
ClientSecret: "yESVmHecEe6F0aou69vl-g",
|
||||
ClientVersion: "1.0.7.1938",
|
||||
PackageName: "com.xunlei.browser",
|
||||
UserAgent: "ANDROID-com.xunlei.browser/1.0.7.1938 netWorkType/5G appid/22062 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/233100 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
|
||||
DownloadUserAgent: "AndroidDownloadManager/12 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
|
||||
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()))
|
||||
op.MustSaveDriverStorage(x)
|
||||
}
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证码token
|
||||
ctoekn := strings.TrimSpace(x.CaptchaToken)
|
||||
if ctoekn != "" {
|
||||
x.SetCaptchaToken(ctoekn)
|
||||
}
|
||||
x.XunLeiBrowserCommon.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)
|
||||
}
|
||||
|
||||
// 获取 spaceToken
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderBrowser) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ThunderBrowserExpert struct {
|
||||
*XunLeiBrowserCommon
|
||||
model.Storage
|
||||
ExpertAddition
|
||||
|
||||
identity string
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) Config() driver.Config {
|
||||
return configExpert
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) GetAddition() driver.Additional {
|
||||
return &x.ExpertAddition
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) Init(ctx context.Context) (err error) {
|
||||
|
||||
spaceTokenFunc := func() error {
|
||||
// 如果用户未设置 "超级保险柜" 密码 则直接返回
|
||||
if x.SafePassword == "" {
|
||||
return nil
|
||||
}
|
||||
// 通过 GetSafeAccessToken 获取
|
||||
token, err := x.GetSafeAccessToken(x.SafePassword)
|
||||
x.SetSpaceTokenResp(token)
|
||||
return err
|
||||
}
|
||||
|
||||
// 防止重复登录
|
||||
identity := x.GetIdentity()
|
||||
if identity != x.identity || !x.IsLogin() {
|
||||
x.identity = identity
|
||||
x.XunLeiBrowserCommon = &XunLeiBrowserCommon{
|
||||
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)
|
||||
}
|
||||
x.XunLeiBrowserCommon.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.XunLeiBrowserCommon.RefreshToken(x.ExpertAddition.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
|
||||
// 刷新token方法
|
||||
x.SetRefreshTokenFunc(func() error {
|
||||
token, err := x.XunLeiBrowserCommon.RefreshToken(x.TokenResp.RefreshToken)
|
||||
if err != nil {
|
||||
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
||||
}
|
||||
x.SetTokenResp(token)
|
||||
op.MustSaveDriverStorage(x)
|
||||
return err
|
||||
})
|
||||
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
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.XunLeiBrowserCommon.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
|
||||
})
|
||||
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 仅修改验证码token
|
||||
if x.CaptchaToken != "" {
|
||||
x.SetCaptchaToken(x.CaptchaToken)
|
||||
}
|
||||
|
||||
err = spaceTokenFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x.XunLeiBrowserCommon.UserAgent = x.UserAgent
|
||||
x.XunLeiBrowserCommon.DownloadUserAgent = x.DownloadUserAgent
|
||||
x.XunLeiBrowserCommon.UseVideoUrl = x.UseVideoUrl
|
||||
x.ExpertAddition.RootFolderID = x.RootFolderID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ThunderBrowserExpert) SetTokenResp(token *TokenResp) {
|
||||
x.XunLeiBrowserCommon.SetTokenResp(token)
|
||||
if token != nil {
|
||||
x.ExpertAddition.RefreshToken = token.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
type XunLeiBrowserCommon struct {
|
||||
*Common
|
||||
*TokenResp // 登录信息
|
||||
|
||||
refreshTokenFunc func() error
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
return xc.getFiles(ctx, dir.GetID(), args.ReqPath)
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
var lFile Files
|
||||
|
||||
params := map[string]string{
|
||||
"_magic": "2021",
|
||||
"space": "SPACE_BROWSER",
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
"with": "url",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if file.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
} else if file.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
params["space"] = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetPathParam("fileID", file.GetID())
|
||||
r.SetQueryParams(params)
|
||||
//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
|
||||
}
|
||||
}
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
js := base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
if parentDir.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
}
|
||||
} else if parentDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
js = base.Json{
|
||||
"kind": FOLDER,
|
||||
"name": dirName,
|
||||
"parent_id": parentDir.GetID(),
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
|
||||
srcSpace := "SPACE_BROWSER"
|
||||
dstSpace := "SPACE_BROWSER"
|
||||
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
srcSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
dstSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"_from": dstSpace,
|
||||
}
|
||||
js := base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
|
||||
"space": srcSpace,
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
js = base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
r.SetQueryParams(params)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
|
||||
params := map[string]string{
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
} else if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
params = map[string]string{
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
_, 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})
|
||||
r.SetQueryParams(params)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
|
||||
srcSpace := "SPACE_BROWSER"
|
||||
dstSpace := "SPACE_BROWSER"
|
||||
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
srcSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
dstSpace = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"_from": dstSpace,
|
||||
}
|
||||
js := base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID(), "space": dstSpace},
|
||||
"space": srcSpace,
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if srcObj.GetPath() == ThunderDriveFileID {
|
||||
params = map[string]string{}
|
||||
js = base.Json{
|
||||
"to": base.Json{"parent_id": dstDir.GetID()},
|
||||
"ids": []string{srcObj.GetID()},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
r.SetQueryParams(params)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) Remove(ctx context.Context, obj model.Obj) error {
|
||||
|
||||
js := base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if obj.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
}
|
||||
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
js = base.Json{
|
||||
"ids": []string{obj.GetID()},
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
if xc.RemoveWay == "delete" && obj.GetPath() == ThunderDriveFileID {
|
||||
_, 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
|
||||
} else if obj.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
_, err := xc.Request(FILE_API_URL+":batchDelete", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL+":batchTrash", http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, nil)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (xc *XunLeiBrowserCommon) 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
|
||||
}
|
||||
}
|
||||
|
||||
js := base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
"space": "SPACE_BROWSER",
|
||||
}
|
||||
// 对 "迅雷云盘" 内的文件 特殊处理
|
||||
if dstDir.GetPath() == ThunderDriveFileID {
|
||||
js = base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
}
|
||||
} else if dstDir.GetPath() == ThunderBrowserDriveSafeFileID {
|
||||
// 对 "超级保险箱" 内的文件 特殊处理
|
||||
js = base.Json{
|
||||
"kind": FILE,
|
||||
"parent_id": dstDir.GetID(),
|
||||
"name": stream.GetName(),
|
||||
"size": stream.GetSize(),
|
||||
"hash": gcid,
|
||||
"upload_type": UPLOAD_TYPE_RESUMABLE,
|
||||
"space": "SPACE_BROWSER_SAFE",
|
||||
}
|
||||
}
|
||||
|
||||
var resp UploadTaskResponse
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetBody(&js)
|
||||
}, &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 *XunLeiBrowserCommon) getFiles(ctx context.Context, folderId string, path string) ([]model.Obj, error) {
|
||||
files := make([]model.Obj, 0)
|
||||
var pageToken string
|
||||
for {
|
||||
var fileList FileList
|
||||
folderSpace := "SPACE_BROWSER"
|
||||
params := map[string]string{
|
||||
"parent_id": folderId,
|
||||
"page_token": pageToken,
|
||||
"space": folderSpace,
|
||||
"filters": `{"trashed":{"eq":false}}`,
|
||||
"with_audit": "true",
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
}
|
||||
var fileType int8
|
||||
// 处理特殊目录 “迅雷云盘” 设置特殊的 params 以便正常访问
|
||||
pattern1 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderDriveFolderName)
|
||||
thunderDriveMatch, _ := regexp.MatchString(pattern1, path)
|
||||
// 处理特殊目录 “超级保险箱” 设置特殊的 params 以便正常访问
|
||||
pattern2 := fmt.Sprintf(`^/.*/%s(/.*)?$`, ThunderBrowserDriveSafeFolderName)
|
||||
thunderBrowserDriveSafeMatch, _ := regexp.MatchString(pattern2, path)
|
||||
|
||||
// 如果是 "迅雷云盘" 内的
|
||||
if folderId == ThunderDriveFileID || thunderDriveMatch {
|
||||
params = 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}}`,
|
||||
}
|
||||
// 如果不是 "迅雷云盘"的"根目录"
|
||||
if folderId == ThunderDriveFileID {
|
||||
params["parent_id"] = ""
|
||||
}
|
||||
fileType = ThunderDriveType
|
||||
} else if thunderBrowserDriveSafeMatch {
|
||||
// 如果是 "超级保险箱" 内的
|
||||
fileType = ThunderBrowserDriveSafeType
|
||||
params["space"] = "SPACE_BROWSER_SAFE"
|
||||
}
|
||||
|
||||
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
|
||||
r.SetContext(ctx)
|
||||
r.SetQueryParams(params)
|
||||
}, &fileList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 对文件夹也进行处理
|
||||
fileList.FolderType = fileType
|
||||
|
||||
for i := 0; i < len(fileList.Files); i++ {
|
||||
file := &fileList.Files[i]
|
||||
// 标记 文件夹内的文件
|
||||
file.FileType = fileList.FolderType
|
||||
// 解决 "迅雷云盘" 重复出现问题————迅雷后端发送错误
|
||||
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType && folderId != "" {
|
||||
continue
|
||||
}
|
||||
// 处理特殊目录 “迅雷云盘” 设置特殊的文件夹ID
|
||||
if file.Name == ThunderDriveFolderName && file.ID == "" && file.FolderType == ThunderDriveFolderType {
|
||||
file.ID = ThunderDriveFileID
|
||||
} else if file.Name == ThunderBrowserDriveSafeFolderName && file.FolderType == ThunderBrowserDriveSafeFolderType {
|
||||
file.FileType = ThunderBrowserDriveSafeType
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
if fileList.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = fileList.NextPageToken
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// SetRefreshTokenFunc 设置刷新Token的方法
|
||||
func (xc *XunLeiBrowserCommon) SetRefreshTokenFunc(fn func() error) {
|
||||
xc.refreshTokenFunc = fn
|
||||
}
|
||||
|
||||
// SetTokenResp 设置Token
|
||||
func (xc *XunLeiBrowserCommon) SetTokenResp(tr *TokenResp) {
|
||||
xc.TokenResp = tr
|
||||
}
|
||||
|
||||
// SetSpaceTokenResp 设置Token
|
||||
func (xc *XunLeiBrowserCommon) SetSpaceTokenResp(spaceToken string) {
|
||||
xc.TokenResp.Token = spaceToken
|
||||
}
|
||||
|
||||
// Request 携带Authorization和CaptchaToken的请求
|
||||
func (xc *XunLeiBrowserCommon) 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.GetToken(),
|
||||
"X-Captcha-Token": xc.GetCaptchaToken(),
|
||||
"X-Space-Authorization": xc.GetSpaceToken(),
|
||||
})
|
||||
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:
|
||||
// space_token 获取失败
|
||||
if errResp.ErrorMsg == "space_token_invalid" {
|
||||
if token, err := xc.GetSafeAccessToken(xc.Token); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
xc.SetSpaceTokenResp(token)
|
||||
}
|
||||
|
||||
}
|
||||
if errResp.ErrorMsg == "captcha_invalid" {
|
||||
// 验证码token过期
|
||||
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return xc.Request(url, method, callback, resp)
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
func (xc *XunLeiBrowserCommon) 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
|
||||
}
|
||||
|
||||
// GetSafeAccessToken 获取 超级保险柜 AccessToken
|
||||
func (xc *XunLeiBrowserCommon) GetSafeAccessToken(safePassword string) (string, error) {
|
||||
var resp TokenResp
|
||||
_, err := xc.Request(XLUSER_API_URL+"/password/check", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(&base.Json{
|
||||
"scene": "box",
|
||||
"password": EncryptPassword(safePassword),
|
||||
})
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Token == "" {
|
||||
return "", errs.EmptyToken
|
||||
}
|
||||
return resp.Token, nil
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (xc *XunLeiBrowserCommon) 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 *XunLeiBrowserCommon) IsLogin() bool {
|
||||
if xc.TokenResp == nil {
|
||||
return false
|
||||
}
|
||||
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
|
||||
return err == nil
|
||||
}
|
108
drivers/thunder_browser/meta.go
Normal file
108
drivers/thunder_browser/meta.go
Normal file
@ -0,0 +1,108 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
// ExpertAddition 高级设置
|
||||
type ExpertAddition struct {
|
||||
driver.RootID
|
||||
|
||||
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
|
||||
SignType string `json:"sign_type" type:"select" options:"algorithms,captcha_sign" default:"algorithms"`
|
||||
|
||||
// 登录方式1
|
||||
Username string `json:"username" required:"true" help:"login type is user,this is required"`
|
||||
Password string `json:"password" required:"true" help:"login type is user,this is required"`
|
||||
SafePassword string `json:"safe_password" required:"false" help:"login type is user,this is required"` // 超级保险箱密码
|
||||
// 登录方式2
|
||||
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
|
||||
|
||||
// 签名方法1
|
||||
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"x+I5XiTByg,6QU1x5DqGAV3JKg6h,VI1vL1WXr7st0es,n+/3yhlrnKs4ewhLgZhZ5ITpt554,UOip2PE7BLIEov/ZX6VOnsz,Q70h9lpViNCOC8sGVkar9o22LhBTjfP,IVHFuB1JcMlaZHnW,bKE,HZRbwxOiQx+diNopi6Nu,fwyasXgYL3rP314331b,LWxXAiSW4,UlWIjv1HGrC6Ngmt4Nohx,FOa+Lc0bxTDpTwIh2,0+RY,xmRVMqokHHpvsiH0"`
|
||||
// 签名方法2
|
||||
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
|
||||
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
|
||||
|
||||
// 验证码
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
|
||||
// 必要且影响登录,由签名决定
|
||||
DeviceID string `json:"device_id" required:"true" default:"9aa5c268e7bcfc197a9ad88e2fb330e5"`
|
||||
ClientID string `json:"client_id" required:"true" default:"ZUBzD9J_XPXfn7f7"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"yESVmHecEe6F0aou69vl-g"`
|
||||
ClientVersion string `json:"client_version" required:"true" default:"1.0.7.1938"`
|
||||
PackageName string `json:"package_name" required:"true" default:"com.xunlei.browser"`
|
||||
|
||||
// 不影响登录,影响下载速度
|
||||
UserAgent string `json:"user_agent" required:"true" default:"ANDROID-com.xunlei.browser/1.0.7.1938 netWorkType/5G appid/22062 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/233100 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)"`
|
||||
DownloadUserAgent string `json:"download_user_agent" required:"true" default:"AndroidDownloadManager/12 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"`
|
||||
|
||||
// 优先使用视频链接代替下载链接
|
||||
UseVideoUrl bool `json:"use_video_url"`
|
||||
// 移除方式
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
}
|
||||
|
||||
// GetIdentity 登录特征,用于判断是否重新登录
|
||||
func (i *ExpertAddition) GetIdentity() string {
|
||||
hash := md5.New()
|
||||
if i.LoginType == "refresh_token" {
|
||||
hash.Write([]byte(i.RefreshToken))
|
||||
} else {
|
||||
hash.Write([]byte(i.Username + i.Password))
|
||||
}
|
||||
|
||||
if i.SignType == "captcha_sign" {
|
||||
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
|
||||
} else {
|
||||
hash.Write([]byte(i.Algorithms))
|
||||
}
|
||||
|
||||
hash.Write([]byte(i.DeviceID))
|
||||
hash.Write([]byte(i.ClientID))
|
||||
hash.Write([]byte(i.ClientSecret))
|
||||
hash.Write([]byte(i.ClientVersion))
|
||||
hash.Write([]byte(i.PackageName))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
SafePassword string `json:"safe_password" required:"false"` // 超级保险箱密码
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
UseVideoUrl bool `json:"use_video_url" default:"false"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
}
|
||||
|
||||
// GetIdentity 登录特征,用于判断是否重新登录
|
||||
func (i *Addition) GetIdentity() string {
|
||||
return utils.GetMD5EncodeStr(i.Username + i.Password)
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "ThunderBrowser",
|
||||
LocalSort: true,
|
||||
OnlyProxy: true,
|
||||
}
|
||||
|
||||
var configExpert = driver.Config{
|
||||
Name: "ThunderBrowserExpert",
|
||||
LocalSort: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderBrowser{}
|
||||
})
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &ThunderBrowserExpert{}
|
||||
})
|
||||
}
|
223
drivers/thunder_browser/types.go
Normal file
223
drivers/thunder_browser/types.go
Normal file
@ -0,0 +1,223 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
|
||||
)
|
||||
|
||||
type ErrResp struct {
|
||||
ErrorCode int64 `json:"error_code"`
|
||||
ErrorMsg string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
// ErrorDetails interface{} `json:"error_details"`
|
||||
}
|
||||
|
||||
func (e *ErrResp) IsError() bool {
|
||||
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
|
||||
}
|
||||
|
||||
func (e *ErrResp) 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"`
|
||||
ClientID string `json:"client_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Meta map[string]string `json:"meta"`
|
||||
RedirectUri string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type CaptchaTokenResponse struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
/*
|
||||
* 登录
|
||||
**/
|
||||
type TokenResp struct {
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
|
||||
Sub string `json:"sub"`
|
||||
UserID string `json:"user_id"`
|
||||
|
||||
Token string `json:"token"` // "超级保险箱" 访问Token
|
||||
}
|
||||
|
||||
func (t *TokenResp) GetToken() string {
|
||||
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
|
||||
}
|
||||
|
||||
// GetSpaceToken 获取"超级保险箱" 访问Token
|
||||
func (t *TokenResp) GetSpaceToken() string {
|
||||
return t.Token
|
||||
}
|
||||
|
||||
type SignInRequest struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
/*
|
||||
* 文件
|
||||
**/
|
||||
type FileList struct {
|
||||
Kind string `json:"kind"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
Files []Files `json:"files"`
|
||||
Version string `json:"version"`
|
||||
VersionOutdated bool `json:"version_outdated"`
|
||||
FolderType int8
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Expire time.Time `json:"expire"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var _ model.Obj = (*Files)(nil)
|
||||
|
||||
type Files struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
ParentID string `json:"parent_id"`
|
||||
Name string `json:"name"`
|
||||
//UserID string `json:"user_id"`
|
||||
Size string `json:"size"`
|
||||
//Revision string `json:"revision"`
|
||||
//FileExtension string `json:"file_extension"`
|
||||
//MimeType string `json:"mime_type"`
|
||||
//Starred bool `json:"starred"`
|
||||
WebContentLink string `json:"web_content_link"`
|
||||
CreatedTime CustomTime `json:"created_time"`
|
||||
ModifiedTime CustomTime `json:"modified_time"`
|
||||
IconLink string `json:"icon_link"`
|
||||
ThumbnailLink string `json:"thumbnail_link"`
|
||||
// Md5Checksum string `json:"md5_checksum"`
|
||||
Hash string `json:"hash"`
|
||||
// Links map[string]Link `json:"links"`
|
||||
// Phase string `json:"phase"`
|
||||
// Audit struct {
|
||||
// Status string `json:"status"`
|
||||
// Message string `json:"message"`
|
||||
// Title string `json:"title"`
|
||||
// } `json:"audit"`
|
||||
Medias []struct {
|
||||
//Category string `json:"category"`
|
||||
//IconLink string `json:"icon_link"`
|
||||
//IsDefault bool `json:"is_default"`
|
||||
//IsOrigin bool `json:"is_origin"`
|
||||
//IsVisible bool `json:"is_visible"`
|
||||
Link Link `json:"link"`
|
||||
//MediaID string `json:"media_id"`
|
||||
//MediaName string `json:"media_name"`
|
||||
//NeedMoreQuota bool `json:"need_more_quota"`
|
||||
//Priority int `json:"priority"`
|
||||
//RedirectLink string `json:"redirect_link"`
|
||||
//ResolutionName string `json:"resolution_name"`
|
||||
// Video struct {
|
||||
// AudioCodec string `json:"audio_codec"`
|
||||
// BitRate int `json:"bit_rate"`
|
||||
// Duration int `json:"duration"`
|
||||
// FrameRate int `json:"frame_rate"`
|
||||
// Height int `json:"height"`
|
||||
// VideoCodec string `json:"video_codec"`
|
||||
// VideoType string `json:"video_type"`
|
||||
// Width int `json:"width"`
|
||||
// } `json:"video"`
|
||||
// VipTypes []string `json:"vip_types"`
|
||||
} `json:"medias"`
|
||||
Trashed bool `json:"trashed"`
|
||||
DeleteTime string `json:"delete_time"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
//Params struct{} `json:"params"`
|
||||
//OriginalFileIndex int `json:"original_file_index"`
|
||||
//Space string `json:"space"`
|
||||
//Apps []interface{} `json:"apps"`
|
||||
//Writable bool `json:"writable"`
|
||||
FolderType string `json:"folder_type"`
|
||||
//Collection interface{} `json:"collection"`
|
||||
FileType int8
|
||||
}
|
||||
|
||||
func (c *Files) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(hash_extend.GCID, c.Hash)
|
||||
}
|
||||
|
||||
func (c *Files) GetSize() int64 { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }
|
||||
func (c *Files) GetName() string { return c.Name }
|
||||
func (c *Files) CreateTime() time.Time { return c.CreatedTime.Time }
|
||||
func (c *Files) ModTime() time.Time { return c.ModifiedTime.Time }
|
||||
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
|
||||
func (c *Files) GetID() string { return c.ID }
|
||||
func (c *Files) GetPath() string {
|
||||
// 对特殊文件进行特殊处理
|
||||
if c.FileType == ThunderDriveType {
|
||||
return ThunderDriveFileID
|
||||
} else if c.FileType == ThunderBrowserDriveSafeType {
|
||||
return ThunderBrowserDriveSafeFileID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (c *Files) Thumb() string { return c.ThumbnailLink }
|
||||
|
||||
/*
|
||||
* 上传
|
||||
**/
|
||||
type UploadTaskResponse struct {
|
||||
UploadType string `json:"upload_type"`
|
||||
|
||||
/*//UPLOAD_TYPE_FORM
|
||||
Form struct {
|
||||
//Headers struct{} `json:"headers"`
|
||||
Kind string `json:"kind"`
|
||||
Method string `json:"method"`
|
||||
MultiParts struct {
|
||||
OSSAccessKeyID string `json:"OSSAccessKeyId"`
|
||||
Signature string `json:"Signature"`
|
||||
Callback string `json:"callback"`
|
||||
Key string `json:"key"`
|
||||
Policy string `json:"policy"`
|
||||
XUserData string `json:"x:user_data"`
|
||||
} `json:"multi_parts"`
|
||||
URL string `json:"url"`
|
||||
} `json:"form"`*/
|
||||
|
||||
//UPLOAD_TYPE_RESUMABLE
|
||||
Resumable struct {
|
||||
Kind string `json:"kind"`
|
||||
Params struct {
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
AccessKeySecret string `json:"access_key_secret"`
|
||||
Bucket string `json:"bucket"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
Key string `json:"key"`
|
||||
SecurityToken string `json:"security_token"`
|
||||
} `json:"params"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"resumable"`
|
||||
|
||||
File Files `json:"file"`
|
||||
}
|
249
drivers/thunder_browser/util.go
Normal file
249
drivers/thunder_browser/util.go
Normal file
@ -0,0 +1,249 @@
|
||||
package thunder_browser
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
API_URL = "https://x-api-pan.xunlei.com/drive/v1"
|
||||
FILE_API_URL = API_URL + "/files"
|
||||
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
FOLDER = "drive#folder"
|
||||
FILE = "drive#file"
|
||||
RESUMABLE = "drive#resumable"
|
||||
)
|
||||
|
||||
const (
|
||||
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
|
||||
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
|
||||
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
|
||||
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
|
||||
)
|
||||
|
||||
const (
|
||||
ThunderDriveFileID = "XXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
ThunderBrowserDriveSafeFileID = "YYYYYYYYYYYYYYYYYYYYYYYYYY"
|
||||
ThunderDriveFolderName = "迅雷云盘"
|
||||
ThunderBrowserDriveSafeFolderName = "超级保险箱"
|
||||
ThunderDriveType = 1
|
||||
ThunderBrowserDriveSafeType = 2
|
||||
ThunderDriveFolderType = "DEFAULT_ROOT"
|
||||
ThunderBrowserDriveSafeFolderType = "BROWSER_SAFE"
|
||||
)
|
||||
|
||||
func GetAction(method string, url string) string {
|
||||
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
|
||||
return method + ":" + urlpath
|
||||
}
|
||||
|
||||
type Common struct {
|
||||
client *resty.Client
|
||||
|
||||
captchaToken string
|
||||
|
||||
// 签名相关,二选一
|
||||
Algorithms []string
|
||||
Timestamp, CaptchaSign string
|
||||
|
||||
// 必要值,签名相关
|
||||
DeviceID string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ClientVersion string
|
||||
PackageName string
|
||||
UserAgent string
|
||||
DownloadUserAgent string
|
||||
UseVideoUrl bool
|
||||
RemoveWay string
|
||||
|
||||
// 验证码token刷新成功回调
|
||||
refreshCTokenCk func(token string)
|
||||
}
|
||||
|
||||
func (c *Common) SetCaptchaToken(captchaToken string) {
|
||||
c.captchaToken = captchaToken
|
||||
}
|
||||
func (c *Common) GetCaptchaToken() string {
|
||||
return c.captchaToken
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
|
||||
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
|
||||
metas := map[string]string{
|
||||
"client_version": c.ClientVersion,
|
||||
"package_name": c.PackageName,
|
||||
"user_id": userID,
|
||||
}
|
||||
metas["timestamp"], metas["captcha_sign"] = c.GetCaptchaSign()
|
||||
return c.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
|
||||
func (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {
|
||||
metas := make(map[string]string)
|
||||
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
|
||||
metas["email"] = username
|
||||
} else if len(username) >= 11 && len(username) <= 18 {
|
||||
metas["phone_number"] = username
|
||||
} else {
|
||||
metas["username"] = username
|
||||
}
|
||||
return c.refreshCaptchaToken(action, metas)
|
||||
}
|
||||
|
||||
// GetCaptchaSign 获取验证码签名
|
||||
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
|
||||
if len(c.Algorithms) == 0 {
|
||||
return c.Timestamp, c.CaptchaSign
|
||||
}
|
||||
timestamp = fmt.Sprint(time.Now().UnixMilli())
|
||||
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
|
||||
for _, algorithm := range c.Algorithms {
|
||||
str = utils.GetMD5EncodeStr(str + algorithm)
|
||||
}
|
||||
sign = "1." + str
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新验证码token
|
||||
func (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {
|
||||
param := CaptchaTokenRequest{
|
||||
Action: action,
|
||||
CaptchaToken: c.captchaToken,
|
||||
ClientID: c.ClientID,
|
||||
DeviceID: c.DeviceID,
|
||||
Meta: metas,
|
||||
RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor",
|
||||
}
|
||||
var e ErrResp
|
||||
var resp CaptchaTokenResponse
|
||||
_, err := c.Request(XLUSER_API_URL+"/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetError(&e).SetBody(param)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.IsError() {
|
||||
return &e
|
||||
}
|
||||
|
||||
if resp.Url != "" {
|
||||
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
|
||||
}
|
||||
|
||||
if resp.CaptchaToken == "" {
|
||||
return fmt.Errorf("empty captchaToken")
|
||||
}
|
||||
|
||||
if c.refreshCTokenCk != nil {
|
||||
c.refreshCTokenCk(resp.CaptchaToken)
|
||||
}
|
||||
c.SetCaptchaToken(resp.CaptchaToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request 只有基础信息的请求
|
||||
func (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := c.client.R().SetHeaders(map[string]string{
|
||||
"user-agent": c.UserAgent,
|
||||
"accept": "application/json;charset=UTF-8",
|
||||
"x-device-id": c.DeviceID,
|
||||
"x-client-id": c.ClientID,
|
||||
"x-client-version": c.ClientVersion,
|
||||
})
|
||||
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var erron ErrResp
|
||||
utils.Json.Unmarshal(res.Body(), &erron)
|
||||
if erron.IsError() {
|
||||
return nil, &erron
|
||||
}
|
||||
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
// 计算文件Gcid
|
||||
func getGcid(r io.Reader, size int64) (string, error) {
|
||||
calcBlockSize := func(j int64) int64 {
|
||||
var psize int64 = 0x40000
|
||||
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||
psize = psize << 1
|
||||
}
|
||||
return psize
|
||||
}
|
||||
|
||||
hash1 := sha1.New()
|
||||
hash2 := sha1.New()
|
||||
readSize := calcBlockSize(size)
|
||||
for {
|
||||
hash2.Reset()
|
||||
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
|
||||
if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
hash1.Write(hash2.Sum(nil))
|
||||
}
|
||||
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||
}
|
||||
|
||||
type CustomTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
const timeFormat = time.RFC3339
|
||||
|
||||
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
|
||||
str := string(b)
|
||||
if str == `""` {
|
||||
*ct = CustomTime{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(`"`+timeFormat+`"`, str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*ct = CustomTime{Time: t}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncryptPassword 超级保险箱 加密
|
||||
func EncryptPassword(password string) string {
|
||||
if password == "" {
|
||||
return ""
|
||||
}
|
||||
// 将字符串转换为字节数组
|
||||
byteData := []byte(password)
|
||||
// 计算MD5哈希值
|
||||
hash := md5.Sum(byteData)
|
||||
// 将哈希值转换为十六进制字符串
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
@ -515,6 +515,7 @@ func (xc *XunLeiXCommon) Login(username, password string) (*TokenResp, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.UserID = resp.Sub
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user