* 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/teambition"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
_ "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/thunderx"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
_ "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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
resp.UserID = resp.Sub
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user