From bbe3d4e19f6171154b14a5b65fe527007e655607 Mon Sep 17 00:00:00 2001
From: YangXu <47767754+Three-taile-dragon@users.noreply.github.com>
Date: Tue, 21 May 2024 23:24:28 +0800
Subject: [PATCH 1/9] feat: add supports for thunderX driver (#6464)
---
drivers/all.go | 1 +
drivers/thunderx/driver.go | 527 +++++++++++++++++++++++++++++++++++++
drivers/thunderx/meta.go | 103 ++++++++
drivers/thunderx/types.go | 206 +++++++++++++++
drivers/thunderx/util.go | 202 ++++++++++++++
5 files changed, 1039 insertions(+)
create mode 100644 drivers/thunderx/driver.go
create mode 100644 drivers/thunderx/meta.go
create mode 100644 drivers/thunderx/types.go
create mode 100644 drivers/thunderx/util.go
diff --git a/drivers/all.go b/drivers/all.go
index d1d5f84a989..bae72b3175a 100644
--- a/drivers/all.go
+++ b/drivers/all.go
@@ -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/thunderx"
_ "github.com/alist-org/alist/v3/drivers/trainbit"
_ "github.com/alist-org/alist/v3/drivers/url_tree"
_ "github.com/alist-org/alist/v3/drivers/uss"
diff --git a/drivers/thunderx/driver.go b/drivers/thunderx/driver.go
new file mode 100644
index 00000000000..17835d913f2
--- /dev/null
+++ b/drivers/thunderx/driver.go
@@ -0,0 +1,527 @@
+package thunderx
+
+import (
+ "context"
+ "fmt"
+ "github.com/go-resty/resty/v2"
+ "net/http"
+ "strings"
+
+ "github.com/alist-org/alist/v3/drivers/base"
+ "github.com/alist-org/alist/v3/internal/driver"
+ "github.com/alist-org/alist/v3/internal/errs"
+ "github.com/alist-org/alist/v3/internal/model"
+ "github.com/alist-org/alist/v3/internal/op"
+ "github.com/alist-org/alist/v3/pkg/utils"
+ hash_extend "github.com/alist-org/alist/v3/pkg/utils/hash"
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
+)
+
+type ThunderX struct {
+ *XunLeiXCommon
+ model.Storage
+ Addition
+
+ identity string
+}
+
+func (x *ThunderX) Config() driver.Config {
+ return config
+}
+
+func (x *ThunderX) GetAddition() driver.Additional {
+ return &x.Addition
+}
+
+func (x *ThunderX) Init(ctx context.Context) (err error) {
+ // 初始化所需参数
+ if x.XunLeiXCommon == nil {
+ x.XunLeiXCommon = &XunLeiXCommon{
+ Common: &Common{
+ client: base.NewRestyClient(),
+ Algorithms: []string{
+ "lHwINjLeqssT28Ym99p5MvR",
+ "xvFcxvtqPKCa9Ajf",
+ "2ywOP8spKHzfuhZMUYZ9IpsViq0t8vT0",
+ "FTBrJism20SHKQ2m2",
+ "BHrWJsPwjnr5VeLtOUr2191X9uXhWmt",
+ "yu0QgHEjNmDoPNwXN17so2hQlDT83T",
+ "OcaMfLMCGZ7oYlvZGIbTqb4U7cCY",
+ "jBGGu0GzXOjtCXYwkOBb+c6TZ/Nymv",
+ "YLWRjVor2rOuYEL",
+ "94wjoPazejyNC+gRpOj+JOm1XXvxa",
+ },
+ DeviceID: utils.GetMD5EncodeStr(x.Username + x.Password),
+ ClientID: "ZQL_zwA4qhHcoe_2",
+ ClientSecret: "Og9Vr1L8Ee6bh0olFxFDRg",
+ ClientVersion: "1.05.0.2115",
+ PackageName: "com.thunder.downloader",
+ UserAgent: "ANDROID-com.thunder.downloader/1.05.0.2115 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
+ DownloadUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
+ 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.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
+ x.Addition.RootFolderID = x.RootFolderID
+ // 防止重复登录
+ identity := x.GetIdentity()
+ if x.identity != identity || !x.IsLogin() {
+ x.identity = identity
+ // 登录
+ token, err := x.Login(x.Username, x.Password)
+ if err != nil {
+ return err
+ }
+ x.SetTokenResp(token)
+ }
+ return nil
+}
+
+func (x *ThunderX) Drop(ctx context.Context) error {
+ return nil
+}
+
+type ThunderXExpert struct {
+ *XunLeiXCommon
+ model.Storage
+ ExpertAddition
+
+ identity string
+}
+
+func (x *ThunderXExpert) Config() driver.Config {
+ return configExpert
+}
+
+func (x *ThunderXExpert) GetAddition() driver.Additional {
+ return &x.ExpertAddition
+}
+
+func (x *ThunderXExpert) Init(ctx context.Context) (err error) {
+ // 防止重复登录
+ identity := x.GetIdentity()
+ if identity != x.identity || !x.IsLogin() {
+ x.identity = identity
+ x.XunLeiXCommon = &XunLeiXCommon{
+ Common: &Common{
+ client: base.NewRestyClient(),
+
+ DeviceID: func() string {
+ if len(x.DeviceID) != 32 {
+ 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.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
+ x.ExpertAddition.RootFolderID = x.RootFolderID
+ // 签名方法
+ if x.SignType == "captcha_sign" {
+ x.Common.Timestamp = x.Timestamp
+ x.Common.CaptchaSign = x.CaptchaSign
+ } else {
+ x.Common.Algorithms = strings.Split(x.Algorithms, ",")
+ }
+
+ // 登录方式
+ if x.LoginType == "refresh_token" {
+ // 通过RefreshToken登录
+ token, err := x.XunLeiXCommon.RefreshToken(x.ExpertAddition.RefreshToken)
+ if err != nil {
+ return err
+ }
+ x.SetTokenResp(token)
+
+ // 刷新token方法
+ x.SetRefreshTokenFunc(func() error {
+ token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)
+ if err != nil {
+ x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
+ }
+ x.SetTokenResp(token)
+ op.MustSaveDriverStorage(x)
+ return err
+ })
+ } else {
+ // 通过用户密码登录
+ token, err := x.Login(x.Username, x.Password)
+ if err != nil {
+ return err
+ }
+ x.SetTokenResp(token)
+ x.SetRefreshTokenFunc(func() error {
+ token, err := x.XunLeiXCommon.RefreshToken(x.TokenResp.RefreshToken)
+ if err != nil {
+ token, err = x.Login(x.Username, x.Password)
+ if err != nil {
+ x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
+ }
+ }
+ x.SetTokenResp(token)
+ op.MustSaveDriverStorage(x)
+ return err
+ })
+ }
+ } else {
+ // 仅修改验证码token
+ if x.CaptchaToken != "" {
+ x.SetCaptchaToken(x.CaptchaToken)
+ }
+ x.XunLeiXCommon.UserAgent = x.UserAgent
+ x.XunLeiXCommon.DownloadUserAgent = x.DownloadUserAgent
+ x.XunLeiXCommon.UseVideoUrl = x.UseVideoUrl
+ x.ExpertAddition.RootFolderID = x.RootFolderID
+ }
+ return nil
+}
+
+func (x *ThunderXExpert) Drop(ctx context.Context) error {
+ return nil
+}
+
+func (x *ThunderXExpert) SetTokenResp(token *TokenResp) {
+ x.XunLeiXCommon.SetTokenResp(token)
+ if token != nil {
+ x.ExpertAddition.RefreshToken = token.RefreshToken
+ }
+}
+
+type XunLeiXCommon struct {
+ *Common
+ *TokenResp // 登录信息
+
+ refreshTokenFunc func() error
+}
+
+func (xc *XunLeiXCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+ return xc.getFiles(ctx, dir.GetID())
+}
+
+func (xc *XunLeiXCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+ var lFile Files
+ _, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetPathParam("fileID", file.GetID())
+ //r.SetQueryParam("space", "")
+ }, &lFile)
+ if err != nil {
+ return nil, err
+ }
+ link := &model.Link{
+ URL: lFile.WebContentLink,
+ Header: http.Header{
+ "User-Agent": {xc.DownloadUserAgent},
+ },
+ }
+
+ if xc.UseVideoUrl {
+ for _, media := range lFile.Medias {
+ if media.Link.URL != "" {
+ link.URL = media.Link.URL
+ break
+ }
+ }
+ }
+
+ /*
+ strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)
+ if len(strs) == 2 {
+ timestamp, err := strconv.ParseInt(strs[1], 10, 64)
+ if err == nil {
+ expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
+ link.Expiration = &expired
+ }
+ }
+ */
+ return link, nil
+}
+
+func (xc *XunLeiXCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
+ _, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetBody(&base.Json{
+ "kind": FOLDER,
+ "name": dirName,
+ "parent_id": parentDir.GetID(),
+ })
+ }, nil)
+ return err
+}
+
+func (xc *XunLeiXCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
+ _, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetBody(&base.Json{
+ "to": base.Json{"parent_id": dstDir.GetID()},
+ "ids": []string{srcObj.GetID()},
+ })
+ }, nil)
+ return err
+}
+
+func (xc *XunLeiXCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
+ _, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetPathParam("fileID", srcObj.GetID())
+ r.SetBody(&base.Json{"name": newName})
+ }, nil)
+ return err
+}
+
+func (xc *XunLeiXCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
+ _, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetBody(&base.Json{
+ "to": base.Json{"parent_id": dstDir.GetID()},
+ "ids": []string{srcObj.GetID()},
+ })
+ }, nil)
+ return err
+}
+
+func (xc *XunLeiXCommon) Remove(ctx context.Context, obj model.Obj) error {
+ _, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetPathParam("fileID", obj.GetID())
+ r.SetBody("{}")
+ }, nil)
+ return err
+}
+
+func (xc *XunLeiXCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
+ hi := stream.GetHash()
+ gcid := hi.GetHash(hash_extend.GCID)
+ if len(gcid) < hash_extend.GCID.Width {
+ tFile, err := stream.CacheFullInTempFile()
+ if err != nil {
+ return err
+ }
+
+ gcid, err = utils.HashFile(hash_extend.GCID, tFile, stream.GetSize())
+ if err != nil {
+ return err
+ }
+ }
+
+ var resp UploadTaskResponse
+ _, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetBody(&base.Json{
+ "kind": FILE,
+ "parent_id": dstDir.GetID(),
+ "name": stream.GetName(),
+ "size": stream.GetSize(),
+ "hash": gcid,
+ "upload_type": UPLOAD_TYPE_RESUMABLE,
+ })
+ }, &resp)
+ if err != nil {
+ return err
+ }
+
+ param := resp.Resumable.Params
+ if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
+ param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
+ s, err := session.NewSession(&aws.Config{
+ Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
+ Region: aws.String("xunlei"),
+ Endpoint: aws.String(param.Endpoint),
+ })
+ if err != nil {
+ return err
+ }
+ uploader := s3manager.NewUploader(s)
+ if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
+ uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
+ }
+ _, err = uploader.UploadWithContext(ctx, &s3manager.UploadInput{
+ Bucket: aws.String(param.Bucket),
+ Key: aws.String(param.Key),
+ Expires: aws.Time(param.Expiration),
+ Body: stream,
+ })
+ return err
+ }
+ return nil
+}
+
+func (xc *XunLeiXCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {
+ files := make([]model.Obj, 0)
+ var pageToken string
+ for {
+ var fileList FileList
+ _, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
+ r.SetContext(ctx)
+ r.SetQueryParams(map[string]string{
+ "space": "",
+ "__type": "drive",
+ "refresh": "true",
+ "__sync": "true",
+ "parent_id": folderId,
+ "page_token": pageToken,
+ "with_audit": "true",
+ "limit": "100",
+ "filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
+ })
+ }, &fileList)
+ if err != nil {
+ return nil, err
+ }
+
+ for i := 0; i < len(fileList.Files); i++ {
+ files = append(files, &fileList.Files[i])
+ }
+
+ if fileList.NextPageToken == "" {
+ break
+ }
+ pageToken = fileList.NextPageToken
+ }
+ return files, nil
+}
+
+// 设置刷新Token的方法
+func (xc *XunLeiXCommon) SetRefreshTokenFunc(fn func() error) {
+ xc.refreshTokenFunc = fn
+}
+
+// 设置Token
+func (xc *XunLeiXCommon) SetTokenResp(tr *TokenResp) {
+ xc.TokenResp = tr
+}
+
+// 携带Authorization和CaptchaToken的请求
+func (xc *XunLeiXCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ data, err := xc.Common.Request(url, method, func(req *resty.Request) {
+ req.SetHeaders(map[string]string{
+ "Authorization": xc.Token(),
+ "X-Captcha-Token": xc.GetCaptchaToken(),
+ })
+ if callback != nil {
+ callback(req)
+ }
+ }, resp)
+
+ errResp, ok := err.(*ErrResp)
+ if !ok {
+ return nil, err
+ }
+
+ switch errResp.ErrorCode {
+ case 0:
+ return data, nil
+ case 4122, 4121, 10, 16:
+ if xc.refreshTokenFunc != nil {
+ if err = xc.refreshTokenFunc(); err == nil {
+ break
+ }
+ }
+ return nil, err
+ case 9: // 验证码token过期
+ if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
+ return nil, err
+ }
+ default:
+ return nil, err
+ }
+ return xc.Request(url, method, callback, resp)
+}
+
+// 刷新Token
+func (xc *XunLeiXCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
+ var resp TokenResp
+ _, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
+ req.SetBody(&base.Json{
+ "grant_type": "refresh_token",
+ "refresh_token": refreshToken,
+ "client_id": xc.ClientID,
+ "client_secret": xc.ClientSecret,
+ })
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.RefreshToken == "" {
+ return nil, errs.EmptyToken
+ }
+ return &resp, nil
+}
+
+// 登录
+func (xc *XunLeiXCommon) Login(username, password string) (*TokenResp, error) {
+ url := XLUSER_API_URL + "/auth/signin"
+ err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
+ if err != nil {
+ return nil, err
+ }
+
+ var resp TokenResp
+ _, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
+ req.SetBody(&SignInRequest{
+ CaptchaToken: xc.GetCaptchaToken(),
+ ClientID: xc.ClientID,
+ ClientSecret: xc.ClientSecret,
+ Username: username,
+ Password: password,
+ })
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
+
+func (xc *XunLeiXCommon) IsLogin() bool {
+ if xc.TokenResp == nil {
+ return false
+ }
+ _, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
+ return err == nil
+}
diff --git a/drivers/thunderx/meta.go b/drivers/thunderx/meta.go
new file mode 100644
index 00000000000..2c114c0f284
--- /dev/null
+++ b/drivers/thunderx/meta.go
@@ -0,0 +1,103 @@
+package thunderx
+
+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"
+)
+
+// 高级设置
+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"`
+ // 登录方式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:"lHwINjLeqssT28Ym99p5MvR,xvFcxvtqPKCa9Ajf,2ywOP8spKHzfuhZMUYZ9IpsViq0t8vT0,FTBrJism20SHKQ2m2,BHrWJsPwjnr5VeLtOUr2191X9uXhWmt,yu0QgHEjNmDoPNwXN17so2hQlDT83T,OcaMfLMCGZ7oYlvZGIbTqb4U7cCY,jBGGu0GzXOjtCXYwkOBb+c6TZ/Nymv,YLWRjVor2rOuYEL,94wjoPazejyNC+gRpOj+JOm1XXvxa"`
+ // 签名方法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:"ZQL_zwA4qhHcoe_2"`
+ ClientSecret string `json:"client_secret" required:"true" default:"Og9Vr1L8Ee6bh0olFxFDRg"`
+ ClientVersion string `json:"client_version" required:"true" default:"1.05.0.2115"`
+ PackageName string `json:"package_name" required:"true" default:"com.thunder.downloader"`
+
+ //不影响登录,影响下载速度
+ UserAgent string `json:"user_agent" required:"true" default:"ANDROID-com.thunder.downloader/1.05.0.2115 netWorkType/4G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gdcf98eab238b) (JAVA 0)"`
+ DownloadUserAgent string `json:"download_user_agent" required:"true" default:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"`
+
+ //优先使用视频链接代替下载链接
+ UseVideoUrl bool `json:"use_video_url"`
+}
+
+// 登录特征,用于判断是否重新登录
+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"`
+ CaptchaToken string `json:"captcha_token"`
+ UseVideoUrl bool `json:"use_video_url" default:"true"`
+}
+
+// 登录特征,用于判断是否重新登录
+func (i *Addition) GetIdentity() string {
+ return utils.GetMD5EncodeStr(i.Username + i.Password)
+}
+
+var config = driver.Config{
+ Name: "ThunderX",
+ LocalSort: true,
+ OnlyProxy: true,
+}
+
+var configExpert = driver.Config{
+ Name: "ThunderXExpert",
+ LocalSort: true,
+}
+
+func init() {
+ op.RegisterDriver(func() driver.Driver {
+ return &ThunderX{}
+ })
+ op.RegisterDriver(func() driver.Driver {
+ return &ThunderXExpert{}
+ })
+}
diff --git a/drivers/thunderx/types.go b/drivers/thunderx/types.go
new file mode 100644
index 00000000000..77cfa0f2415
--- /dev/null
+++ b/drivers/thunderx/types.go
@@ -0,0 +1,206 @@
+package thunderx
+
+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"`
+}
+
+func (t *TokenResp) Token() string {
+ return fmt.Sprint(t.TokenType, " ", t.AccessToken)
+}
+
+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"`
+}
+
+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 time.Time `json:"created_time"`
+ ModifiedTime time.Time `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"`
+}
+
+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 }
+func (c *Files) ModTime() time.Time { return c.ModifiedTime }
+func (c *Files) IsDir() bool { return c.Kind == FOLDER }
+func (c *Files) GetID() string { return c.ID }
+func (c *Files) GetPath() string { 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"`
+}
diff --git a/drivers/thunderx/util.go b/drivers/thunderx/util.go
new file mode 100644
index 00000000000..6fa323ebc28
--- /dev/null
+++ b/drivers/thunderx/util.go
@@ -0,0 +1,202 @@
+package thunderx
+
+import (
+ "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://api-pan.xunleix.com/drive/v1"
+ FILE_API_URL = API_URL + "/files"
+ XLUSER_API_URL = "https://xluser-ssl.xunleix.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"
+)
+
+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
+
+ // 验证码token刷新成功回调
+ refreshCTokenCk func(token string)
+}
+
+func (c *Common) SetCaptchaToken(captchaToken string) {
+ c.captchaToken = captchaToken
+}
+func (c *Common) GetCaptchaToken() string {
+ return c.captchaToken
+}
+
+// 刷新验证码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)
+}
+
+// 刷新验证码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)
+}
+
+// 获取验证码签名
+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://xbase.cloud/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: Click Here`, resp.Url)
+ }
+
+ if resp.CaptchaToken == "" {
+ return fmt.Errorf("empty captchaToken")
+ }
+
+ if c.refreshCTokenCk != nil {
+ c.refreshCTokenCk(resp.CaptchaToken)
+ }
+ c.SetCaptchaToken(resp.CaptchaToken)
+ return nil
+}
+
+// 只有基础信息的请求
+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
+}
From 037850bbd5096707c43e05b361b9b154d1a2d183 Mon Sep 17 00:00:00 2001
From: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com>
Date: Wed, 22 May 2024 09:27:48 +0800
Subject: [PATCH 2/9] feat(alias): support Rename and Remove (#6478)
* feat(alias): support `Rename` and `Remove`
* fix(alias): `autoFlatten` not updated after editing
* feat(alias): add `protect_same_name` option
---
drivers/alias/driver.go | 26 ++++++++++++++++++++++++++
drivers/alias/meta.go | 9 +++++++--
drivers/alias/util.go | 33 +++++++++++++++++++++++++++++++++
internal/errs/errors.go | 4 ++++
4 files changed, 70 insertions(+), 2 deletions(-)
diff --git a/drivers/alias/driver.go b/drivers/alias/driver.go
index 271096b3e46..d9b290edc65 100644
--- a/drivers/alias/driver.go
+++ b/drivers/alias/driver.go
@@ -7,6 +7,7 @@ import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
+ "github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
)
@@ -45,6 +46,9 @@ func (d *Alias) Init(ctx context.Context) error {
d.oneKey = k
}
d.autoFlatten = true
+ } else {
+ d.oneKey = ""
+ d.autoFlatten = false
}
return nil
}
@@ -111,4 +115,26 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, errs.ObjectNotFound
}
+func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
+ reqPath, err := d.getReqPath(ctx, srcObj)
+ if err == nil {
+ return fs.Rename(ctx, *reqPath, newName)
+ }
+ if errs.IsNotImplement(err) {
+ return errors.New("same-name files cannot be Rename")
+ }
+ return err
+}
+
+func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
+ reqPath, err := d.getReqPath(ctx, obj)
+ if err == nil {
+ return fs.Remove(ctx, *reqPath)
+ }
+ if errs.IsNotImplement(err) {
+ return errors.New("same-name files cannot be Delete")
+ }
+ return err
+}
+
var _ driver.Driver = (*Alias)(nil)
diff --git a/drivers/alias/meta.go b/drivers/alias/meta.go
index 6611e1dc302..2bebe7b00a8 100644
--- a/drivers/alias/meta.go
+++ b/drivers/alias/meta.go
@@ -9,7 +9,8 @@ type Addition struct {
// Usually one of two
// driver.RootPath
// define other
- Paths string `json:"paths" required:"true" type:"text"`
+ Paths string `json:"paths" required:"true" type:"text"`
+ ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
}
var config = driver.Config{
@@ -22,6 +23,10 @@ var config = driver.Config{
func init() {
op.RegisterDriver(func() driver.Driver {
- return &Alias{}
+ return &Alias{
+ Addition: Addition{
+ ProtectSameName: true,
+ },
+ }
})
}
diff --git a/drivers/alias/util.go b/drivers/alias/util.go
index 4e3d6bf0f5c..803bd0736b9 100644
--- a/drivers/alias/util.go
+++ b/drivers/alias/util.go
@@ -6,6 +6,7 @@ import (
stdpath "path"
"strings"
+ "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/sign"
@@ -112,3 +113,35 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
link, _, err := fs.Link(ctx, reqPath, args)
return link, err
}
+
+func (d *Alias) getReqPath(ctx context.Context, obj model.Obj) (*string, error) {
+ root, sub := d.getRootAndPath(obj.GetPath())
+ if sub == "" || sub == "/" {
+ return nil, errs.NotSupport
+ }
+ dsts, ok := d.pathMap[root]
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+ var reqPath string
+ var err error
+ for _, dst := range dsts {
+ reqPath = stdpath.Join(dst, sub)
+ _, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
+ if err == nil {
+ if d.ProtectSameName {
+ if ok {
+ ok = false
+ } else {
+ return nil, errs.NotImplement
+ }
+ } else {
+ break
+ }
+ }
+ }
+ if err != nil {
+ return nil, errs.ObjectNotFound
+ }
+ return &reqPath, nil
+}
diff --git a/internal/errs/errors.go b/internal/errs/errors.go
index cd681e607b3..ecfe43e3dc0 100644
--- a/internal/errs/errors.go
+++ b/internal/errs/errors.go
@@ -3,6 +3,7 @@ package errs
import (
"errors"
"fmt"
+
pkgerr "github.com/pkg/errors"
)
@@ -33,3 +34,6 @@ func IsNotFoundError(err error) bool {
func IsNotSupportError(err error) bool {
return errors.Is(pkgerr.Cause(err), NotSupport)
}
+func IsNotImplement(err error) bool {
+ return errors.Is(pkgerr.Cause(err), NotImplement)
+}
From 9eec87263726fbfbe7b786e3a7d142b8d1a8dbc4 Mon Sep 17 00:00:00 2001
From: Kuingsmile
Date: Wed, 22 May 2024 23:28:14 +0800
Subject: [PATCH 3/9] feat(mega): add 2FA support (#6473)
* feat(mega): add support for two-factor authentication in Mega driver #6226
* feat(mega): remove debug print statement in Mega driver Init function
* feat(mega): add help message for new field
---
drivers/mega/driver.go | 11 ++++++++++-
drivers/mega/meta.go | 6 ++++--
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/drivers/mega/driver.go b/drivers/mega/driver.go
index 9fa1d0eeafa..162aeef37e0 100644
--- a/drivers/mega/driver.go
+++ b/drivers/mega/driver.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/alist-org/alist/v3/pkg/http_range"
+ "github.com/pquerna/otp/totp"
"github.com/rclone/rclone/lib/readers"
"github.com/alist-org/alist/v3/internal/driver"
@@ -33,8 +34,16 @@ func (d *Mega) GetAddition() driver.Additional {
}
func (d *Mega) Init(ctx context.Context) error {
+ var twoFACode = d.TwoFACode
d.c = mega.New()
- return d.c.Login(d.Email, d.Password)
+ if d.TwoFASecret != "" {
+ code, err := totp.GenerateCode(d.TwoFASecret, time.Now())
+ if err != nil {
+ return fmt.Errorf("generate totp code failed: %w", err)
+ }
+ twoFACode = code
+ }
+ return d.c.MultiFactorLogin(d.Email, d.Password, twoFACode)
}
func (d *Mega) Drop(ctx context.Context) error {
diff --git a/drivers/mega/meta.go b/drivers/mega/meta.go
index 77e768f0542..d0758637bb3 100644
--- a/drivers/mega/meta.go
+++ b/drivers/mega/meta.go
@@ -9,8 +9,10 @@ type Addition struct {
// Usually one of two
//driver.RootPath
//driver.RootID
- Email string `json:"email" required:"true"`
- Password string `json:"password" required:"true"`
+ Email string `json:"email" required:"true"`
+ Password string `json:"password" required:"true"`
+ TwoFACode string `json:"two_fa_code" required:"false" help:"2FA 6-digit code, filling in the 2FA code alone will not support reloading driver"`
+ TwoFASecret string `json:"two_fa_secret" required:"false" help:"2FA secret"`
}
var config = driver.Config{
From 7013d1b7b86705488e714e8a8dcbf3729938e01e Mon Sep 17 00:00:00 2001
From: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date: Wed, 22 May 2024 23:29:29 +0800
Subject: [PATCH 4/9] fix: pikpak captcha_required (#6497)
* fix: pikpak captcha_required
* fix(pikpak_share): video download
---
drivers/pikpak/driver.go | 33 +++++++++--
drivers/pikpak/meta.go | 2 +
drivers/pikpak/util.go | 101 +++------------------------------
drivers/pikpak_share/driver.go | 36 ++++++++++--
drivers/pikpak_share/meta.go | 10 ++--
drivers/pikpak_share/util.go | 70 ++---------------------
6 files changed, 81 insertions(+), 171 deletions(-)
diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index f3676b8291b..e27263ddbb7 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -17,13 +17,14 @@ import (
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
+ "golang.org/x/oauth2"
)
type PikPak struct {
model.Storage
Addition
- RefreshToken string
- AccessToken string
+
+ oauth2Token oauth2.TokenSource
}
func (d *PikPak) Config() driver.Config {
@@ -34,8 +35,32 @@ func (d *PikPak) GetAddition() driver.Additional {
return &d.Addition
}
-func (d *PikPak) Init(ctx context.Context) error {
- return d.login()
+func (d *PikPak) Init(ctx context.Context) (err error) {
+ if d.ClientID == "" || d.ClientSecret == "" {
+ d.ClientID = "YNxT9w7GMdWvEOKa"
+ d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
+ }
+
+ withClient := func(ctx context.Context) context.Context {
+ return context.WithValue(ctx, oauth2.HTTPClient, base.HttpClient)
+ }
+
+ oauth2Config := &oauth2.Config{
+ ClientID: d.ClientID,
+ ClientSecret: d.ClientSecret,
+ Endpoint: oauth2.Endpoint{
+ AuthURL: "https://user.mypikpak.com/v1/auth/signin",
+ TokenURL: "https://user.mypikpak.com/v1/auth/token",
+ AuthStyle: oauth2.AuthStyleInParams,
+ },
+ }
+
+ oauth2Token, err := oauth2Config.PasswordCredentialsToken(withClient(ctx), d.Username, d.Password)
+ if err != nil {
+ return err
+ }
+ d.oauth2Token = oauth2Config.TokenSource(withClient(context.Background()), oauth2Token)
+ return nil
}
func (d *PikPak) Drop(ctx context.Context) error {
diff --git a/drivers/pikpak/meta.go b/drivers/pikpak/meta.go
index 3512c44b93b..c462ed13d6b 100644
--- a/drivers/pikpak/meta.go
+++ b/drivers/pikpak/meta.go
@@ -9,6 +9,8 @@ type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
+ ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
+ ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
DisableMediaLink bool `json:"disable_media_link"`
}
diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go
index 71ad1dca8a3..0edfc384eba 100644
--- a/drivers/pikpak/util.go
+++ b/drivers/pikpak/util.go
@@ -1,78 +1,24 @@
package pikpak
import (
- "crypto/sha1"
- "encoding/hex"
"errors"
- "github.com/alist-org/alist/v3/pkg/utils"
- "io"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
- "github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2"
- jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
-func (d *PikPak) login() error {
- url := "https://user.mypikpak.com/v1/auth/signin"
- var e RespErr
- res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
- "captcha_token": "",
- "client_id": "YNxT9w7GMdWvEOKa",
- "client_secret": "dbw2OtmVEeuUvIptb1Coyg",
- "username": d.Username,
- "password": d.Password,
- }).Post(url)
- if err != nil {
- return err
- }
- if e.ErrorCode != 0 {
- return errors.New(e.Error)
- }
- data := res.Body()
- d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
- d.AccessToken = jsoniter.Get(data, "access_token").ToString()
- return nil
-}
+func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ req := base.RestyClient.R()
-func (d *PikPak) refreshToken() error {
- url := "https://user.mypikpak.com/v1/auth/token"
- var e RespErr
- res, err := base.RestyClient.R().SetError(&e).
- SetHeader("user-agent", "").SetBody(base.Json{
- "client_id": "YNxT9w7GMdWvEOKa",
- "client_secret": "dbw2OtmVEeuUvIptb1Coyg",
- "grant_type": "refresh_token",
- "refresh_token": d.RefreshToken,
- }).Post(url)
+ token, err := d.oauth2Token.Token()
if err != nil {
- d.Status = err.Error()
- op.MustSaveDriverStorage(d)
- return err
- }
- if e.ErrorCode != 0 {
- if e.ErrorCode == 4126 {
- // refresh_token invalid, re-login
- return d.login()
- }
- d.Status = e.Error
- op.MustSaveDriverStorage(d)
- return errors.New(e.Error)
+ return nil, err
}
- data := res.Body()
- d.Status = "work"
- d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
- d.AccessToken = jsoniter.Get(data, "access_token").ToString()
- op.MustSaveDriverStorage(d)
- return nil
-}
+ req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
-func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- req := base.RestyClient.R()
- req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if callback != nil {
callback(req)
}
@@ -85,17 +31,9 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
if err != nil {
return nil, err
}
+
if e.ErrorCode != 0 {
- if e.ErrorCode == 16 {
- // login / refresh token
- err = d.refreshToken()
- if err != nil {
- return nil, err
- }
- return d.request(url, method, callback, resp)
- } else {
- return nil, errors.New(e.Error)
- }
+ return nil, errors.New(e.Error)
}
return res.Body(), nil
}
@@ -127,28 +65,3 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
}
return res, nil
}
-
-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
-}
diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go
index 3003ff48860..58c2c8c4b55 100644
--- a/drivers/pikpak_share/driver.go
+++ b/drivers/pikpak_share/driver.go
@@ -4,17 +4,18 @@ import (
"context"
"net/http"
+ "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
+ "golang.org/x/oauth2"
)
type PikPakShare struct {
model.Storage
Addition
- RefreshToken string
- AccessToken string
+ oauth2Token oauth2.TokenSource
PassCodeToken string
}
@@ -27,10 +28,31 @@ func (d *PikPakShare) GetAddition() driver.Additional {
}
func (d *PikPakShare) Init(ctx context.Context) error {
- err := d.login()
+ if d.ClientID == "" || d.ClientSecret == "" {
+ d.ClientID = "YNxT9w7GMdWvEOKa"
+ d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
+ }
+
+ withClient := func(ctx context.Context) context.Context {
+ return context.WithValue(ctx, oauth2.HTTPClient, base.HttpClient)
+ }
+
+ oauth2Config := &oauth2.Config{
+ ClientID: d.ClientID,
+ ClientSecret: d.ClientSecret,
+ Endpoint: oauth2.Endpoint{
+ AuthURL: "https://user.mypikpak.com/v1/auth/signin",
+ TokenURL: "https://user.mypikpak.com/v1/auth/token",
+ AuthStyle: oauth2.AuthStyleInParams,
+ },
+ }
+
+ oauth2Token, err := oauth2Config.PasswordCredentialsToken(withClient(ctx), d.Username, d.Password)
if err != nil {
return err
}
+ d.oauth2Token = oauth2Config.TokenSource(withClient(context.Background()), oauth2Token)
+
if d.SharePwd != "" {
err = d.getSharePassToken()
if err != nil {
@@ -67,8 +89,14 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
if err != nil {
return nil, err
}
+
+ downloadUrl := resp.FileInfo.WebContentLink
+ if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 {
+ downloadUrl = resp.FileInfo.Medias[0].Link.Url
+ }
+
link := model.Link{
- URL: resp.FileInfo.WebContentLink,
+ URL: downloadUrl,
}
return &link, nil
}
diff --git a/drivers/pikpak_share/meta.go b/drivers/pikpak_share/meta.go
index bf77e22b3cf..5d05badb590 100644
--- a/drivers/pikpak_share/meta.go
+++ b/drivers/pikpak_share/meta.go
@@ -7,10 +7,12 @@ import (
type Addition struct {
driver.RootID
- Username string `json:"username" required:"true"`
- Password string `json:"password" required:"true"`
- ShareId string `json:"share_id" required:"true"`
- SharePwd string `json:"share_pwd"`
+ Username string `json:"username" required:"true"`
+ Password string `json:"password" required:"true"`
+ ShareId string `json:"share_id" required:"true"`
+ SharePwd string `json:"share_pwd"`
+ ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
+ ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
}
var config = driver.Config{
diff --git a/drivers/pikpak_share/util.go b/drivers/pikpak_share/util.go
index e62f17842c6..41bb30d4aa6 100644
--- a/drivers/pikpak_share/util.go
+++ b/drivers/pikpak_share/util.go
@@ -5,70 +5,18 @@ import (
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
- "github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2"
- jsoniter "github.com/json-iterator/go"
)
-// do others that not defined in Driver interface
-
-func (d *PikPakShare) login() error {
- url := "https://user.mypikpak.com/v1/auth/signin"
- var e RespErr
- res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
- "captcha_token": "",
- "client_id": "YNxT9w7GMdWvEOKa",
- "client_secret": "dbw2OtmVEeuUvIptb1Coyg",
- "username": d.Username,
- "password": d.Password,
- }).Post(url)
- if err != nil {
- return err
- }
- if e.ErrorCode != 0 {
- return errors.New(e.Error)
- }
- data := res.Body()
- d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
- d.AccessToken = jsoniter.Get(data, "access_token").ToString()
- return nil
-}
+func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ req := base.RestyClient.R()
-func (d *PikPakShare) refreshToken() error {
- url := "https://user.mypikpak.com/v1/auth/token"
- var e RespErr
- res, err := base.RestyClient.R().SetError(&e).
- SetHeader("user-agent", "").SetBody(base.Json{
- "client_id": "YNxT9w7GMdWvEOKa",
- "client_secret": "dbw2OtmVEeuUvIptb1Coyg",
- "grant_type": "refresh_token",
- "refresh_token": d.RefreshToken,
- }).Post(url)
+ token, err := d.oauth2Token.Token()
if err != nil {
- d.Status = err.Error()
- op.MustSaveDriverStorage(d)
- return err
- }
- if e.ErrorCode != 0 {
- if e.ErrorCode == 4126 {
- // refresh_token invalid, re-login
- return d.login()
- }
- d.Status = e.Error
- op.MustSaveDriverStorage(d)
- return errors.New(e.Error)
+ return nil, err
}
- data := res.Body()
- d.Status = "work"
- d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
- d.AccessToken = jsoniter.Get(data, "access_token").ToString()
- op.MustSaveDriverStorage(d)
- return nil
-}
+ req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
-func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- req := base.RestyClient.R()
- req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if callback != nil {
callback(req)
}
@@ -82,14 +30,6 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
return nil, err
}
if e.ErrorCode != 0 {
- if e.ErrorCode == 16 {
- // login / refresh token
- err = d.refreshToken()
- if err != nil {
- return nil, err
- }
- return d.request(url, method, callback, resp)
- }
return nil, errors.New(e.Error)
}
return res.Body(), nil
From 5f60b51cf8abd48265904b82cb0a7ff0f21a099f Mon Sep 17 00:00:00 2001
From: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com>
Date: Wed, 22 May 2024 23:31:42 +0800
Subject: [PATCH 5/9] feat: add `proxy_range` option for `139Yun` `Alias`
`AList V3` (#6496)
---
drivers/139/meta.go | 9 ++++++---
drivers/alias/meta.go | 11 ++++++-----
drivers/alias/util.go | 8 ++++++--
drivers/alist_v3/meta.go | 9 +++++----
internal/driver/config.go | 1 +
internal/model/storage.go | 1 +
internal/op/driver.go | 11 +++++++++++
server/common/proxy.go | 20 ++++++++++++++++++++
server/handles/down.go | 3 +++
server/webdav/webdav.go | 3 +++
10 files changed, 62 insertions(+), 14 deletions(-)
diff --git a/drivers/139/meta.go b/drivers/139/meta.go
index 416e63a796c..56a4c1df96b 100644
--- a/drivers/139/meta.go
+++ b/drivers/139/meta.go
@@ -14,12 +14,15 @@ type Addition struct {
}
var config = driver.Config{
- Name: "139Yun",
- LocalSort: true,
+ Name: "139Yun",
+ LocalSort: true,
+ ProxyRangeOption: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
- return &Yun139{}
+ d := &Yun139{}
+ d.ProxyRange = true
+ return d
})
}
diff --git a/drivers/alias/meta.go b/drivers/alias/meta.go
index 2bebe7b00a8..45b885753d0 100644
--- a/drivers/alias/meta.go
+++ b/drivers/alias/meta.go
@@ -14,11 +14,12 @@ type Addition struct {
}
var config = driver.Config{
- Name: "Alias",
- LocalSort: true,
- NoCache: true,
- NoUpload: true,
- DefaultRoot: "/",
+ Name: "Alias",
+ LocalSort: true,
+ NoCache: true,
+ NoUpload: true,
+ DefaultRoot: "/",
+ ProxyRangeOption: true,
}
func init() {
diff --git a/drivers/alias/util.go b/drivers/alias/util.go
index 803bd0736b9..ba1f7e72649 100644
--- a/drivers/alias/util.go
+++ b/drivers/alias/util.go
@@ -103,12 +103,16 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
return nil, err
}
if common.ShouldProxy(storage, stdpath.Base(sub)) {
- return &model.Link{
+ link := &model.Link{
URL: fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(args.HttpReq),
utils.EncodePath(reqPath, true),
sign.Sign(reqPath)),
- }, nil
+ }
+ if args.HttpReq != nil && d.ProxyRange {
+ link.RangeReadCloser = common.NoProxyRange
+ }
+ return link, nil
}
link, _, err := fs.Link(ctx, reqPath, args)
return link, err
diff --git a/drivers/alist_v3/meta.go b/drivers/alist_v3/meta.go
index c04c737b149..cc5f2189395 100644
--- a/drivers/alist_v3/meta.go
+++ b/drivers/alist_v3/meta.go
@@ -16,10 +16,11 @@ type Addition struct {
}
var config = driver.Config{
- Name: "AList V3",
- LocalSort: true,
- DefaultRoot: "/",
- CheckStatus: true,
+ Name: "AList V3",
+ LocalSort: true,
+ DefaultRoot: "/",
+ CheckStatus: true,
+ ProxyRangeOption: true,
}
func init() {
diff --git a/internal/driver/config.go b/internal/driver/config.go
index c9e3f949af0..6068143cb71 100644
--- a/internal/driver/config.go
+++ b/internal/driver/config.go
@@ -12,6 +12,7 @@ type Config struct {
CheckStatus bool `json:"-"`
Alert string `json:"alert"` //info,success,warning,danger
NoOverwriteUpload bool `json:"-"` // whether to support overwrite upload
+ ProxyRangeOption bool `json:"-"`
}
func (c Config) MustProxy() bool {
diff --git a/internal/model/storage.go b/internal/model/storage.go
index 1045a00767b..14bcf45f6e2 100644
--- a/internal/model/storage.go
+++ b/internal/model/storage.go
@@ -27,6 +27,7 @@ type Sort struct {
type Proxy struct {
WebProxy bool `json:"web_proxy"`
WebdavPolicy string `json:"webdav_policy"`
+ ProxyRange bool `json:"proxy_range"`
DownProxyUrl string `json:"down_proxy_url"`
}
diff --git a/internal/op/driver.go b/internal/op/driver.go
index 66d1ae3ce9c..4f10e8e23c6 100644
--- a/internal/op/driver.go
+++ b/internal/op/driver.go
@@ -93,6 +93,17 @@ func getMainItems(config driver.Config) []driver.Item {
Required: true,
},
}...)
+ if config.ProxyRangeOption {
+ item := driver.Item{
+ Name: "proxy_range",
+ Type: conf.TypeBool,
+ Help: "Need to enable proxy",
+ }
+ if config.Name == "139Yun" {
+ item.Default = "true"
+ }
+ items = append(items, item)
+ }
} else {
items = append(items, driver.Item{
Name: "webdav_policy",
diff --git a/server/common/proxy.go b/server/common/proxy.go
index 4ca4ba7f6ab..10923613ede 100644
--- a/server/common/proxy.go
+++ b/server/common/proxy.go
@@ -9,8 +9,10 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/net"
+ "github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
+ log "github.com/sirupsen/logrus"
)
func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.Obj) error {
@@ -82,3 +84,21 @@ func attachFileName(w http.ResponseWriter, file model.Obj) {
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, fileName, url.PathEscape(fileName)))
w.Header().Set("Content-Type", utils.GetMimeType(fileName))
}
+
+var NoProxyRange = &model.RangeReadCloser{}
+
+func ProxyRange(link *model.Link, size int64) {
+ if link.MFile != nil {
+ return
+ }
+ if link.RangeReadCloser == nil {
+ var rrc, err = stream.GetRangeReadCloserFromLink(size, link)
+ if err != nil {
+ log.Warnf("ProxyRange error: %s", err)
+ return
+ }
+ link.RangeReadCloser = rrc
+ } else if link.RangeReadCloser == NoProxyRange {
+ link.RangeReadCloser = nil
+ }
+}
diff --git a/server/handles/down.go b/server/handles/down.go
index d3d41e85a2b..0020ed1453e 100644
--- a/server/handles/down.go
+++ b/server/handles/down.go
@@ -106,6 +106,9 @@ func Proxy(c *gin.Context) {
return
}
}
+ if storage.GetStorage().ProxyRange {
+ common.ProxyRange(link, file.GetSize())
+ }
err = common.Proxy(c.Writer, c.Request, link, file)
if err != nil {
common.ErrorResp(c, err, 500, true)
diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go
index 6054991a0c2..b84e65b06b7 100644
--- a/server/webdav/webdav.go
+++ b/server/webdav/webdav.go
@@ -247,6 +247,9 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
if err != nil {
return http.StatusInternalServerError, err
}
+ if storage.GetStorage().ProxyRange {
+ common.ProxyRange(link, fi.GetSize())
+ }
err = common.Proxy(w, r, link, fi)
if err != nil {
log.Errorf("webdav proxy error: %+v", err)
From 85d743c5d26236d4f48ee1a909083364973d03cb Mon Sep 17 00:00:00 2001
From: WintBit
Date: Wed, 22 May 2024 23:31:58 +0800
Subject: [PATCH 6/9] feat: add support for lark driver (#6475)
* feat: lark storage driver
* feat: external view mode
* limit lark targets
* fix: missing package
---------
Co-authored-by: Andy Hsu
---
drivers/lark.go | 8 +
drivers/lark/driver.go | 396 +++++++++++++++++++++++++++++++++++++++++
drivers/lark/meta.go | 36 ++++
drivers/lark/types.go | 32 ++++
drivers/lark/util.go | 66 +++++++
go.mod | 3 +-
go.sum | 18 ++
7 files changed, 558 insertions(+), 1 deletion(-)
create mode 100644 drivers/lark.go
create mode 100644 drivers/lark/driver.go
create mode 100644 drivers/lark/meta.go
create mode 100644 drivers/lark/types.go
create mode 100644 drivers/lark/util.go
diff --git a/drivers/lark.go b/drivers/lark.go
new file mode 100644
index 00000000000..0fbbaddc09b
--- /dev/null
+++ b/drivers/lark.go
@@ -0,0 +1,8 @@
+// +build linux darwin
+// +build amd64 arm64
+
+package drivers
+
+import (
+ _ "github.com/alist-org/alist/v3/drivers/lark"
+)
\ No newline at end of file
diff --git a/drivers/lark/driver.go b/drivers/lark/driver.go
new file mode 100644
index 00000000000..6783aa5241d
--- /dev/null
+++ b/drivers/lark/driver.go
@@ -0,0 +1,396 @@
+package lark
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "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/ipfs/boxo/path"
+ lark "github.com/larksuite/oapi-sdk-go/v3"
+ larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
+ larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
+ "golang.org/x/time/rate"
+ "io"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+type Lark struct {
+ model.Storage
+ Addition
+
+ client *lark.Client
+ rootFolderToken string
+}
+
+func (c *Lark) Config() driver.Config {
+ return config
+}
+
+func (c *Lark) GetAddition() driver.Additional {
+ return &c.Addition
+}
+
+func (c *Lark) Init(ctx context.Context) error {
+ c.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache()))
+
+ paths := path.SplitList(c.RootFolderPath)
+ token := ""
+
+ var ok bool
+ var file *larkdrive.File
+ for _, p := range paths {
+ if p == "" {
+ token = ""
+ continue
+ }
+
+ resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
+ if err != nil {
+ return err
+ }
+
+ for {
+ ok, file, err = resp.Next()
+ if !ok {
+ return errs.ObjectNotFound
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if *file.Type == "folder" && *file.Name == p {
+ token = *file.Token
+ break
+ }
+ }
+ }
+
+ c.rootFolderToken = token
+
+ return nil
+}
+
+func (c *Lark) Drop(ctx context.Context) error {
+ return nil
+}
+
+func (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+ token, ok := c.getObjToken(ctx, dir.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ if token == emptyFolderToken {
+ return nil, nil
+ }
+
+ resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
+ if err != nil {
+ return nil, err
+ }
+
+ ok = false
+ var file *larkdrive.File
+ var res []model.Obj
+
+ for {
+ ok, file, err = resp.Next()
+ if !ok {
+ break
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ modifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64)
+ createdUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64)
+
+ f := model.Object{
+ ID: *file.Token,
+ Path: path.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}),
+ Name: *file.Name,
+ Size: 0,
+ Modified: time.Unix(modifiedUnix, 0),
+ Ctime: time.Unix(createdUnix, 0),
+ IsFolder: *file.Type == "folder",
+ }
+ res = append(res, &f)
+ }
+
+ return res, nil
+}
+
+func (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+ token, ok := c.getObjToken(ctx, file.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ resp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{
+ AppID: c.AppId,
+ AppSecret: c.AppSecret,
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ if !c.ExternalMode {
+ accessToken := resp.TenantAccessToken
+
+ url := fmt.Sprintf("https://open.feishu.cn/open-apis/drive/v1/files/%s/download", token)
+
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
+ req.Header.Set("Range", "bytes=0-1")
+
+ ar, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if ar.StatusCode != http.StatusPartialContent {
+ return nil, errors.New("failed to get download link")
+ }
+
+ return &model.Link{
+ URL: url,
+ Header: http.Header{
+ "Authorization": []string{fmt.Sprintf("Bearer %s", accessToken)},
+ },
+ }, nil
+ } else {
+ url := path.Join([]string{c.TenantUrlPrefix, "file", token})
+
+ return &model.Link{
+ URL: url,
+ }, nil
+ }
+}
+
+func (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
+ token, ok := c.getObjToken(ctx, parentDir.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ body, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build()
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.client.Drive.File.CreateFolder(ctx,
+ larkdrive.NewCreateFolderFileReqBuilder().Body(body).Build())
+ if err != nil {
+ return nil, err
+ }
+
+ if !resp.Success() {
+ return nil, errors.New(resp.Error())
+ }
+
+ return &model.Object{
+ ID: *resp.Data.Token,
+ Path: path.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}),
+ Name: dirName,
+ Size: 0,
+ IsFolder: true,
+ }, nil
+}
+
+func (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+ srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ req := larkdrive.NewMoveFileReqBuilder().
+ Body(larkdrive.NewMoveFileReqBodyBuilder().
+ Type("file").
+ FolderToken(dstDirToken).
+ Build()).FileToken(srcToken).
+ Build()
+
+ // 发起请求
+ resp, err := c.client.Drive.File.Move(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ if !resp.Success() {
+ return nil, errors.New(resp.Error())
+ }
+
+ return nil, nil
+}
+
+func (c *Lark) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
+ // TODO rename obj, optional
+ return nil, errs.NotImplement
+}
+
+func (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+ srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ req := larkdrive.NewCopyFileReqBuilder().
+ Body(larkdrive.NewCopyFileReqBodyBuilder().
+ Name(srcObj.GetName()).
+ Type("file").
+ FolderToken(dstDirToken).
+ Build()).FileToken(srcToken).
+ Build()
+
+ // 发起请求
+ resp, err := c.client.Drive.File.Copy(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ if !resp.Success() {
+ return nil, errors.New(resp.Error())
+ }
+
+ return nil, nil
+}
+
+func (c *Lark) Remove(ctx context.Context, obj model.Obj) error {
+ token, ok := c.getObjToken(ctx, obj.GetPath())
+ if !ok {
+ return errs.ObjectNotFound
+ }
+
+ req := larkdrive.NewDeleteFileReqBuilder().
+ FileToken(token).
+ Type("file").
+ Build()
+
+ // 发起请求
+ resp, err := c.client.Drive.File.Delete(ctx, req)
+ if err != nil {
+ return err
+ }
+
+ if !resp.Success() {
+ return errors.New(resp.Error())
+ }
+
+ return nil
+}
+
+var uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5)
+
+func (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
+ token, ok := c.getObjToken(ctx, dstDir.GetPath())
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+
+ // prepare
+ req := larkdrive.NewUploadPrepareFileReqBuilder().
+ FileUploadInfo(larkdrive.NewFileUploadInfoBuilder().
+ FileName(stream.GetName()).
+ ParentType(`explorer`).
+ ParentNode(token).
+ Size(int(stream.GetSize())).
+ Build()).
+ Build()
+
+ // 发起请求
+ uploadLimit.Wait(ctx)
+ resp, err := c.client.Drive.File.UploadPrepare(ctx, req)
+ if err != nil {
+ return nil, err
+ }
+
+ if !resp.Success() {
+ return nil, errors.New(resp.Error())
+ }
+
+ uploadId := *resp.Data.UploadId
+ blockSize := *resp.Data.BlockSize
+ blockCount := *resp.Data.BlockNum
+
+ // upload
+ for i := 0; i < blockCount; i++ {
+ length := int64(blockSize)
+ if i == blockCount-1 {
+ length = stream.GetSize() - int64(i*blockSize)
+ }
+
+ reader := io.LimitReader(stream, length)
+
+ req := larkdrive.NewUploadPartFileReqBuilder().
+ Body(larkdrive.NewUploadPartFileReqBodyBuilder().
+ UploadId(uploadId).
+ Seq(i).
+ Size(int(length)).
+ File(reader).
+ Build()).
+ Build()
+
+ // 发起请求
+ uploadLimit.Wait(ctx)
+ resp, err := c.client.Drive.File.UploadPart(ctx, req)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if !resp.Success() {
+ return nil, errors.New(resp.Error())
+ }
+
+ up(float64(i) / float64(blockCount))
+ }
+
+ //close
+ closeReq := larkdrive.NewUploadFinishFileReqBuilder().
+ Body(larkdrive.NewUploadFinishFileReqBodyBuilder().
+ UploadId(uploadId).
+ BlockNum(blockCount).
+ Build()).
+ Build()
+
+ // 发起请求
+ closeResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq)
+ if err != nil {
+ return nil, err
+ }
+
+ if !closeResp.Success() {
+ return nil, errors.New(closeResp.Error())
+ }
+
+ return &model.Object{
+ ID: *closeResp.Data.FileToken,
+ }, nil
+}
+
+//func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
+// return nil, errs.NotSupport
+//}
+
+var _ driver.Driver = (*Lark)(nil)
diff --git a/drivers/lark/meta.go b/drivers/lark/meta.go
new file mode 100644
index 00000000000..221345e222c
--- /dev/null
+++ b/drivers/lark/meta.go
@@ -0,0 +1,36 @@
+package lark
+
+import (
+ "github.com/alist-org/alist/v3/internal/driver"
+ "github.com/alist-org/alist/v3/internal/op"
+)
+
+type Addition struct {
+ // Usually one of two
+ driver.RootPath
+ // define other
+ AppId string `json:"app_id" type:"text" help:"app id"`
+ AppSecret string `json:"app_secret" type:"text" help:"app secret"`
+ ExternalMode bool `json:"external_mode" type:"bool" help:"external mode"`
+ TenantUrlPrefix string `json:"tenant_url_prefix" type:"text" help:"tenant url prefix"`
+}
+
+var config = driver.Config{
+ Name: "Lark",
+ LocalSort: false,
+ OnlyLocal: false,
+ OnlyProxy: false,
+ NoCache: false,
+ NoUpload: false,
+ NeedMs: false,
+ DefaultRoot: "/",
+ CheckStatus: false,
+ Alert: "",
+ NoOverwriteUpload: true,
+}
+
+func init() {
+ op.RegisterDriver(func() driver.Driver {
+ return &Lark{}
+ })
+}
diff --git a/drivers/lark/types.go b/drivers/lark/types.go
new file mode 100644
index 00000000000..3ebefd556dc
--- /dev/null
+++ b/drivers/lark/types.go
@@ -0,0 +1,32 @@
+package lark
+
+import (
+ "context"
+ "github.com/Xhofe/go-cache"
+ "time"
+)
+
+type TokenCache struct {
+ cache.ICache[string]
+}
+
+func (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error {
+ t.ICache.Set(key, value, cache.WithEx[string](expireTime))
+
+ return nil
+}
+
+func (t *TokenCache) Get(_ context.Context, key string) (string, error) {
+ v, ok := t.ICache.Get(key)
+ if ok {
+ return v, nil
+ }
+
+ return "", nil
+}
+
+func newTokenCache() *TokenCache {
+ c := cache.NewMemCache[string]()
+
+ return &TokenCache{c}
+}
diff --git a/drivers/lark/util.go b/drivers/lark/util.go
new file mode 100644
index 00000000000..8c6828bd176
--- /dev/null
+++ b/drivers/lark/util.go
@@ -0,0 +1,66 @@
+package lark
+
+import (
+ "context"
+ "github.com/Xhofe/go-cache"
+ larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
+ log "github.com/sirupsen/logrus"
+ "path"
+ "time"
+)
+
+const objTokenCacheDuration = 5 * time.Minute
+const emptyFolderToken = "empty"
+
+var objTokenCache = cache.NewMemCache[string]()
+var exOpts = cache.WithEx[string](objTokenCacheDuration)
+
+func (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) {
+ if token, ok := objTokenCache.Get(folderPath); ok {
+ return token, true
+ }
+
+ dir, name := path.Split(folderPath)
+ // strip the last slash of dir if it exists
+ if len(dir) > 0 && dir[len(dir)-1] == '/' {
+ dir = dir[:len(dir)-1]
+ }
+ if name == "" {
+ return c.rootFolderToken, true
+ }
+
+ var parentToken string
+ var found bool
+ parentToken, found = c.getObjToken(ctx, dir)
+ if !found {
+ return emptyFolderToken, false
+ }
+
+ req := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build()
+ resp, err := c.client.Drive.File.ListByIterator(ctx, req)
+
+ if err != nil {
+ log.WithError(err).Error("failed to list files")
+ return emptyFolderToken, false
+ }
+
+ var file *larkdrive.File
+ for {
+ found, file, err = resp.Next()
+ if !found {
+ break
+ }
+
+ if err != nil {
+ log.WithError(err).Error("failed to get next file")
+ break
+ }
+
+ if *file.Name == name {
+ objTokenCache.Set(folderPath, *file.Token, exOpts)
+ return *file.Token, true
+ }
+ }
+
+ return emptyFolderToken, false
+}
diff --git a/go.mod b/go.mod
index a994d7688f7..118c5a35001 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
github.com/charmbracelet/lipgloss v0.9.1
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/deckarep/golang-set/v2 v2.6.0
+ github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/disintegration/imaging v1.6.2
github.com/djherbis/times v1.6.0
github.com/dlclark/regexp2 v1.10.0
@@ -36,6 +37,7 @@ require (
github.com/ipfs/go-ipfs-api v0.7.0
github.com/jlaffaye/ftp v0.2.0
github.com/json-iterator/go v1.1.12
+ github.com/larksuite/oapi-sdk-go/v3 v3.2.5
github.com/maruel/natural v1.1.1
github.com/meilisearch/meilisearch-go v0.26.1
github.com/minio/sio v0.3.0
@@ -108,7 +110,6 @@ require (
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
- github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/geoffgarside/ber v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 539151e660a..d8bc53dbb9a 100644
--- a/go.sum
+++ b/go.sum
@@ -184,6 +184,7 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
@@ -215,6 +216,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvki
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -261,6 +263,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg=
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
@@ -281,6 +284,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/larksuite/oapi-sdk-go/v3 v3.2.5 h1:MkmkfCHzvmi35EId9SeFPJMZ8bUsijnxwneAWHnnk0k=
+github.com/larksuite/oapi-sdk-go/v3 v3.2.5/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
@@ -483,6 +488,8 @@ github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3K
github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0=
github.com/xhofe/tache v0.1.1 h1:O5QY4cVjIGELx3UGh6LbVAc18MWGXgRNQjMt72x6w/8=
github.com/xhofe/tache v0.1.1/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -497,6 +504,7 @@ golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -514,10 +522,14 @@ golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -534,6 +546,8 @@ golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
@@ -596,12 +610,16 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw=
google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
From d781f7127a356ac389512ce1a70ae00f2dcd9023 Mon Sep 17 00:00:00 2001
From: Andy Hsu
Date: Thu, 23 May 2024 11:52:37 +0800
Subject: [PATCH 7/9] fix: add lark to windows target
---
drivers/lark.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/lark.go b/drivers/lark.go
index 0fbbaddc09b..d5070078651 100644
--- a/drivers/lark.go
+++ b/drivers/lark.go
@@ -1,8 +1,8 @@
-// +build linux darwin
+// +build linux darwin windows
// +build amd64 arm64
package drivers
import (
_ "github.com/alist-org/alist/v3/drivers/lark"
-)
\ No newline at end of file
+)
From 0a8d710e01cdf161d4bf6d52a869ff0a2259885e Mon Sep 17 00:00:00 2001
From: foxxorcat <95907542+foxxorcat@users.noreply.github.com>
Date: Thu, 23 May 2024 18:56:17 +0800
Subject: [PATCH 8/9] fix(mopan): upgrade version (#6500)
---
go.mod | 3 ++-
go.sum | 6 ++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index 118c5a35001..9dd012c9667 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
github.com/djherbis/times v1.6.0
github.com/dlclark/regexp2 v1.10.0
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564
- github.com/foxxorcat/mopan-sdk-go v0.1.5
+ github.com/foxxorcat/mopan-sdk-go v0.1.6
github.com/foxxorcat/weiyun-sdk-go v0.1.3
github.com/gaoyb7/115drive-webdav v0.1.8
github.com/gin-contrib/cors v1.5.0
@@ -129,6 +129,7 @@ require (
github.com/google/go-tpm v0.9.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-version v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/boxo v0.12.0 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
diff --git a/go.sum b/go.sum
index d8bc53dbb9a..c4f6832bcf1 100644
--- a/go.sum
+++ b/go.sum
@@ -133,8 +133,8 @@ github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
-github.com/foxxorcat/mopan-sdk-go v0.1.5 h1:N3LqOvk2aWWxszsFIkArP5udIv74uTei/bH2jM3tfSc=
-github.com/foxxorcat/mopan-sdk-go v0.1.5/go.mod h1:iWHA2JFhzmKR28ySp1ON0g6DjLaYtvb5jhTqPVTDW9A=
+github.com/foxxorcat/mopan-sdk-go v0.1.6 h1:6J37oI4wMZLj8EPgSCcSTTIbnI5D6RCNW/srX8vQd1Y=
+github.com/foxxorcat/mopan-sdk-go v0.1.6/go.mod h1:UaY6D88yBXWGrcu/PcyLWyL4lzrk5pSxSABPHftOvxs=
github.com/foxxorcat/weiyun-sdk-go v0.1.3 h1:I5c5nfGErhq9DBumyjCVCggRA74jhgriMqRRFu5jeeY=
github.com/foxxorcat/weiyun-sdk-go v0.1.3/go.mod h1:TPxzN0d2PahweUEHlOBWlwZSA+rELSUlGYMWgXRn9ps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -224,6 +224,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
+github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
From 8e2b9c681a47ce0cf75c619000c8bc62cf5968b1 Mon Sep 17 00:00:00 2001
From: Andy Hsu
Date: Thu, 23 May 2024 20:05:00 +0800
Subject: [PATCH 9/9] fix(ilanzou): upgrade devVersion
---
drivers/ilanzou/driver.go | 10 ++++++++--
drivers/ilanzou/meta.go | 4 ++--
drivers/ilanzou/util.go | 5 +++--
3 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/drivers/ilanzou/driver.go b/drivers/ilanzou/driver.go
index 1d8e5d36b09..63d86363962 100644
--- a/drivers/ilanzou/driver.go
+++ b/drivers/ilanzou/driver.go
@@ -147,7 +147,8 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
// get the url after redirect
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
//"Origin": d.conf.site,
- "Referer": d.conf.site + "/",
+ "Referer": d.conf.site + "/",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
}).Get(realURL)
if err != nil {
return nil, err
@@ -155,7 +156,12 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
if res.StatusCode() == 302 {
realURL = res.Header().Get("location")
} else {
- return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
+ contentLengthStr := res.Header().Get("Content-Length")
+ contentLength, err := strconv.Atoi(contentLengthStr)
+ if err != nil || contentLength == 0 || contentLength > 1024*10 {
+ return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
+ }
+ return nil, fmt.Errorf("redirect failed, content: %s", res.String())
}
link := model.Link{URL: realURL}
return &link, nil
diff --git a/drivers/ilanzou/meta.go b/drivers/ilanzou/meta.go
index ed5b2edb52e..f15fc01a492 100644
--- a/drivers/ilanzou/meta.go
+++ b/drivers/ilanzou/meta.go
@@ -46,7 +46,7 @@ func init() {
bucket: "wpanstore-lanzou",
unproved: "unproved",
proved: "proved",
- devVersion: "122",
+ devVersion: "125",
site: "https://www.ilanzou.com",
},
}
@@ -72,7 +72,7 @@ func init() {
bucket: "wpanstore",
unproved: "ws",
proved: "app",
- devVersion: "121",
+ devVersion: "125",
site: "https://www.feijipan.com",
},
}
diff --git a/drivers/ilanzou/util.go b/drivers/ilanzou/util.go
index 37e111ad53e..c9a30765b7f 100644
--- a/drivers/ilanzou/util.go
+++ b/drivers/ilanzou/util.go
@@ -59,8 +59,9 @@ func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, pr
"extra": "2",
})
req.SetHeaders(map[string]string{
- "Origin": d.conf.site,
- "Referer": d.conf.site + "/",
+ "Origin": d.conf.site,
+ "Referer": d.conf.site + "/",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
})
if proved {
req.SetQueryParam("appToken", d.Token)