mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-11-25 19:37:41 +08:00
Compare commits
54 Commits
v4.1.4
...
renovate/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
005a9867b4 | ||
|
|
ca401b9af9 | ||
|
|
addce8b691 | ||
|
|
42fc841dc1 | ||
|
|
4c0916b64b | ||
|
|
3989d35abd | ||
|
|
72e2ae1f14 | ||
|
|
3e37f575d8 | ||
|
|
c0d480366d | ||
|
|
9de7561154 | ||
|
|
0866b9075f | ||
|
|
055696f576 | ||
|
|
854415160c | ||
|
|
8f4f7d1291 | ||
|
|
ee2c77acd8 | ||
|
|
fc90ec1b53 | ||
|
|
7d78944d14 | ||
|
|
f2e0fe8589 | ||
|
|
39dcf9bd19 | ||
|
|
25f38df4ca | ||
|
|
a1f1f98f94 | ||
|
|
affc499913 | ||
|
|
c7574b545c | ||
|
|
9e852ba12d | ||
|
|
174eae802a | ||
|
|
b9f058fcc9 | ||
|
|
6de15b6310 | ||
|
|
2844797684 | ||
|
|
9f4e439478 | ||
|
|
9d09ee133d | ||
|
|
d88f0e8f3c | ||
|
|
0857478516 | ||
|
|
66d9809057 | ||
|
|
db8a7e8caf | ||
|
|
8f18e34da0 | ||
|
|
525f26dc23 | ||
|
|
a0fcfa3ed2 | ||
|
|
15f276537c | ||
|
|
623a12050e | ||
|
|
ae2d2d1021 | ||
|
|
a109152a13 | ||
|
|
febbcd6027 | ||
|
|
549e60136b | ||
|
|
14d2b8290a | ||
|
|
cdc069d8e7 | ||
|
|
fb5094f688 | ||
|
|
670e0bdc45 | ||
|
|
89235012af | ||
|
|
2bfbad2874 | ||
|
|
4ba7696032 | ||
|
|
66645516e5 | ||
|
|
eb2ff2d2ca | ||
|
|
4153245f2c | ||
|
|
6fe9af7819 |
16
.github/ISSUE_TEMPLATE/00-bug_report_zh.yml
vendored
16
.github/ISSUE_TEMPLATE/00-bug_report_zh.yml
vendored
@@ -13,7 +13,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 请确认以下事项
|
label: 请确认以下事项
|
||||||
description: |
|
description: |
|
||||||
您必须勾选以下内容,否则您的问题可能会被直接关闭。
|
您必须确认、同意并勾选以下内容,否则您的问题一定会被直接关闭。
|
||||||
或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。
|
或者您可以去[讨论区](https://github.com/OpenListTeam/OpenList/discussions)。
|
||||||
options:
|
options:
|
||||||
- label: |
|
- label: |
|
||||||
@@ -59,6 +59,14 @@ body:
|
|||||||
label: 问题描述(必填)
|
label: 问题描述(必填)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: 日志(必填)
|
||||||
|
description: |
|
||||||
|
请复制粘贴错误日志,或者截图。(可隐藏隐私字段) [查看方法](https://doc.oplist.org/faq/howto#%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8Dbug)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: config
|
id: config
|
||||||
attributes:
|
attributes:
|
||||||
@@ -67,12 +75,6 @@ body:
|
|||||||
请提供您的`OpenList`应用的配置文件,并截图相关存储配置。(可隐藏隐私字段)
|
请提供您的`OpenList`应用的配置文件,并截图相关存储配置。(可隐藏隐私字段)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: 日志(可选)
|
|
||||||
description: |
|
|
||||||
请复制粘贴错误日志,或者截图。(可隐藏隐私字段) [查看方法](https://doc.oplist.org/faq/howto#%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8Dbug)
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
16
.github/ISSUE_TEMPLATE/01-bug_report_en.yml
vendored
16
.github/ISSUE_TEMPLATE/01-bug_report_en.yml
vendored
@@ -13,7 +13,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Please confirm the following
|
label: Please confirm the following
|
||||||
description: |
|
description: |
|
||||||
You must check all the following, otherwise your issue may be closed directly.
|
You must confirm, agree, and check all the following, otherwise your issue will definitely be closed directly.
|
||||||
Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions).
|
Or you can go to the [discussions](https://github.com/OpenListTeam/OpenList/discussions).
|
||||||
options:
|
options:
|
||||||
- label: |
|
- label: |
|
||||||
@@ -59,6 +59,14 @@ body:
|
|||||||
label: Bug Description (required)
|
label: Bug Description (required)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs (required)
|
||||||
|
description: |
|
||||||
|
Please copy and paste any relevant log output or screenshots. (You may mask sensitive fields) [Guide](https://doc.oplist.org/faq/howto#how-to-quickly-locate-bugs)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: config
|
id: config
|
||||||
attributes:
|
attributes:
|
||||||
@@ -67,12 +75,6 @@ body:
|
|||||||
Please provide your `OpenList` application's configuration file and a screenshot of the relevant storage configuration. (You may mask sensitive fields)
|
Please provide your `OpenList` application's configuration file and a screenshot of the relevant storage configuration. (You may mask sensitive fields)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Logs (optional)
|
|
||||||
description: |
|
|
||||||
Please copy and paste any relevant log output or screenshots. (You may mask sensitive fields) [Guide](https://doc.oplist.org/faq/howto#how-to-quickly-locate-bugs)
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ Thank you for your support and understanding of the OpenList project.
|
|||||||
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
||||||
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
|
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
|
||||||
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
|
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
|
||||||
- [x] [Mediatrack](https://www.mediatrack.cn)
|
|
||||||
- [x] [MediaFire](https://www.mediafire.com)
|
- [x] [MediaFire](https://www.mediafire.com)
|
||||||
|
- [x] [Mediatrack](https://www.mediatrack.cn)
|
||||||
|
- [x] [ProtonDrive](https://proton.me/drive)
|
||||||
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
|
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
|
||||||
- [x] [YandexDisk](https://disk.yandex.com)
|
- [x] [YandexDisk](https://disk.yandex.com)
|
||||||
- [x] [BaiduNetdisk](http://pan.baidu.com)
|
- [x] [BaiduNetdisk](http://pan.baidu.com)
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
|
|||||||
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
|
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
|
||||||
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
|
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
|
||||||
- [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.com))
|
- [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.com))
|
||||||
- [x] [分秒帧](https://www.mediatrack.cn)
|
|
||||||
- [x] [MediaFire](https://www.mediafire.com)
|
- [x] [MediaFire](https://www.mediafire.com)
|
||||||
|
- [x] [分秒帧](https://www.mediatrack.cn)
|
||||||
|
- [x] [ProtonDrive](https://proton.me/drive)
|
||||||
- [x] [和彩云](https://yun.139.com)(个人、家庭、群组)
|
- [x] [和彩云](https://yun.139.com)(个人、家庭、群组)
|
||||||
- [x] [YandexDisk](https://disk.yandex.com)
|
- [x] [YandexDisk](https://disk.yandex.com)
|
||||||
- [x] [百度网盘](http://pan.baidu.com)
|
- [x] [百度网盘](http://pan.baidu.com)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
|
|||||||
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
|
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
|
||||||
- [x] Teambition([中国](https://www.teambition.com), [国際](https://us.teambition.com))
|
- [x] Teambition([中国](https://www.teambition.com), [国際](https://us.teambition.com))
|
||||||
- [x] [Mediatrack](https://www.mediatrack.cn)
|
- [x] [Mediatrack](https://www.mediatrack.cn)
|
||||||
|
- [x] [ProtonDrive](https://proton.me/drive)
|
||||||
- [x] [139yun](https://yun.139.com)(個人、家族、グループ)
|
- [x] [139yun](https://yun.139.com)(個人、家族、グループ)
|
||||||
- [x] [YandexDisk](https://disk.yandex.com)
|
- [x] [YandexDisk](https://disk.yandex.com)
|
||||||
- [x] [BaiduNetdisk](http://pan.baidu.com)
|
- [x] [BaiduNetdisk](http://pan.baidu.com)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ Dank u voor uw ondersteuning en begrip
|
|||||||
- [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.com))
|
- [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.com))
|
||||||
- [x] [MediaFire](https://www.mediafire.com)
|
- [x] [MediaFire](https://www.mediafire.com)
|
||||||
- [x] [Mediatrack](https://www.mediatrack.cn)
|
- [x] [Mediatrack](https://www.mediatrack.cn)
|
||||||
|
- [x] [ProtonDrive](https://proton.me/drive)
|
||||||
- [x] [139yun](https://yun.139.com) (Persoonlijk, Familie, Groep)
|
- [x] [139yun](https://yun.139.com) (Persoonlijk, Familie, Groep)
|
||||||
- [x] [YandexDisk](https://disk.yandex.com)
|
- [x] [YandexDisk](https://disk.yandex.com)
|
||||||
- [x] [BaiduNetdisk](http://pan.baidu.com)
|
- [x] [BaiduNetdisk](http://pan.baidu.com)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package flags
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
DataDir string
|
DataDir string
|
||||||
|
ConfigPath string
|
||||||
Debug bool
|
Debug bool
|
||||||
NoPrefix bool
|
NoPrefix bool
|
||||||
Dev bool
|
Dev bool
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
|
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data directory (relative paths are resolved against the current working directory)")
|
||||||
|
RootCmd.PersistentFlags().StringVar(&flags.ConfigPath, "config", "", "path to config.json (relative to current working directory; defaults to [data directory]/config.json, where [data directory] is set by --data)")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
|
||||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
||||||
|
"github.com/quic-go/quic-go/http3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerCmd represents the server command
|
// ServerCmd represents the server command
|
||||||
@@ -63,6 +65,7 @@ the address is defined in config file`,
|
|||||||
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
httpHandler = h2c.NewHandler(r, &http2.Server{})
|
||||||
}
|
}
|
||||||
var httpSrv, httpsSrv, unixSrv *http.Server
|
var httpSrv, httpsSrv, unixSrv *http.Server
|
||||||
|
var quicSrv *http3.Server
|
||||||
if conf.Conf.Scheme.HttpPort != -1 {
|
if conf.Conf.Scheme.HttpPort != -1 {
|
||||||
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
|
||||||
fmt.Printf("start HTTP server @ %s\n", httpBase)
|
fmt.Printf("start HTTP server @ %s\n", httpBase)
|
||||||
@@ -86,6 +89,24 @@ the address is defined in config file`,
|
|||||||
utils.Log.Fatalf("failed to start https: %s", err.Error())
|
utils.Log.Fatalf("failed to start https: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
if conf.Conf.Scheme.EnableH3 {
|
||||||
|
fmt.Printf("start HTTP3 (quic) server @ %s\n", httpsBase)
|
||||||
|
utils.Log.Infof("start HTTP3 (quic) server @ %s", httpsBase)
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
if c.Request.TLS != nil {
|
||||||
|
port := conf.Conf.Scheme.HttpsPort
|
||||||
|
c.Header("Alt-Svc", fmt.Sprintf("h3=\":%d\"; ma=86400", port))
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
quicSrv = &http3.Server{Addr: httpsBase, Handler: r}
|
||||||
|
go func() {
|
||||||
|
err := quicSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
utils.Log.Fatalf("failed to start http3 (quic): %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.UnixFile != "" {
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
fmt.Printf("start unix server @ %s\n", conf.Conf.Scheme.UnixFile)
|
fmt.Printf("start unix server @ %s\n", conf.Conf.Scheme.UnixFile)
|
||||||
@@ -203,6 +224,15 @@ the address is defined in config file`,
|
|||||||
utils.Log.Fatal("HTTPS server shutdown err: ", err)
|
utils.Log.Fatal("HTTPS server shutdown err: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
if conf.Conf.Scheme.EnableH3 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := quicSrv.Shutdown(ctx); err != nil {
|
||||||
|
utils.Log.Fatal("HTTP3 (quic) server shutdown err: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if conf.Conf.Scheme.UnixFile != "" {
|
if conf.Conf.Scheme.UnixFile != "" {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "115 Cloud",
|
Name: "115 Cloud",
|
||||||
DefaultRoot: "0",
|
DefaultRoot: "0",
|
||||||
// OnlyProxy: true,
|
LinkCacheMode: driver.LinkCacheUA,
|
||||||
// NoOverwriteUpload: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -131,23 +131,6 @@ func (d *Open115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Open115) GetObjInfo(ctx context.Context, path string) (model.Obj, error) {
|
|
||||||
if err := d.WaitLimit(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := d.client.GetFolderInfoByPath(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Obj{
|
|
||||||
Fid: resp.FileID,
|
|
||||||
Fn: resp.FileName,
|
|
||||||
Fc: resp.FileCategory,
|
|
||||||
Sha1: resp.Sha1,
|
|
||||||
Pc: resp.PickCode,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
func (d *Open115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
if err := d.WaitLimit(ctx); err != nil {
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "115 Open",
|
Name: "115 Open",
|
||||||
DefaultRoot: "0",
|
DefaultRoot: "0",
|
||||||
|
LinkCacheMode: driver.LinkCacheUA,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ func (d *Pan123) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) Init(ctx context.Context) error {
|
func (d *Pan123) Init(ctx context.Context) error {
|
||||||
_, err := d.Request(UserInfo, http.MethodGet, nil, nil)
|
_, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
|
||||||
|
req.SetHeader("platform", "web")
|
||||||
|
}, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,10 +262,7 @@ func (d *Pan123) GetDetails(ctx context.Context) (*model.StorageDetails, error)
|
|||||||
}
|
}
|
||||||
total := userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp
|
total := userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(userInfo.Data.SpaceUsed, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: total - userInfo.Data.SpaceUsed,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ type Addition struct {
|
|||||||
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
|
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
|
||||||
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
AccessToken string
|
AccessToken string
|
||||||
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
|
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
|
||||||
|
Platform string `json:"platform" type:"string" default:"web" help:"the platform header value, sent with API requests"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
@@ -27,6 +28,7 @@ func init() {
|
|||||||
return &Pan123{
|
return &Pan123{
|
||||||
Addition: Addition{
|
Addition: Addition{
|
||||||
UploadThread: 3,
|
UploadThread: 3,
|
||||||
|
Platform: "web",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ do:
|
|||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"authorization": "Bearer " + d.AccessToken,
|
"authorization": "Bearer " + d.AccessToken,
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",
|
||||||
"platform": "web",
|
"platform": d.Platform,
|
||||||
"app-version": "3",
|
"app-version": "3",
|
||||||
//"user-agent": base.UserAgent,
|
//"user-agent": base.UserAgent,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -229,6 +229,14 @@ func (d *Open123) GetDetails(ctx context.Context) (*model.StorageDetails, error)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open123) OfflineDownload(ctx context.Context, url string, dir model.Obj, callback string) (int, error) {
|
||||||
|
return d.createOfflineDownloadTask(ctx, url, dir.GetID(), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open123) OfflineDownloadProcess(ctx context.Context, taskID int) (float64, int, error) {
|
||||||
|
return d.queryOfflineDownloadStatus(ctx, taskID)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ driver.Driver = (*Open123)(nil)
|
_ driver.Driver = (*Open123)(nil)
|
||||||
_ driver.PutResult = (*Open123)(nil)
|
_ driver.PutResult = (*Open123)(nil)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func (a *ApiInfo) Require() {
|
|||||||
a.token <- struct{}{}
|
a.token <- struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ApiInfo) Release() {
|
func (a *ApiInfo) Release() {
|
||||||
if a.qps > 0 {
|
if a.qps > 0 {
|
||||||
time.AfterFunc(time.Second, func() {
|
time.AfterFunc(time.Second, func() {
|
||||||
@@ -26,13 +27,16 @@ func (a *ApiInfo) Release() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ApiInfo) SetQPS(qps int) {
|
func (a *ApiInfo) SetQPS(qps int) {
|
||||||
a.qps = qps
|
a.qps = qps
|
||||||
a.token = make(chan struct{}, qps)
|
a.token = make(chan struct{}, qps)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ApiInfo) NowLen() int {
|
func (a *ApiInfo) NowLen() int {
|
||||||
return len(a.token)
|
return len(a.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitApiInfo(url string, qps int) *ApiInfo {
|
func InitApiInfo(url string, qps int) *ApiInfo {
|
||||||
return &ApiInfo{
|
return &ApiInfo{
|
||||||
url: url,
|
url: url,
|
||||||
@@ -185,3 +189,18 @@ type UploadCompleteResp struct {
|
|||||||
FileID int64 `json:"fileID"`
|
FileID int64 `json:"fileID"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OfflineDownloadResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
TaskID int `json:"taskID"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OfflineDownloadProcessResp struct {
|
||||||
|
BaseResp
|
||||||
|
Data struct {
|
||||||
|
Process float64 `json:"process"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易
|
|||||||
Trash = InitApiInfo(Api+"/api/v1/file/trash", 2)
|
Trash = InitApiInfo(Api+"/api/v1/file/trash", 2)
|
||||||
UploadCreate = InitApiInfo(Api+"/upload/v2/file/create", 2)
|
UploadCreate = InitApiInfo(Api+"/upload/v2/file/create", 2)
|
||||||
UploadComplete = InitApiInfo(Api+"/upload/v2/file/upload_complete", 0)
|
UploadComplete = InitApiInfo(Api+"/upload/v2/file/upload_complete", 0)
|
||||||
|
|
||||||
|
OfflineDownload = InitApiInfo(Api+"/api/v1/offline/download", 1)
|
||||||
|
OfflineDownloadProcess = InitApiInfo(Api+"/api/v1/offline/download/process", 5)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
@@ -277,3 +280,34 @@ func (d *Open123) trash(fileId int64) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Open123) createOfflineDownloadTask(ctx context.Context, url string, dirID, callback string) (taskID int, err error) {
|
||||||
|
body := base.Json{
|
||||||
|
"url": url,
|
||||||
|
"dirID": dirID,
|
||||||
|
}
|
||||||
|
if len(callback) > 0 {
|
||||||
|
body["callBackUrl"] = callback
|
||||||
|
}
|
||||||
|
var resp OfflineDownloadResp
|
||||||
|
_, err = d.Request(OfflineDownload, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(body)
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return resp.Data.TaskID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Open123) queryOfflineDownloadStatus(ctx context.Context, taskID int) (process float64, status int, err error) {
|
||||||
|
var resp OfflineDownloadProcessResp
|
||||||
|
_, err = d.Request(OfflineDownloadProcess, http.MethodGet, func(req *resty.Request) {
|
||||||
|
req.SetQueryParams(map[string]string{
|
||||||
|
"taskID": strconv.Itoa(taskID),
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return .0, 0, err
|
||||||
|
}
|
||||||
|
return resp.Data.Process, resp.Data.Status, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,10 +200,7 @@ func (d *Cloud189) GetDetails(ctx context.Context) (*model.StorageDetails, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(capacityInfo.CloudCapacityInfo.UsedSize, capacityInfo.CloudCapacityInfo.TotalSize),
|
||||||
TotalSpace: capacityInfo.CloudCapacityInfo.TotalSize,
|
|
||||||
FreeSpace: capacityInfo.CloudCapacityInfo.FreeSize,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,13 +72,13 @@ type CapacityResp struct {
|
|||||||
ResMessage string `json:"res_message"`
|
ResMessage string `json:"res_message"`
|
||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
CloudCapacityInfo struct {
|
CloudCapacityInfo struct {
|
||||||
FreeSize uint64 `json:"freeSize"`
|
FreeSize int64 `json:"freeSize"`
|
||||||
MailUsedSize uint64 `json:"mail189UsedSize"`
|
MailUsedSize uint64 `json:"mail189UsedSize"`
|
||||||
TotalSize uint64 `json:"totalSize"`
|
TotalSize uint64 `json:"totalSize"`
|
||||||
UsedSize uint64 `json:"usedSize"`
|
UsedSize uint64 `json:"usedSize"`
|
||||||
} `json:"cloudCapacityInfo"`
|
} `json:"cloudCapacityInfo"`
|
||||||
FamilyCapacityInfo struct {
|
FamilyCapacityInfo struct {
|
||||||
FreeSize uint64 `json:"freeSize"`
|
FreeSize int64 `json:"freeSize"`
|
||||||
TotalSize uint64 `json:"totalSize"`
|
TotalSize uint64 `json:"totalSize"`
|
||||||
UsedSize uint64 `json:"usedSize"`
|
UsedSize uint64 `json:"usedSize"`
|
||||||
} `json:"familyCapacityInfo"`
|
} `json:"familyCapacityInfo"`
|
||||||
|
|||||||
@@ -284,18 +284,15 @@ func (y *Cloud189TV) GetDetails(ctx context.Context) (*model.StorageDetails, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var total, free uint64
|
var total, used uint64
|
||||||
if y.isFamily() {
|
if y.isFamily() {
|
||||||
total = capacityInfo.FamilyCapacityInfo.TotalSize
|
total = capacityInfo.FamilyCapacityInfo.TotalSize
|
||||||
free = capacityInfo.FamilyCapacityInfo.FreeSize
|
used = capacityInfo.FamilyCapacityInfo.UsedSize
|
||||||
} else {
|
} else {
|
||||||
total = capacityInfo.CloudCapacityInfo.TotalSize
|
total = capacityInfo.CloudCapacityInfo.TotalSize
|
||||||
free = capacityInfo.CloudCapacityInfo.FreeSize
|
used = capacityInfo.CloudCapacityInfo.UsedSize
|
||||||
}
|
}
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: free,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,13 +322,13 @@ type CapacityResp struct {
|
|||||||
ResMessage string `json:"res_message"`
|
ResMessage string `json:"res_message"`
|
||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
CloudCapacityInfo struct {
|
CloudCapacityInfo struct {
|
||||||
FreeSize uint64 `json:"freeSize"`
|
FreeSize int64 `json:"freeSize"`
|
||||||
MailUsedSize uint64 `json:"mail189UsedSize"`
|
MailUsedSize uint64 `json:"mail189UsedSize"`
|
||||||
TotalSize uint64 `json:"totalSize"`
|
TotalSize uint64 `json:"totalSize"`
|
||||||
UsedSize uint64 `json:"usedSize"`
|
UsedSize uint64 `json:"usedSize"`
|
||||||
} `json:"cloudCapacityInfo"`
|
} `json:"cloudCapacityInfo"`
|
||||||
FamilyCapacityInfo struct {
|
FamilyCapacityInfo struct {
|
||||||
FreeSize uint64 `json:"freeSize"`
|
FreeSize int64 `json:"freeSize"`
|
||||||
TotalSize uint64 `json:"totalSize"`
|
TotalSize uint64 `json:"totalSize"`
|
||||||
UsedSize uint64 `json:"usedSize"`
|
UsedSize uint64 `json:"usedSize"`
|
||||||
} `json:"familyCapacityInfo"`
|
} `json:"familyCapacityInfo"`
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, para
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) requestWithRetry(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, retryCount int, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189TV) requestWithRetry(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, retryCount int, isFamily ...bool) ([]byte, error) {
|
||||||
|
if y.tokenInfo == nil {
|
||||||
|
return nil, fmt.Errorf("login failed")
|
||||||
|
}
|
||||||
req := y.client.R().SetQueryParams(clientSuffix())
|
req := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
|
||||||
if params != nil {
|
if params != nil {
|
||||||
|
|||||||
@@ -416,18 +416,15 @@ func (y *Cloud189PC) GetDetails(ctx context.Context) (*model.StorageDetails, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var total, free uint64
|
var total, used uint64
|
||||||
if y.isFamily() {
|
if y.isFamily() {
|
||||||
total = capacityInfo.FamilyCapacityInfo.TotalSize
|
total = capacityInfo.FamilyCapacityInfo.TotalSize
|
||||||
free = capacityInfo.FamilyCapacityInfo.FreeSize
|
used = capacityInfo.FamilyCapacityInfo.UsedSize
|
||||||
} else {
|
} else {
|
||||||
total = capacityInfo.CloudCapacityInfo.TotalSize
|
total = capacityInfo.CloudCapacityInfo.TotalSize
|
||||||
free = capacityInfo.CloudCapacityInfo.FreeSize
|
used = capacityInfo.CloudCapacityInfo.UsedSize
|
||||||
}
|
}
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: free,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,13 +415,13 @@ type CapacityResp struct {
|
|||||||
ResMessage string `json:"res_message"`
|
ResMessage string `json:"res_message"`
|
||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
CloudCapacityInfo struct {
|
CloudCapacityInfo struct {
|
||||||
FreeSize uint64 `json:"freeSize"`
|
FreeSize int64 `json:"freeSize"`
|
||||||
MailUsedSize uint64 `json:"mail189UsedSize"`
|
MailUsedSize uint64 `json:"mail189UsedSize"`
|
||||||
TotalSize uint64 `json:"totalSize"`
|
TotalSize uint64 `json:"totalSize"`
|
||||||
UsedSize uint64 `json:"usedSize"`
|
UsedSize uint64 `json:"usedSize"`
|
||||||
} `json:"cloudCapacityInfo"`
|
} `json:"cloudCapacityInfo"`
|
||||||
FamilyCapacityInfo struct {
|
FamilyCapacityInfo struct {
|
||||||
FreeSize uint64 `json:"freeSize"`
|
FreeSize int64 `json:"freeSize"`
|
||||||
TotalSize uint64 `json:"totalSize"`
|
TotalSize uint64 `json:"totalSize"`
|
||||||
UsedSize uint64 `json:"usedSize"`
|
UsedSize uint64 `json:"usedSize"`
|
||||||
} `json:"familyCapacityInfo"`
|
} `json:"familyCapacityInfo"`
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||||
|
if y.getTokenInfo() == nil {
|
||||||
|
return nil, fmt.Errorf("login failed")
|
||||||
|
}
|
||||||
req := y.getClient().R().SetQueryParams(clientSuffix())
|
req := y.getClient().R().SetQueryParams(clientSuffix())
|
||||||
|
|
||||||
// 设置params
|
// 设置params
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
type Alias struct {
|
type Alias struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
rootOrder []string
|
||||||
pathMap map[string][]string
|
pathMap map[string][]string
|
||||||
autoFlatten bool
|
autoFlatten bool
|
||||||
oneKey string
|
oneKey string
|
||||||
@@ -40,13 +41,18 @@ func (d *Alias) Init(ctx context.Context) error {
|
|||||||
if d.Paths == "" {
|
if d.Paths == "" {
|
||||||
return errors.New("paths is required")
|
return errors.New("paths is required")
|
||||||
}
|
}
|
||||||
|
paths := strings.Split(d.Paths, "\n")
|
||||||
|
d.rootOrder = make([]string, 0, len(paths))
|
||||||
d.pathMap = make(map[string][]string)
|
d.pathMap = make(map[string][]string)
|
||||||
for _, path := range strings.Split(d.Paths, "\n") {
|
for _, path := range paths {
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
k, v := getPair(path)
|
k, v := getPair(path)
|
||||||
|
if _, ok := d.pathMap[k]; !ok {
|
||||||
|
d.rootOrder = append(d.rootOrder, k)
|
||||||
|
}
|
||||||
d.pathMap[k] = append(d.pathMap[k], v)
|
d.pathMap[k] = append(d.pathMap[k], v)
|
||||||
}
|
}
|
||||||
if len(d.pathMap) == 1 {
|
if len(d.pathMap) == 1 {
|
||||||
@@ -62,6 +68,7 @@ func (d *Alias) Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Alias) Drop(ctx context.Context) error {
|
func (d *Alias) Drop(ctx context.Context) error {
|
||||||
|
d.rootOrder = nil
|
||||||
d.pathMap = nil
|
d.pathMap = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -123,7 +130,7 @@ func (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {
|
|||||||
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
path := dir.GetPath()
|
path := dir.GetPath()
|
||||||
if utils.PathEqual(path, "/") && !d.autoFlatten {
|
if utils.PathEqual(path, "/") && !d.autoFlatten {
|
||||||
return d.listRoot(ctx, args.WithStorageDetails && d.DetailsPassThrough), nil
|
return d.listRoot(ctx, args.WithStorageDetails && d.DetailsPassThrough, args.Refresh), nil
|
||||||
}
|
}
|
||||||
root, sub := d.getRootAndPath(path)
|
root, sub := d.getRootAndPath(path)
|
||||||
dsts, ok := d.pathMap[root]
|
dsts, ok := d.pathMap[root]
|
||||||
@@ -139,22 +146,27 @@ func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
|
|||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tmp, err = utils.SliceConvert(tmp, func(obj model.Obj) (model.Obj, error) {
|
tmp, err = utils.SliceConvert(tmp, func(obj model.Obj) (model.Obj, error) {
|
||||||
thumb, ok := model.GetThumb(obj)
|
|
||||||
objRes := model.Object{
|
objRes := model.Object{
|
||||||
Name: obj.GetName(),
|
Name: obj.GetName(),
|
||||||
Size: obj.GetSize(),
|
Size: obj.GetSize(),
|
||||||
Modified: obj.ModTime(),
|
Modified: obj.ModTime(),
|
||||||
IsFolder: obj.IsDir(),
|
IsFolder: obj.IsDir(),
|
||||||
}
|
}
|
||||||
if !ok {
|
if thumb, ok := model.GetThumb(obj); ok {
|
||||||
return &objRes, nil
|
return &model.ObjThumb{
|
||||||
|
Object: objRes,
|
||||||
|
Thumbnail: model.Thumbnail{
|
||||||
|
Thumbnail: thumb,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
return &model.ObjThumb{
|
if details, ok := model.GetStorageDetails(obj); ok {
|
||||||
Object: objRes,
|
return &model.ObjStorageDetails{
|
||||||
Thumbnail: model.Thumbnail{
|
Obj: &objRes,
|
||||||
Thumbnail: thumb,
|
StorageDetailsWithName: *details,
|
||||||
},
|
}, nil
|
||||||
}, nil
|
}
|
||||||
|
return &objRes, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -199,9 +211,6 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
if resultLink.ContentLength == 0 {
|
if resultLink.ContentLength == 0 {
|
||||||
resultLink.ContentLength = fi.GetSize()
|
resultLink.ContentLength = fi.GetSize()
|
||||||
}
|
}
|
||||||
if resultLink.MFile != nil {
|
|
||||||
return &resultLink, nil
|
|
||||||
}
|
|
||||||
if d.DownloadConcurrency > 0 {
|
if d.DownloadConcurrency > 0 {
|
||||||
resultLink.Concurrency = d.DownloadConcurrency
|
resultLink.Concurrency = d.DownloadConcurrency
|
||||||
}
|
}
|
||||||
@@ -515,4 +524,25 @@ func (d *Alias) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Alias) ResolveLinkCacheMode(path string) driver.LinkCacheMode {
|
||||||
|
root, sub := d.getRootAndPath(path)
|
||||||
|
dsts, ok := d.pathMap[root]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, dst := range dsts {
|
||||||
|
storage, actualPath, err := op.GetStorageAndActualPath(stdpath.Join(dst, sub))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mode := storage.Config().LinkCacheMode
|
||||||
|
if mode == -1 {
|
||||||
|
return storage.(driver.LinkCacheModeResolver).ResolveLinkCacheMode(actualPath)
|
||||||
|
} else {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Alias)(nil)
|
var _ driver.Driver = (*Alias)(nil)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ var config = driver.Config{
|
|||||||
NoUpload: false,
|
NoUpload: false,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
ProxyRangeOption: true,
|
ProxyRangeOption: true,
|
||||||
|
LinkCacheMode: driver.LinkCacheAuto,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
@@ -16,10 +16,16 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Alias) listRoot(ctx context.Context, withDetails bool) []model.Obj {
|
type detailWithIndex struct {
|
||||||
|
idx int
|
||||||
|
val *model.StorageDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model.Obj {
|
||||||
var objs []model.Obj
|
var objs []model.Obj
|
||||||
var wg sync.WaitGroup
|
detailsChan := make(chan detailWithIndex, len(d.pathMap))
|
||||||
for k, v := range d.pathMap {
|
workerCount := 0
|
||||||
|
for _, k := range d.rootOrder {
|
||||||
obj := model.Object{
|
obj := model.Object{
|
||||||
Name: k,
|
Name: k,
|
||||||
IsFolder: true,
|
IsFolder: true,
|
||||||
@@ -27,6 +33,7 @@ func (d *Alias) listRoot(ctx context.Context, withDetails bool) []model.Obj {
|
|||||||
}
|
}
|
||||||
idx := len(objs)
|
idx := len(objs)
|
||||||
objs = append(objs, &obj)
|
objs = append(objs, &obj)
|
||||||
|
v := d.pathMap[k]
|
||||||
if !withDetails || len(v) != 1 {
|
if !withDetails || len(v) != 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -45,20 +52,26 @@ func (d *Alias) listRoot(ctx context.Context, withDetails bool) []model.Obj {
|
|||||||
DriverName: remoteDriver.Config().Name,
|
DriverName: remoteDriver.Config().Name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
wg.Add(1)
|
workerCount++
|
||||||
go func() {
|
go func(dri driver.Driver, i int) {
|
||||||
defer wg.Done()
|
details, e := op.GetStorageDetails(ctx, dri, refresh)
|
||||||
details, e := op.GetStorageDetails(ctx, remoteDriver)
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
if !errors.Is(e, errs.NotImplement) {
|
if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {
|
||||||
log.Errorf("failed get %s storage details: %+v", remoteDriver.GetStorage().MountPath, e)
|
log.Errorf("failed get %s storage details: %+v", dri.GetStorage().MountPath, e)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
objs[idx].(*model.ObjStorageDetails).StorageDetails = details
|
detailsChan <- detailWithIndex{idx: i, val: details}
|
||||||
}()
|
}(remoteDriver, idx)
|
||||||
|
}
|
||||||
|
for workerCount > 0 {
|
||||||
|
select {
|
||||||
|
case r := <-detailsChan:
|
||||||
|
objs[r.idx].(*model.ObjStorageDetails).StorageDetails = r.val
|
||||||
|
workerCount--
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
workerCount = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
return objs
|
return objs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -337,10 +337,7 @@ func (d *AliDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error
|
|||||||
used := utils.Json.Get(res, "drive_used_size").ToUint64()
|
used := utils.Json.Get(res, "drive_used_size").ToUint64()
|
||||||
total := utils.Json.Get(res, "drive_total_size").ToUint64()
|
total := utils.Json.Get(res, "drive_total_size").ToUint64()
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: total - used,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -299,10 +299,7 @@ func (d *AliyundriveOpen) GetDetails(ctx context.Context) (*model.StorageDetails
|
|||||||
total := utils.Json.Get(res, "personal_space_info", "total_size").ToUint64()
|
total := utils.Json.Get(res, "personal_space_info", "total_size").ToUint64()
|
||||||
used := utils.Json.Get(res, "personal_space_info", "used_size").ToUint64()
|
used := utils.Json.Get(res, "personal_space_info", "used_size").ToUint64()
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: total - used,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import (
|
|||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/google_drive"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/google_drive"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/google_photo"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/google_photo"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/halalcloud"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/halalcloud"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/halalcloud_open"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/ilanzou"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/ilanzou"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/ipfs_api"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/ipfs_api"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/kodbox"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/kodbox"
|
||||||
@@ -55,6 +56,7 @@ import (
|
|||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/openlist_share"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/openlist_share"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share"
|
||||||
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/proton_drive"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_open"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_open"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc"
|
||||||
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc_tv"
|
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc_tv"
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
@@ -18,8 +22,10 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
|
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/avast/retry-go"
|
"github.com/avast/retry-go"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,8 +35,20 @@ type BaiduNetdisk struct {
|
|||||||
|
|
||||||
uploadThread int
|
uploadThread int
|
||||||
vipType int // 会员类型,0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)
|
vipType int // 会员类型,0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)
|
||||||
|
|
||||||
|
upClient *resty.Client // 上传文件使用的http客户端
|
||||||
|
uploadUrlG singleflight.Group[string]
|
||||||
|
uploadUrlMu sync.RWMutex
|
||||||
|
uploadUrlCache map[string]uploadURLCacheEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type uploadURLCacheEntry struct {
|
||||||
|
url string
|
||||||
|
updateTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUploadIDExpired = errors.New("uploadid expired")
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Config() driver.Config {
|
func (d *BaiduNetdisk) Config() driver.Config {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@@ -40,19 +58,32 @@ func (d *BaiduNetdisk) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) Init(ctx context.Context) error {
|
func (d *BaiduNetdisk) Init(ctx context.Context) error {
|
||||||
|
timeout := DEFAULT_UPLOAD_SLICE_TIMEOUT
|
||||||
|
if d.UploadSliceTimeout > 0 {
|
||||||
|
timeout = time.Second * time.Duration(d.UploadSliceTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.upClient = base.NewRestyClient().
|
||||||
|
SetTimeout(timeout).
|
||||||
|
SetRetryCount(UPLOAD_RETRY_COUNT).
|
||||||
|
SetRetryWaitTime(UPLOAD_RETRY_WAIT_TIME).
|
||||||
|
SetRetryMaxWaitTime(UPLOAD_RETRY_MAX_WAIT_TIME)
|
||||||
|
d.uploadUrlCache = make(map[string]uploadURLCacheEntry)
|
||||||
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
|
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
|
||||||
if d.uploadThread < 1 || d.uploadThread > 32 {
|
if d.uploadThread < 1 {
|
||||||
d.uploadThread, d.UploadThread = 3, "3"
|
d.uploadThread, d.UploadThread = 1, "1"
|
||||||
|
} else if d.uploadThread > 32 {
|
||||||
|
d.uploadThread, d.UploadThread = 32, "32"
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := url.Parse(d.UploadAPI); d.UploadAPI == "" || err != nil {
|
if _, err := url.Parse(d.UploadAPI); d.UploadAPI == "" || err != nil {
|
||||||
d.UploadAPI = "https://d.pcs.baidu.com"
|
d.UploadAPI = UPLOAD_FALLBACK_API
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := d.get("/xpan/nas", map[string]string{
|
res, err := d.get("/xpan/nas", map[string]string{
|
||||||
"method": "uinfo",
|
"method": "uinfo",
|
||||||
}, nil)
|
}, nil)
|
||||||
log.Debugf("[baidu] get uinfo: %s", string(res))
|
log.Debugf("[baidu_netdisk] get uinfo: %s", string(res))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -179,6 +210,11 @@ func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream mo
|
|||||||
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
|
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
|
||||||
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
|
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
|
||||||
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
// 百度网盘不允许上传空文件
|
||||||
|
if stream.GetSize() < 1 {
|
||||||
|
return nil, ErrBaiduEmptyFilesNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
// rapid upload
|
// rapid upload
|
||||||
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
|
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
|
||||||
return newObj, nil
|
return newObj, nil
|
||||||
@@ -214,7 +250,6 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
|
|
||||||
// cal md5 for first 256k data
|
// cal md5 for first 256k data
|
||||||
const SliceSize int64 = 256 * utils.KB
|
const SliceSize int64 = 256 * utils.KB
|
||||||
// cal md5
|
|
||||||
blockList := make([]string, 0, count)
|
blockList := make([]string, 0, count)
|
||||||
byteSize := sliceSize
|
byteSize := sliceSize
|
||||||
fileMd5H := md5.New()
|
fileMd5H := md5.New()
|
||||||
@@ -244,7 +279,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
}
|
}
|
||||||
if tmpF != nil {
|
if tmpF != nil {
|
||||||
if written != streamSize {
|
if written != streamSize {
|
||||||
return nil, errs.NewErr(err, "CreateTempFile failed, incoming stream actual size= %d, expect = %d ", written, streamSize)
|
return nil, errs.NewErr(err, "CreateTempFile failed, size mismatch: %d != %d ", written, streamSize)
|
||||||
}
|
}
|
||||||
_, err = tmpF.Seek(0, io.SeekStart)
|
_, err = tmpF.Seek(0, io.SeekStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -258,31 +293,14 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
mtime := stream.ModTime().Unix()
|
mtime := stream.ModTime().Unix()
|
||||||
ctime := stream.CreateTime().Unix()
|
ctime := stream.CreateTime().Unix()
|
||||||
|
|
||||||
// step.1 预上传
|
// step.1 尝试读取已保存进度
|
||||||
// 尝试获取之前的进度
|
|
||||||
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
|
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
|
||||||
if !ok {
|
if !ok {
|
||||||
params := map[string]string{
|
// 没有进度,走预上传
|
||||||
"method": "precreate",
|
precreateResp, err = d.precreate(ctx, path, streamSize, blockListStr, contentMd5, sliceMd5, ctime, mtime)
|
||||||
}
|
|
||||||
form := map[string]string{
|
|
||||||
"path": path,
|
|
||||||
"size": strconv.FormatInt(streamSize, 10),
|
|
||||||
"isdir": "0",
|
|
||||||
"autoinit": "1",
|
|
||||||
"rtype": "3",
|
|
||||||
"block_list": blockListStr,
|
|
||||||
"content-md5": contentMd5,
|
|
||||||
"slice-md5": sliceMd5,
|
|
||||||
}
|
|
||||||
joinTime(form, ctime, mtime)
|
|
||||||
|
|
||||||
log.Debugf("[baidu_netdisk] precreate data: %s", form)
|
|
||||||
_, err = d.postForm("/xpan/file", params, form, &precreateResp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Debugf("%+v", precreateResp)
|
|
||||||
if precreateResp.ReturnType == 2 {
|
if precreateResp.ReturnType == 2 {
|
||||||
// rapid upload, since got md5 match from baidu server
|
// rapid upload, since got md5 match from baidu server
|
||||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||||
@@ -291,45 +309,96 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
return fileToObj(precreateResp.File), nil
|
return fileToObj(precreateResp.File), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// step.2 上传分片
|
ensureUploadURL := func() {
|
||||||
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
if precreateResp.UploadURL != "" {
|
||||||
retry.Attempts(1),
|
return
|
||||||
retry.Delay(time.Second),
|
|
||||||
retry.DelayType(retry.BackOffDelay))
|
|
||||||
|
|
||||||
for i, partseq := range precreateResp.BlockList {
|
|
||||||
if utils.IsCanceled(upCtx) {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
precreateResp.UploadURL = d.getUploadUrl(path, precreateResp.Uploadid)
|
||||||
i, partseq, offset, byteSize := i, partseq, int64(partseq)*sliceSize, sliceSize
|
|
||||||
if partseq+1 == count {
|
|
||||||
byteSize = lastBlockSize
|
|
||||||
}
|
|
||||||
threadG.Go(func(ctx context.Context) error {
|
|
||||||
params := map[string]string{
|
|
||||||
"method": "upload",
|
|
||||||
"access_token": d.AccessToken,
|
|
||||||
"type": "tmpfile",
|
|
||||||
"path": path,
|
|
||||||
"uploadid": precreateResp.Uploadid,
|
|
||||||
"partseq": strconv.Itoa(partseq),
|
|
||||||
}
|
|
||||||
err := d.uploadSlice(ctx, params, stream.GetName(),
|
|
||||||
driver.NewLimitedUploadStream(ctx, io.NewSectionReader(cache, offset, byteSize)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
|
|
||||||
precreateResp.BlockList[i] = -1
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if err = threadG.Wait(); err != nil {
|
ensureUploadURL()
|
||||||
// 如果属于用户主动取消,则保存上传进度
|
|
||||||
|
// step.2 上传分片
|
||||||
|
uploadLoop:
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
// 获取上传域名
|
||||||
|
if precreateResp.UploadURL == "" {
|
||||||
|
ensureUploadURL()
|
||||||
|
}
|
||||||
|
uploadUrl := precreateResp.UploadURL
|
||||||
|
// 并发上传
|
||||||
|
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
||||||
|
retry.Attempts(1),
|
||||||
|
retry.Delay(time.Second),
|
||||||
|
retry.DelayType(retry.BackOffDelay))
|
||||||
|
|
||||||
|
cacheReaderAt, okReaderAt := cache.(io.ReaderAt)
|
||||||
|
if !okReaderAt {
|
||||||
|
return nil, fmt.Errorf("cache object must implement io.ReaderAt interface for upload operations")
|
||||||
|
}
|
||||||
|
|
||||||
|
totalParts := len(precreateResp.BlockList)
|
||||||
|
|
||||||
|
for i, partseq := range precreateResp.BlockList {
|
||||||
|
if utils.IsCanceled(upCtx) || partseq < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i, partseq := i, partseq
|
||||||
|
offset, size := int64(partseq)*sliceSize, sliceSize
|
||||||
|
if partseq+1 == count {
|
||||||
|
size = lastBlockSize
|
||||||
|
}
|
||||||
|
threadG.Go(func(ctx context.Context) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"method": "upload",
|
||||||
|
"access_token": d.AccessToken,
|
||||||
|
"type": "tmpfile",
|
||||||
|
"path": path,
|
||||||
|
"uploadid": precreateResp.Uploadid,
|
||||||
|
"partseq": strconv.Itoa(partseq),
|
||||||
|
}
|
||||||
|
section := io.NewSectionReader(cacheReaderAt, offset, size)
|
||||||
|
err := d.uploadSlice(ctx, uploadUrl, params, stream.GetName(), driver.NewLimitedUploadStream(ctx, section))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
precreateResp.BlockList[i] = -1
|
||||||
|
// 当前goroutine还没退出,+1才是真正成功的数量
|
||||||
|
success := threadG.Success() + 1
|
||||||
|
progress := float64(success) * 100 / float64(totalParts)
|
||||||
|
up(progress)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = threadG.Wait()
|
||||||
|
if err == nil {
|
||||||
|
break uploadLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存进度(所有错误都会保存)
|
||||||
|
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
|
||||||
|
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
|
||||||
|
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
|
return nil, err
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrUploadIDExpired) {
|
||||||
|
log.Warn("[baidu_netdisk] uploadid expired, will restart from scratch")
|
||||||
|
d.clearUploadUrlCache(precreateResp.Uploadid)
|
||||||
|
// 重新 precreate(所有分片都要重传)
|
||||||
|
newPre, err2 := d.precreate(ctx, path, streamSize, blockListStr, "", "", ctime, mtime)
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err2
|
||||||
|
}
|
||||||
|
if newPre.ReturnType == 2 {
|
||||||
|
return fileToObj(newPre.File), nil
|
||||||
|
}
|
||||||
|
precreateResp = newPre
|
||||||
|
precreateResp.UploadURL = ""
|
||||||
|
ensureUploadURL()
|
||||||
|
// 覆盖掉旧的进度
|
||||||
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
|
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
|
||||||
|
continue uploadLoop
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -343,23 +412,72 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||||
newFile.Ctime = ctime
|
newFile.Ctime = ctime
|
||||||
newFile.Mtime = mtime
|
newFile.Mtime = mtime
|
||||||
|
// 上传成功清理进度
|
||||||
|
base.SaveUploadProgress(d, nil, d.AccessToken, contentMd5)
|
||||||
|
d.clearUploadUrlCache(precreateResp.Uploadid)
|
||||||
return fileToObj(newFile), nil
|
return fileToObj(newFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string, fileName string, file io.Reader) error {
|
// precreate 执行预上传操作,支持首次上传和 uploadid 过期重试
|
||||||
res, err := base.RestyClient.R().
|
func (d *BaiduNetdisk) precreate(ctx context.Context, path string, streamSize int64, blockListStr, contentMd5, sliceMd5 string, ctime, mtime int64) (*PrecreateResp, error) {
|
||||||
|
params := map[string]string{"method": "precreate"}
|
||||||
|
form := map[string]string{
|
||||||
|
"path": path,
|
||||||
|
"size": strconv.FormatInt(streamSize, 10),
|
||||||
|
"isdir": "0",
|
||||||
|
"autoinit": "1",
|
||||||
|
"rtype": "3",
|
||||||
|
"block_list": blockListStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在首次上传时才包含 content-md5 和 slice-md5
|
||||||
|
if contentMd5 != "" && sliceMd5 != "" {
|
||||||
|
form["content-md5"] = contentMd5
|
||||||
|
form["slice-md5"] = sliceMd5
|
||||||
|
}
|
||||||
|
|
||||||
|
joinTime(form, ctime, mtime)
|
||||||
|
|
||||||
|
var precreateResp PrecreateResp
|
||||||
|
_, err := d.postForm("/xpan/file", params, form, &precreateResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
||||||
|
if precreateResp.ReturnType == 2 {
|
||||||
|
precreateResp.File.Ctime = ctime
|
||||||
|
precreateResp.File.Mtime = mtime
|
||||||
|
}
|
||||||
|
|
||||||
|
return &precreateResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *BaiduNetdisk) uploadSlice(ctx context.Context, uploadUrl string, params map[string]string, fileName string, file io.Reader) error {
|
||||||
|
res, err := d.upClient.R().
|
||||||
SetContext(ctx).
|
SetContext(ctx).
|
||||||
SetQueryParams(params).
|
SetQueryParams(params).
|
||||||
SetFileReader("file", fileName, file).
|
SetFileReader("file", fileName, file).
|
||||||
Post(d.UploadAPI + "/rest/2.0/pcs/superfile2")
|
Post(uploadUrl + "/rest/2.0/pcs/superfile2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debugln(res.RawResponse.Status + res.String())
|
log.Debugln(res.RawResponse.Status + res.String())
|
||||||
|
if res.StatusCode() != http.StatusOK {
|
||||||
|
return errs.NewErr(errs.StreamIncomplete, "baidu upload failed, status=%d, body=%s", res.StatusCode(), res.String())
|
||||||
|
}
|
||||||
errCode := utils.Json.Get(res.Body(), "error_code").ToInt()
|
errCode := utils.Json.Get(res.Body(), "error_code").ToInt()
|
||||||
errNo := utils.Json.Get(res.Body(), "errno").ToInt()
|
errNo := utils.Json.Get(res.Body(), "errno").ToInt()
|
||||||
|
respStr := res.String()
|
||||||
|
lower := strings.ToLower(respStr)
|
||||||
|
// 合并 uploadid 过期检测逻辑
|
||||||
|
if strings.Contains(lower, "uploadid") &&
|
||||||
|
(strings.Contains(lower, "invalid") || strings.Contains(lower, "expired") || strings.Contains(lower, "not found")) {
|
||||||
|
return ErrUploadIDExpired
|
||||||
|
}
|
||||||
|
|
||||||
if errCode != 0 || errNo != 0 {
|
if errCode != 0 || errNo != 0 {
|
||||||
return errs.NewErr(errs.StreamIncomplete, "error in uploading to baidu, will retry. response=%s", res.String())
|
return errs.NewErr(errs.StreamIncomplete, "error uploading to baidu, response=%s", res.String())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -369,7 +487,7 @@ func (d *BaiduNetdisk) GetDetails(ctx context.Context) (*model.StorageDetails, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &model.StorageDetails{DiskUsage: *du}, nil
|
return &model.StorageDetails{DiskUsage: du}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*BaiduNetdisk)(nil)
|
var _ driver.Driver = (*BaiduNetdisk)(nil)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package baidu_netdisk
|
|||||||
import (
|
import (
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
@@ -18,12 +19,23 @@ type Addition struct {
|
|||||||
AccessToken string
|
AccessToken string
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
|
UploadSliceTimeout int `json:"upload_timeout" type:"number" default:"60" help:"per-slice upload timeout in seconds"`
|
||||||
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
||||||
|
UseDynamicUploadAPI bool `json:"use_dynamic_upload_api" default:"true" help:"dynamically get upload api domain, when enabled, the 'Upload API' setting will be used as a fallback if failed to get"`
|
||||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
||||||
LowBandwithUploadMode bool `json:"low_bandwith_upload_mode" default:"false"`
|
LowBandwithUploadMode bool `json:"low_bandwith_upload_mode" default:"false"`
|
||||||
OnlyListVideoFile bool `json:"only_list_video_file" default:"false"`
|
OnlyListVideoFile bool `json:"only_list_video_file" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
UPLOAD_FALLBACK_API = "https://d.pcs.baidu.com" // 备用上传地址
|
||||||
|
UPLOAD_URL_EXPIRE_TIME = time.Minute * 60 // 上传地址有效期(分钟)
|
||||||
|
DEFAULT_UPLOAD_SLICE_TIMEOUT = time.Second * 60 // 上传分片请求默认超时时间
|
||||||
|
UPLOAD_RETRY_COUNT = 3
|
||||||
|
UPLOAD_RETRY_WAIT_TIME = time.Second * 1
|
||||||
|
UPLOAD_RETRY_MAX_WAIT_TIME = time.Second * 5
|
||||||
|
)
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "BaiduNetdisk",
|
Name: "BaiduNetdisk",
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package baidu_netdisk
|
package baidu_netdisk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -9,6 +10,10 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBaiduEmptyFilesNotAllowed = errors.New("empty files are not allowed by baidu netdisk")
|
||||||
|
)
|
||||||
|
|
||||||
type TokenErrResp struct {
|
type TokenErrResp struct {
|
||||||
ErrorDescription string `json:"error_description"`
|
ErrorDescription string `json:"error_description"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
@@ -188,6 +193,32 @@ type PrecreateResp struct {
|
|||||||
|
|
||||||
// return_type=2
|
// return_type=2
|
||||||
File File `json:"info"`
|
File File `json:"info"`
|
||||||
|
|
||||||
|
UploadURL string `json:"-"` // 保存断点续传对应的上传域名
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadServerResp struct {
|
||||||
|
BakServer []any `json:"bak_server"`
|
||||||
|
BakServers []struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
} `json:"bak_servers"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
ErrorCode int `json:"error_code"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
Expire int `json:"expire"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Newno string `json:"newno"`
|
||||||
|
QuicServer []any `json:"quic_server"`
|
||||||
|
QuicServers []struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
} `json:"quic_servers"`
|
||||||
|
RequestID int64 `json:"request_id"`
|
||||||
|
Server []any `json:"server"`
|
||||||
|
ServerTime int `json:"server_time"`
|
||||||
|
Servers []struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
} `json:"servers"`
|
||||||
|
Sl int `json:"sl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuotaResp struct {
|
type QuotaResp struct {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
@@ -114,7 +115,7 @@ func (d *BaiduNetdisk) request(furl string, method string, callback base.ReqCall
|
|||||||
errno := utils.Json.Get(res.Body(), "errno").ToInt()
|
errno := utils.Json.Get(res.Body(), "errno").ToInt()
|
||||||
if errno != 0 {
|
if errno != 0 {
|
||||||
if utils.SliceContains([]int{111, -6}, errno) {
|
if utils.SliceContains([]int{111, -6}, errno) {
|
||||||
log.Info("refreshing baidu_netdisk token.")
|
log.Info("[baidu_netdisk] refreshing baidu_netdisk token.")
|
||||||
err2 := d.refreshToken()
|
err2 := d.refreshToken()
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return retry.Unrecoverable(err2)
|
return retry.Unrecoverable(err2)
|
||||||
@@ -325,10 +326,10 @@ func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
|
|||||||
// 非会员固定为 4MB
|
// 非会员固定为 4MB
|
||||||
if d.vipType == 0 {
|
if d.vipType == 0 {
|
||||||
if d.CustomUploadPartSize != 0 {
|
if d.CustomUploadPartSize != 0 {
|
||||||
log.Warnf("CustomUploadPartSize is not supported for non-vip user, use DefaultSliceSize")
|
log.Warnf("[baidu_netdisk] CustomUploadPartSize is not supported for non-vip user, use DefaultSliceSize")
|
||||||
}
|
}
|
||||||
if filesize > MaxSliceNum*DefaultSliceSize {
|
if filesize > MaxSliceNum*DefaultSliceSize {
|
||||||
log.Warnf("File size(%d) is too large, may cause upload failure", filesize)
|
log.Warnf("[baidu_netdisk] File size(%d) is too large, may cause upload failure", filesize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return DefaultSliceSize
|
return DefaultSliceSize
|
||||||
@@ -336,17 +337,17 @@ func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
|
|||||||
|
|
||||||
if d.CustomUploadPartSize != 0 {
|
if d.CustomUploadPartSize != 0 {
|
||||||
if d.CustomUploadPartSize < DefaultSliceSize {
|
if d.CustomUploadPartSize < DefaultSliceSize {
|
||||||
log.Warnf("CustomUploadPartSize(%d) is less than DefaultSliceSize(%d), use DefaultSliceSize", d.CustomUploadPartSize, DefaultSliceSize)
|
log.Warnf("[baidu_netdisk] CustomUploadPartSize(%d) is less than DefaultSliceSize(%d), use DefaultSliceSize", d.CustomUploadPartSize, DefaultSliceSize)
|
||||||
return DefaultSliceSize
|
return DefaultSliceSize
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.vipType == 1 && d.CustomUploadPartSize > VipSliceSize {
|
if d.vipType == 1 && d.CustomUploadPartSize > VipSliceSize {
|
||||||
log.Warnf("CustomUploadPartSize(%d) is greater than VipSliceSize(%d), use VipSliceSize", d.CustomUploadPartSize, VipSliceSize)
|
log.Warnf("[baidu_netdisk] CustomUploadPartSize(%d) is greater than VipSliceSize(%d), use VipSliceSize", d.CustomUploadPartSize, VipSliceSize)
|
||||||
return VipSliceSize
|
return VipSliceSize
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.vipType == 2 && d.CustomUploadPartSize > SVipSliceSize {
|
if d.vipType == 2 && d.CustomUploadPartSize > SVipSliceSize {
|
||||||
log.Warnf("CustomUploadPartSize(%d) is greater than SVipSliceSize(%d), use SVipSliceSize", d.CustomUploadPartSize, SVipSliceSize)
|
log.Warnf("[baidu_netdisk] CustomUploadPartSize(%d) is greater than SVipSliceSize(%d), use SVipSliceSize", d.CustomUploadPartSize, SVipSliceSize)
|
||||||
return SVipSliceSize
|
return SVipSliceSize
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,24 +377,112 @@ func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if filesize > MaxSliceNum*maxSliceSize {
|
if filesize > MaxSliceNum*maxSliceSize {
|
||||||
log.Warnf("File size(%d) is too large, may cause upload failure", filesize)
|
log.Warnf("[baidu_netdisk] File size(%d) is too large, may cause upload failure", filesize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxSliceSize
|
return maxSliceSize
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *BaiduNetdisk) quota(ctx context.Context) (*model.DiskUsage, error) {
|
func (d *BaiduNetdisk) quota(ctx context.Context) (model.DiskUsage, error) {
|
||||||
var resp QuotaResp
|
var resp QuotaResp
|
||||||
_, err := d.request("https://pan.baidu.com/api/quota", http.MethodGet, func(req *resty.Request) {
|
_, err := d.request("https://pan.baidu.com/api/quota", http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return model.DiskUsage{}, err
|
||||||
}
|
}
|
||||||
return &model.DiskUsage{
|
return driver.DiskUsageFromUsedAndTotal(resp.Used, resp.Total), nil
|
||||||
TotalSpace: resp.Total,
|
}
|
||||||
FreeSpace: resp.Total - resp.Used,
|
|
||||||
}, nil
|
// getUploadUrl 从开放平台获取上传域名/地址,并发请求会被合并,结果会在 uploadid 生命周期内复用。
|
||||||
|
// 如果获取失败,则返回 Upload API设置项。
|
||||||
|
func (d *BaiduNetdisk) getUploadUrl(path, uploadId string) string {
|
||||||
|
if !d.UseDynamicUploadAPI || uploadId == "" {
|
||||||
|
return d.UploadAPI
|
||||||
|
}
|
||||||
|
getCachedUrlFunc := func() (string, bool) {
|
||||||
|
d.uploadUrlMu.RLock()
|
||||||
|
defer d.uploadUrlMu.RUnlock()
|
||||||
|
if entry, ok := d.uploadUrlCache[uploadId]; ok {
|
||||||
|
return entry.url, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
// 检查地址缓存
|
||||||
|
if uploadUrl, ok := getCachedUrlFunc(); ok {
|
||||||
|
return uploadUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadUrlGetFunc := func() (string, error) {
|
||||||
|
// 双重检查缓存
|
||||||
|
if uploadUrl, ok := getCachedUrlFunc(); ok {
|
||||||
|
return uploadUrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadUrl, err := d.requestForUploadUrl(path, uploadId)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.uploadUrlMu.Lock()
|
||||||
|
d.uploadUrlCache[uploadId] = uploadURLCacheEntry{
|
||||||
|
url: uploadUrl,
|
||||||
|
updateTime: time.Now(),
|
||||||
|
}
|
||||||
|
d.uploadUrlMu.Unlock()
|
||||||
|
return uploadUrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadUrl, err, _ := d.uploadUrlG.Do(uploadId, uploadUrlGetFunc)
|
||||||
|
if err != nil {
|
||||||
|
fallback := d.UploadAPI
|
||||||
|
log.Warnf("[baidu_netdisk] get upload URL failed (%v), will use fallback URL: %s", err, fallback)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return uploadUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *BaiduNetdisk) clearUploadUrlCache(uploadId string) {
|
||||||
|
if uploadId == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.uploadUrlMu.Lock()
|
||||||
|
if _, ok := d.uploadUrlCache[uploadId]; ok {
|
||||||
|
delete(d.uploadUrlCache, uploadId)
|
||||||
|
}
|
||||||
|
d.uploadUrlMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestForUploadUrl 请求获取上传地址。
|
||||||
|
// 实测此接口不需要认证,传method和upload_version就行,不过还是按文档规范调用。
|
||||||
|
// https://pan.baidu.com/union/doc/Mlvw5hfnr
|
||||||
|
func (d *BaiduNetdisk) requestForUploadUrl(path, uploadId string) (string, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"method": "locateupload",
|
||||||
|
"appid": "250528",
|
||||||
|
"path": path,
|
||||||
|
"uploadid": uploadId,
|
||||||
|
"upload_version": "2.0",
|
||||||
|
}
|
||||||
|
apiUrl := "https://d.pcs.baidu.com/rest/2.0/pcs/file"
|
||||||
|
var resp UploadServerResp
|
||||||
|
_, err := d.request(apiUrl, http.MethodGet, func(req *resty.Request) {
|
||||||
|
req.SetQueryParams(params)
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 应该是https开头的一个地址
|
||||||
|
var uploadUrl string
|
||||||
|
if len(resp.Servers) > 0 {
|
||||||
|
uploadUrl = resp.Servers[0].Server
|
||||||
|
} else if len(resp.BakServers) > 0 {
|
||||||
|
uploadUrl = resp.BakServers[0].Server
|
||||||
|
}
|
||||||
|
if uploadUrl == "" {
|
||||||
|
return "", errors.New("upload URL is empty")
|
||||||
|
}
|
||||||
|
return uploadUrl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func encodeURIComponent(str string) string {
|
// func encodeURIComponent(str string) string {
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "BaiduPhoto",
|
Name: "BaiduPhoto",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
|
LinkCacheMode: driver.LinkCacheUA,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func InitClient() {
|
|||||||
}),
|
}),
|
||||||
).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
||||||
NoRedirectClient.SetHeader("user-agent", UserAgent)
|
NoRedirectClient.SetHeader("user-agent", UserAgent)
|
||||||
|
net.SetRestyProxyIfConfigured(NoRedirectClient)
|
||||||
|
|
||||||
RestyClient = NewRestyClient()
|
RestyClient = NewRestyClient()
|
||||||
HttpClient = net.NewHttpClient()
|
HttpClient = net.NewHttpClient()
|
||||||
@@ -37,5 +38,7 @@ func NewRestyClient() *resty.Client {
|
|||||||
SetRetryResetReaders(true).
|
SetRetryResetReaders(true).
|
||||||
SetTimeout(DefaultTimeout).
|
SetTimeout(DefaultTimeout).
|
||||||
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
||||||
|
|
||||||
|
net.SetRestyProxyIfConfigured(client)
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, file model.FileStr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = writer.WriteField("puid", fmt.Sprintf("%d", resp.Msg.Puid))
|
err = writer.WriteField("puid", strconv.Itoa(resp.Msg.Puid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error writing param2 to request body:", err)
|
fmt.Println("Error writing param2 to request body:", err)
|
||||||
return err
|
return err
|
||||||
@@ -260,7 +261,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, file model.FileStr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
|
req.Header.Set("Content-Length", strconv.Itoa(body.Len()))
|
||||||
resps, err := http.DefaultClient.Do(req)
|
resps, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ type UploadDoneParam struct {
|
|||||||
func fileToObj(f File) *model.Object {
|
func fileToObj(f File) *model.Object {
|
||||||
if len(f.Content.FolderName) > 0 {
|
if len(f.Content.FolderName) > 0 {
|
||||||
return &model.Object{
|
return &model.Object{
|
||||||
ID: fmt.Sprintf("%d", f.ID),
|
ID: strconv.Itoa(f.ID),
|
||||||
Name: f.Content.FolderName,
|
Name: f.Content.FolderName,
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Modified: time.UnixMilli(f.Inserttime),
|
Modified: time.UnixMilli(f.Inserttime),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||||
@@ -172,7 +173,7 @@ func (d *ChaoXing) Login() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
|
req.Header.Set("Content-Length", strconv.Itoa(body.Len()))
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -349,10 +349,7 @@ func (d *CloudreveV4) GetDetails(ctx context.Context) (*model.StorageDetails, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(r.Used, r.Total),
|
||||||
TotalSpace: r.Total,
|
|
||||||
FreeSpace: r.Total - r.Used,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -317,7 +317,8 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
}
|
}
|
||||||
return readSeeker, nil
|
return readSeeker, nil
|
||||||
}),
|
}),
|
||||||
SyncClosers: utils.NewSyncClosers(remoteLink),
|
SyncClosers: utils.NewSyncClosers(remoteLink),
|
||||||
|
RequireReference: remoteLink.RequireReference,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Doubao struct {
|
type Doubao struct {
|
||||||
@@ -23,6 +24,7 @@ type Doubao struct {
|
|||||||
*UploadToken
|
*UploadToken
|
||||||
UserId string
|
UserId string
|
||||||
uploadThread int
|
uploadThread int
|
||||||
|
limiter *rate.Limiter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) Config() driver.Config {
|
func (d *Doubao) Config() driver.Config {
|
||||||
@@ -61,6 +63,17 @@ func (d *Doubao) Init(ctx context.Context) error {
|
|||||||
d.UploadToken = uploadToken
|
d.UploadToken = uploadToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.LimitRate > 0 {
|
||||||
|
d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Doubao) WaitLimit(ctx context.Context) error {
|
||||||
|
if d.limiter != nil {
|
||||||
|
return d.limiter.Wait(ctx)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +82,10 @@ func (d *Doubao) Drop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *Doubao) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var files []model.Obj
|
var files []model.Obj
|
||||||
fileList, err := d.getFiles(dir.GetID(), "")
|
fileList, err := d.getFiles(dir.GetID(), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,6 +112,10 @@ func (d *Doubao) List(ctx context.Context, dir model.Obj, args model.ListArgs) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *Doubao) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var downloadUrl string
|
var downloadUrl string
|
||||||
|
|
||||||
if u, ok := file.(*Object); ok {
|
if u, ok := file.(*Object); ok {
|
||||||
@@ -160,6 +181,10 @@ func (d *Doubao) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *Doubao) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var r UploadNodeResp
|
var r UploadNodeResp
|
||||||
_, err := d.request("/samantha/aispace/upload_node", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/samantha/aispace/upload_node", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
@@ -177,6 +202,10 @@ func (d *Doubao) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *Doubao) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var r UploadNodeResp
|
var r UploadNodeResp
|
||||||
_, err := d.request("/samantha/aispace/move_node", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/samantha/aispace/move_node", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
@@ -191,6 +220,10 @@ func (d *Doubao) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *Doubao) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var r BaseResp
|
var r BaseResp
|
||||||
_, err := d.request("/samantha/aispace/rename_node", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/samantha/aispace/rename_node", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
@@ -207,6 +240,10 @@ func (d *Doubao) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *Doubao) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var r BaseResp
|
var r BaseResp
|
||||||
_, err := d.request("/samantha/aispace/delete_node", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/samantha/aispace/delete_node", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{"node_list": []base.Json{{"id": obj.GetID()}}})
|
req.SetBody(base.Json{"node_list": []base.Json{{"id": obj.GetID()}}})
|
||||||
@@ -215,6 +252,10 @@ func (d *Doubao) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Doubao) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
func (d *Doubao) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
if err := d.WaitLimit(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// 根据MIME类型确定数据类型
|
// 根据MIME类型确定数据类型
|
||||||
mimetype := file.GetMimetype()
|
mimetype := file.GetMimetype()
|
||||||
dataType := FileDataType
|
dataType := FileDataType
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ type Addition struct {
|
|||||||
// driver.RootPath
|
// driver.RootPath
|
||||||
driver.RootID
|
driver.RootID
|
||||||
// define other
|
// define other
|
||||||
Cookie string `json:"cookie" type:"text"`
|
Cookie string `json:"cookie" type:"text"`
|
||||||
UploadThread string `json:"upload_thread" default:"3"`
|
UploadThread string `json:"upload_thread" default:"3"`
|
||||||
DownloadApi string `json:"download_api" type:"select" options:"get_file_url,get_download_info" default:"get_file_url"`
|
DownloadApi string `json:"download_api" type:"select" options:"get_file_url,get_download_info" default:"get_file_url"`
|
||||||
|
LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate ([limit]r/1s)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
@@ -23,6 +24,10 @@ var config = driver.Config{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
return &Doubao{}
|
return &Doubao{
|
||||||
|
Addition: Addition{
|
||||||
|
LimitRate: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ func (d *Doubao) Upload(ctx context.Context, config *UploadConfig, dstDir model.
|
|||||||
"Authorization": {storeInfo.Auth},
|
"Authorization": {storeInfo.Auth},
|
||||||
"Content-Type": {"application/octet-stream"},
|
"Content-Type": {"application/octet-stream"},
|
||||||
"Content-Crc32": {crc32Value},
|
"Content-Crc32": {crc32Value},
|
||||||
"Content-Length": {fmt.Sprintf("%d", file.GetSize())},
|
"Content-Length": {strconv.FormatInt(file.GetSize(), 10)},
|
||||||
"Content-Disposition": {fmt.Sprintf("attachment; filename=%s", url.QueryEscape(storeInfo.StoreURI))},
|
"Content-Disposition": {fmt.Sprintf("attachment; filename=%s", url.QueryEscape(storeInfo.StoreURI))},
|
||||||
}
|
}
|
||||||
res, err := base.HttpClient.Do(req)
|
res, err := base.HttpClient.Do(req)
|
||||||
@@ -612,7 +612,7 @@ func (d *Doubao) UploadByMultipart(ctx context.Context, config *UploadConfig, fi
|
|||||||
"Authorization": {storeInfo.Auth},
|
"Authorization": {storeInfo.Auth},
|
||||||
"Content-Type": {"application/octet-stream"},
|
"Content-Type": {"application/octet-stream"},
|
||||||
"Content-Crc32": {crc32Value},
|
"Content-Crc32": {crc32Value},
|
||||||
"Content-Length": {fmt.Sprintf("%d", size)},
|
"Content-Length": {strconv.FormatInt(size, 10)},
|
||||||
"Content-Disposition": {fmt.Sprintf("attachment; filename=%s", url.QueryEscape(storeInfo.StoreURI))},
|
"Content-Disposition": {fmt.Sprintf("attachment; filename=%s", url.QueryEscape(storeInfo.StoreURI))},
|
||||||
}
|
}
|
||||||
res, err := base.HttpClient.Do(req)
|
res, err := base.HttpClient.Do(req)
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "FebBox",
|
Name: "FebBox",
|
||||||
NoUpload: true,
|
NoUpload: true,
|
||||||
DefaultRoot: "0",
|
DefaultRoot: "0",
|
||||||
|
LinkCacheMode: driver.LinkCacheIP,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -113,9 +113,7 @@ func (d *FTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
RangeReader: &model.FileRangeReader{
|
RangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader),
|
||||||
RangeReaderIF: stream.RateLimitRangeReaderFunc(resultRangeReader),
|
|
||||||
},
|
|
||||||
SyncClosers: utils.NewSyncClosers(utils.CloseFunc(conn.Quit)),
|
SyncClosers: utils.NewSyncClosers(utils.CloseFunc(conn.Quit)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "FTP",
|
Name: "FTP",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
OnlyLinkMFile: false,
|
OnlyProxy: true,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
NoLinkURL: true,
|
NoLinkURL: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis
|
|||||||
if d.Addition.ShowReadme {
|
if d.Addition.ShowReadme {
|
||||||
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)
|
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)
|
||||||
}
|
}
|
||||||
|
if d.Addition.ShowSourceCode{
|
||||||
|
files = append(files, point.GetSourceCode()...)
|
||||||
|
}
|
||||||
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
|
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
|
||||||
nextDir := GetNextDir(point.Point, path)
|
nextDir := GetNextDir(point.Point, path)
|
||||||
if nextDir == "" {
|
if nextDir == "" {
|
||||||
@@ -117,6 +120,10 @@ func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.Lis
|
|||||||
}
|
}
|
||||||
|
|
||||||
files = append(files, point.GetReleaseByTagName(tagName)...)
|
files = append(files, point.GetReleaseByTagName(tagName)...)
|
||||||
|
|
||||||
|
if d.Addition.ShowSourceCode{
|
||||||
|
files = append(files, point.GetSourceCodeByTagName(tagName)...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type Addition struct {
|
|||||||
RepoStructure string `json:"repo_structure" type:"text" required:"true" default:"OpenListTeam/OpenList" help:"structure:[path:]org/repo"`
|
RepoStructure string `json:"repo_structure" type:"text" required:"true" default:"OpenListTeam/OpenList" help:"structure:[path:]org/repo"`
|
||||||
ShowReadme bool `json:"show_readme" type:"bool" default:"true" help:"show README、LICENSE file"`
|
ShowReadme bool `json:"show_readme" type:"bool" default:"true" help:"show README、LICENSE file"`
|
||||||
Token string `json:"token" type:"string" required:"false" help:"GitHub token, if you want to access private repositories or increase the rate limit"`
|
Token string `json:"token" type:"string" required:"false" help:"GitHub token, if you want to access private repositories or increase the rate limit"`
|
||||||
|
ShowSourceCode bool `json:"show_source_code" type:"bool" default:"false" help:"show Source code (zip/tar.gz)"`
|
||||||
ShowAllVersion bool `json:"show_all_version" type:"bool" default:"false" help:"show all versions"`
|
ShowAllVersion bool `json:"show_all_version" type:"bool" default:"false" help:"show all versions"`
|
||||||
GitHubProxy string `json:"gh_proxy" type:"string" default:"" help:"GitHub proxy, e.g. https://ghproxy.net/github.com or https://gh-proxy.com/github.com "`
|
GitHubProxy string `json:"gh_proxy" type:"string" default:"" help:"GitHub proxy, e.g. https://ghproxy.net/github.com or https://gh-proxy.com/github.com "`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,60 @@ func (m *MountPoint) GetAllVersionSize() int64 {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MountPoint) GetSourceCode() []File {
|
||||||
|
files := make([]File, 0)
|
||||||
|
|
||||||
|
// 无法获取文件大小,此处设为 1
|
||||||
|
files = append(files, File{
|
||||||
|
Path: m.Point + "/" + "Source code (zip)",
|
||||||
|
FileName: "Source code (zip)",
|
||||||
|
Size: 1,
|
||||||
|
Type: "file",
|
||||||
|
UpdateAt: m.Release.CreatedAt,
|
||||||
|
CreateAt: m.Release.CreatedAt,
|
||||||
|
Url: m.Release.ZipballUrl,
|
||||||
|
})
|
||||||
|
files = append(files, File{
|
||||||
|
Path: m.Point + "/" + "Source code (tar.gz)",
|
||||||
|
FileName: "Source code (tar.gz)",
|
||||||
|
Size: 1,
|
||||||
|
Type: "file",
|
||||||
|
UpdateAt: m.Release.CreatedAt,
|
||||||
|
CreateAt: m.Release.CreatedAt,
|
||||||
|
Url: m.Release.TarballUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MountPoint) GetSourceCodeByTagName(tagName string) []File {
|
||||||
|
for _, item := range *m.Releases {
|
||||||
|
if item.TagName == tagName {
|
||||||
|
files := make([]File, 0)
|
||||||
|
files = append(files, File{
|
||||||
|
Path: m.Point + "/" + "Source code (zip)",
|
||||||
|
FileName: "Source code (zip)",
|
||||||
|
Size: 1,
|
||||||
|
Type: "file",
|
||||||
|
UpdateAt: item.CreatedAt,
|
||||||
|
CreateAt: item.CreatedAt,
|
||||||
|
Url: item.ZipballUrl,
|
||||||
|
})
|
||||||
|
files = append(files, File{
|
||||||
|
Path: m.Point + "/" + "Source code (tar.gz)",
|
||||||
|
FileName: "Source code (tar.gz)",
|
||||||
|
Size: 1,
|
||||||
|
Type: "file",
|
||||||
|
UpdateAt: item.CreatedAt,
|
||||||
|
CreateAt: item.CreatedAt,
|
||||||
|
Url: item.TarballUrl,
|
||||||
|
})
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MountPoint) GetOtherFile(get func(url string) (*resty.Response, error), refresh bool) []File {
|
func (m *MountPoint) GetOtherFile(get func(url string) (*resty.Response, error), refresh bool) []File {
|
||||||
if m.OtherFile == nil || refresh {
|
if m.OtherFile == nil || refresh {
|
||||||
resp, _ := get("https://api.github.com/repos/" + m.Repo + "/contents")
|
resp, _ := get("https://api.github.com/repos/" + m.Repo + "/contents")
|
||||||
|
|||||||
@@ -189,10 +189,7 @@ func (d *GoogleDrive) GetDetails(ctx context.Context) (*model.StorageDetails, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: total - used,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ import (
|
|||||||
|
|
||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
|
// Google Drive API field constants
|
||||||
|
const (
|
||||||
|
// File list query fields
|
||||||
|
FilesListFields = "files(id,name,mimeType,size,modifiedTime,createdTime,thumbnailLink,shortcutDetails,md5Checksum,sha1Checksum,sha256Checksum),nextPageToken"
|
||||||
|
// Single file query fields
|
||||||
|
FileInfoFields = "id,name,mimeType,size,md5Checksum,sha1Checksum,sha256Checksum"
|
||||||
|
)
|
||||||
|
|
||||||
type googleDriveServiceAccount struct {
|
type googleDriveServiceAccount struct {
|
||||||
// Type string `json:"type"`
|
// Type string `json:"type"`
|
||||||
// ProjectID string `json:"project_id"`
|
// ProjectID string `json:"project_id"`
|
||||||
@@ -235,7 +243,7 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
|
|||||||
}
|
}
|
||||||
query := map[string]string{
|
query := map[string]string{
|
||||||
"orderBy": orderBy,
|
"orderBy": orderBy,
|
||||||
"fields": "files(id,name,mimeType,size,modifiedTime,createdTime,thumbnailLink,shortcutDetails,md5Checksum,sha1Checksum,sha256Checksum),nextPageToken",
|
"fields": FilesListFields,
|
||||||
"pageSize": "1000",
|
"pageSize": "1000",
|
||||||
"q": fmt.Sprintf("'%s' in parents and trashed = false", id),
|
"q": fmt.Sprintf("'%s' in parents and trashed = false", id),
|
||||||
//"includeItemsFromAllDrives": "true",
|
//"includeItemsFromAllDrives": "true",
|
||||||
@@ -249,11 +257,82 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pageToken = resp.NextPageToken
|
pageToken = resp.NextPageToken
|
||||||
|
|
||||||
|
// Batch process shortcuts, API calls only for file shortcuts
|
||||||
|
shortcutTargetIds := make([]string, 0)
|
||||||
|
shortcutIndices := make([]int, 0)
|
||||||
|
|
||||||
|
// Collect target IDs of all file shortcuts (skip folder shortcuts)
|
||||||
|
for i := range resp.Files {
|
||||||
|
if resp.Files[i].MimeType == "application/vnd.google-apps.shortcut" &&
|
||||||
|
resp.Files[i].ShortcutDetails.TargetId != "" &&
|
||||||
|
resp.Files[i].ShortcutDetails.TargetMimeType != "application/vnd.google-apps.folder" {
|
||||||
|
shortcutTargetIds = append(shortcutTargetIds, resp.Files[i].ShortcutDetails.TargetId)
|
||||||
|
shortcutIndices = append(shortcutIndices, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch get target file info (only for file shortcuts)
|
||||||
|
if len(shortcutTargetIds) > 0 {
|
||||||
|
targetFiles := d.batchGetTargetFilesInfo(shortcutTargetIds)
|
||||||
|
// Update shortcut file info
|
||||||
|
for j, targetId := range shortcutTargetIds {
|
||||||
|
if targetFile, exists := targetFiles[targetId]; exists {
|
||||||
|
fileIndex := shortcutIndices[j]
|
||||||
|
if targetFile.Size != "" {
|
||||||
|
resp.Files[fileIndex].Size = targetFile.Size
|
||||||
|
}
|
||||||
|
if targetFile.MD5Checksum != "" {
|
||||||
|
resp.Files[fileIndex].MD5Checksum = targetFile.MD5Checksum
|
||||||
|
}
|
||||||
|
if targetFile.SHA1Checksum != "" {
|
||||||
|
resp.Files[fileIndex].SHA1Checksum = targetFile.SHA1Checksum
|
||||||
|
}
|
||||||
|
if targetFile.SHA256Checksum != "" {
|
||||||
|
resp.Files[fileIndex].SHA256Checksum = targetFile.SHA256Checksum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res = append(res, resp.Files...)
|
res = append(res, resp.Files...)
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTargetFileInfo gets target file details for shortcuts
|
||||||
|
func (d *GoogleDrive) getTargetFileInfo(targetId string) (File, error) {
|
||||||
|
var targetFile File
|
||||||
|
url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s", targetId)
|
||||||
|
query := map[string]string{
|
||||||
|
"fields": FileInfoFields,
|
||||||
|
}
|
||||||
|
_, err := d.request(url, http.MethodGet, func(req *resty.Request) {
|
||||||
|
req.SetQueryParams(query)
|
||||||
|
}, &targetFile)
|
||||||
|
if err != nil {
|
||||||
|
return File{}, err
|
||||||
|
}
|
||||||
|
return targetFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// batchGetTargetFilesInfo batch gets target file info, sequential processing to avoid concurrency complexity
|
||||||
|
func (d *GoogleDrive) batchGetTargetFilesInfo(targetIds []string) map[string]File {
|
||||||
|
if len(targetIds) == 0 {
|
||||||
|
return make(map[string]File)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]File)
|
||||||
|
// Sequential processing to avoid concurrency complexity
|
||||||
|
for _, targetId := range targetIds {
|
||||||
|
file, err := d.getTargetFileInfo(targetId)
|
||||||
|
if err == nil {
|
||||||
|
result[targetId] = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (d *GoogleDrive) chunkUpload(ctx context.Context, file model.FileStreamer, url string, up driver.UpdateProgress) error {
|
func (d *GoogleDrive) chunkUpload(ctx context.Context, file model.FileStreamer, url string, up driver.UpdateProgress) error {
|
||||||
defaultChunkSize := d.ChunkSize * 1024 * 1024
|
defaultChunkSize := d.ChunkSize * 1024 * 1024
|
||||||
ss, err := stream.NewStreamSectionReader(file, int(defaultChunkSize), &up)
|
ss, err := stream.NewStreamSectionReader(file, int(defaultChunkSize), &up)
|
||||||
|
|||||||
111
drivers/halalcloud_open/common.go
Normal file
111
drivers/halalcloud_open/common.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sdkUser "github.com/halalcloud/golang-sdk-lite/halalcloud/services/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
slicePostErrorRetryInterval = time.Second * 120
|
||||||
|
retryTimes = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type halalCommon struct {
|
||||||
|
// *AuthService // 登录信息
|
||||||
|
UserInfo *sdkUser.User // 用户信息
|
||||||
|
refreshTokenFunc func(token string) error
|
||||||
|
// serv *AuthService
|
||||||
|
configs sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *halalCommon) GetAccessToken() (string, error) {
|
||||||
|
value, exists := m.configs.Load("access_token")
|
||||||
|
if !exists {
|
||||||
|
return "", nil // 如果不存在,返回空字符串
|
||||||
|
}
|
||||||
|
return value.(string), nil // 返回配置项的值
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefreshToken implements ConfigStore.
|
||||||
|
func (m *halalCommon) GetRefreshToken() (string, error) {
|
||||||
|
value, exists := m.configs.Load("refresh_token")
|
||||||
|
if !exists {
|
||||||
|
return "", nil // 如果不存在,返回空字符串
|
||||||
|
}
|
||||||
|
return value.(string), nil // 返回配置项的值
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAccessToken implements ConfigStore.
|
||||||
|
func (m *halalCommon) SetAccessToken(token string) error {
|
||||||
|
m.configs.Store("access_token", token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRefreshToken implements ConfigStore.
|
||||||
|
func (m *halalCommon) SetRefreshToken(token string) error {
|
||||||
|
m.configs.Store("refresh_token", token)
|
||||||
|
if m.refreshTokenFunc != nil {
|
||||||
|
return m.refreshTokenFunc(token)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetToken implements ConfigStore.
|
||||||
|
func (m *halalCommon) SetToken(accessToken string, refreshToken string, expiresIn int64) error {
|
||||||
|
m.configs.Store("access_token", accessToken)
|
||||||
|
m.configs.Store("refresh_token", refreshToken)
|
||||||
|
m.configs.Store("expires_in", expiresIn)
|
||||||
|
if m.refreshTokenFunc != nil {
|
||||||
|
return m.refreshTokenFunc(refreshToken)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearConfigs implements ConfigStore.
|
||||||
|
func (m *halalCommon) ClearConfigs() error {
|
||||||
|
m.configs = sync.Map{} // 清空map
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteConfig implements ConfigStore.
|
||||||
|
func (m *halalCommon) DeleteConfig(key string) error {
|
||||||
|
_, exists := m.configs.Load(key)
|
||||||
|
if !exists {
|
||||||
|
return nil // 如果不存在,直接返回
|
||||||
|
}
|
||||||
|
m.configs.Delete(key) // 删除指定的配置项
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig implements ConfigStore.
|
||||||
|
func (m *halalCommon) GetConfig(key string) (string, error) {
|
||||||
|
value, exists := m.configs.Load(key)
|
||||||
|
if !exists {
|
||||||
|
return "", nil // 如果不存在,返回空字符串
|
||||||
|
}
|
||||||
|
return value.(string), nil // 返回配置项的值
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListConfigs implements ConfigStore.
|
||||||
|
func (m *halalCommon) ListConfigs() (map[string]string, error) {
|
||||||
|
configs := make(map[string]string)
|
||||||
|
m.configs.Range(func(key, value interface{}) bool {
|
||||||
|
configs[key.(string)] = value.(string) // 将每个配置项添加到map中
|
||||||
|
return true // 继续遍历
|
||||||
|
})
|
||||||
|
return configs, nil // 返回所有配置项
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig implements ConfigStore.
|
||||||
|
func (m *halalCommon) SetConfig(key string, value string) error {
|
||||||
|
m.configs.Store(key, value) // 使用Store方法设置或更新配置项
|
||||||
|
return nil // 成功设置配置项后返回nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHalalCommon() *halalCommon {
|
||||||
|
return &halalCommon{
|
||||||
|
configs: sync.Map{},
|
||||||
|
}
|
||||||
|
}
|
||||||
29
drivers/halalcloud_open/driver.go
Normal file
29
drivers/halalcloud_open/driver.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
sdkClient "github.com/halalcloud/golang-sdk-lite/halalcloud/apiclient"
|
||||||
|
sdkUser "github.com/halalcloud/golang-sdk-lite/halalcloud/services/user"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HalalCloudOpen struct {
|
||||||
|
*halalCommon
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
sdkClient *sdkClient.Client
|
||||||
|
sdkUserFileService *sdkUserFile.UserFileService
|
||||||
|
sdkUserService *sdkUser.UserService
|
||||||
|
uploadThread int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*HalalCloudOpen)(nil)
|
||||||
131
drivers/halalcloud_open/driver_curd_impl.go
Normal file
131
drivers/halalcloud_open/driver_curd_impl.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
sdkModel "github.com/halalcloud/golang-sdk-lite/halalcloud/model"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) getFiles(ctx context.Context, dir model.Obj) ([]model.Obj, error) {
|
||||||
|
|
||||||
|
files := make([]model.Obj, 0)
|
||||||
|
limit := int64(100)
|
||||||
|
token := ""
|
||||||
|
|
||||||
|
for {
|
||||||
|
result, err := d.sdkUserFileService.List(ctx, &sdkUserFile.FileListRequest{
|
||||||
|
Parent: &sdkUserFile.File{Path: dir.GetPath()},
|
||||||
|
ListInfo: &sdkModel.ScanListRequest{
|
||||||
|
Limit: strconv.FormatInt(limit, 10),
|
||||||
|
Token: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; len(result.Files) > i; i++ {
|
||||||
|
files = append(files, NewObjFile(result.Files[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.ListInfo == nil || result.ListInfo.Token == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
token = result.ListInfo.Token
|
||||||
|
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) makeDir(ctx context.Context, dir model.Obj, name string) (model.Obj, error) {
|
||||||
|
_, err := d.sdkUserFileService.Create(ctx, &sdkUserFile.File{
|
||||||
|
Path: dir.GetPath(),
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) move(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {
|
||||||
|
oldDir := obj.GetPath()
|
||||||
|
newDir := dir.GetPath()
|
||||||
|
_, err := d.sdkUserFileService.Move(ctx, &sdkUserFile.BatchOperationRequest{
|
||||||
|
Source: []*sdkUserFile.File{
|
||||||
|
{
|
||||||
|
Path: oldDir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dest: &sdkUserFile.File{
|
||||||
|
Path: newDir,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) rename(ctx context.Context, obj model.Obj, name string) (model.Obj, error) {
|
||||||
|
|
||||||
|
_, err := d.sdkUserFileService.Rename(ctx, &sdkUserFile.File{
|
||||||
|
Path: obj.GetPath(),
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) copy(ctx context.Context, obj model.Obj, dir model.Obj) (model.Obj, error) {
|
||||||
|
id := obj.GetID()
|
||||||
|
sourcePath := obj.GetPath()
|
||||||
|
if len(id) > 0 {
|
||||||
|
sourcePath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
destID := dir.GetID()
|
||||||
|
destPath := dir.GetPath()
|
||||||
|
if len(destID) > 0 {
|
||||||
|
destPath = ""
|
||||||
|
}
|
||||||
|
dest := &sdkUserFile.File{
|
||||||
|
Path: destPath,
|
||||||
|
Identity: destID,
|
||||||
|
}
|
||||||
|
_, err := d.sdkUserFileService.Copy(ctx, &sdkUserFile.BatchOperationRequest{
|
||||||
|
Source: []*sdkUserFile.File{
|
||||||
|
{
|
||||||
|
Path: sourcePath,
|
||||||
|
Identity: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dest: dest,
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
id := obj.GetID()
|
||||||
|
_, err := d.sdkUserFileService.Delete(ctx, &sdkUserFile.BatchOperationRequest{
|
||||||
|
Source: []*sdkUserFile.File{
|
||||||
|
{
|
||||||
|
Identity: id,
|
||||||
|
Path: obj.GetPath(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) details(ctx context.Context) (*model.StorageDetails, error) {
|
||||||
|
ret, err := d.sdkUserService.GetStatisticsAndQuota(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
total := uint64(ret.DiskStatisticsQuota.BytesQuota)
|
||||||
|
|
||||||
|
free := uint64(ret.DiskStatisticsQuota.BytesFree)
|
||||||
|
return &model.StorageDetails{
|
||||||
|
DiskUsage: model.DiskUsage{
|
||||||
|
TotalSpace: total,
|
||||||
|
FreeSpace: free,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
108
drivers/halalcloud_open/driver_get_link.go
Normal file
108
drivers/halalcloud_open/driver_get_link.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
"github.com/rclone/rclone/lib/readers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) getLink(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
if args.Redirect {
|
||||||
|
// return nil, model.ErrUnsupported
|
||||||
|
fid := file.GetID()
|
||||||
|
fpath := file.GetPath()
|
||||||
|
if fid != "" {
|
||||||
|
fpath = ""
|
||||||
|
}
|
||||||
|
fi, err := d.sdkUserFileService.GetDirectDownloadAddress(ctx, &sdkUserFile.DirectDownloadRequest{
|
||||||
|
Identity: fid,
|
||||||
|
Path: fpath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expireAt := fi.ExpireAt
|
||||||
|
duration := time.Until(time.UnixMilli(expireAt))
|
||||||
|
return &model.Link{
|
||||||
|
URL: fi.DownloadAddress,
|
||||||
|
Expiration: &duration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
result, err := d.sdkUserFileService.ParseFileSlice(ctx, &sdkUserFile.File{
|
||||||
|
Identity: file.GetID(),
|
||||||
|
Path: file.GetPath(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fileAddrs := []*sdkUserFile.SliceDownloadInfo{}
|
||||||
|
var addressDuration int64
|
||||||
|
|
||||||
|
nodesNumber := len(result.RawNodes)
|
||||||
|
nodesIndex := nodesNumber - 1
|
||||||
|
startIndex, endIndex := 0, nodesIndex
|
||||||
|
for nodesIndex >= 0 {
|
||||||
|
if nodesIndex >= 200 {
|
||||||
|
endIndex = 200
|
||||||
|
} else {
|
||||||
|
endIndex = nodesNumber
|
||||||
|
}
|
||||||
|
for ; endIndex <= nodesNumber; endIndex += 200 {
|
||||||
|
if endIndex == 0 {
|
||||||
|
endIndex = 1
|
||||||
|
}
|
||||||
|
sliceAddress, err := d.sdkUserFileService.GetSliceDownloadAddress(ctx, &sdkUserFile.SliceDownloadAddressRequest{
|
||||||
|
Identity: result.RawNodes[startIndex:endIndex],
|
||||||
|
Version: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addressDuration, _ = strconv.ParseInt(sliceAddress.ExpireAt, 10, 64)
|
||||||
|
fileAddrs = append(fileAddrs, sliceAddress.Addresses...)
|
||||||
|
startIndex = endIndex
|
||||||
|
nodesIndex -= 200
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
size, _ := strconv.ParseInt(result.FileSize, 10, 64)
|
||||||
|
chunks := getChunkSizes(result.Sizes)
|
||||||
|
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
||||||
|
length := httpRange.Length
|
||||||
|
if httpRange.Length < 0 || httpRange.Start+httpRange.Length >= size {
|
||||||
|
length = size - httpRange.Start
|
||||||
|
}
|
||||||
|
oo := &openObject{
|
||||||
|
ctx: ctx,
|
||||||
|
d: fileAddrs,
|
||||||
|
chunk: []byte{},
|
||||||
|
chunks: chunks,
|
||||||
|
skip: httpRange.Start,
|
||||||
|
sha: result.Sha1,
|
||||||
|
shaTemp: sha1.New(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return readers.NewLimitedReadCloser(oo, length), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var duration time.Duration
|
||||||
|
if addressDuration != 0 {
|
||||||
|
duration = time.Until(time.UnixMilli(addressDuration))
|
||||||
|
} else {
|
||||||
|
duration = time.Until(time.Now().Add(time.Hour))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Link{
|
||||||
|
RangeReader: stream.RateLimitRangeReaderFunc(resultRangeReader),
|
||||||
|
Expiration: &duration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
50
drivers/halalcloud_open/driver_init.go
Normal file
50
drivers/halalcloud_open/driver_init.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
"github.com/halalcloud/golang-sdk-lite/halalcloud/apiclient"
|
||||||
|
sdkUser "github.com/halalcloud/golang-sdk-lite/halalcloud/services/user"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Init(ctx context.Context) error {
|
||||||
|
if d.uploadThread < 1 || d.uploadThread > 32 {
|
||||||
|
d.uploadThread, d.UploadThread = 3, 3
|
||||||
|
}
|
||||||
|
if d.halalCommon == nil {
|
||||||
|
d.halalCommon = &halalCommon{
|
||||||
|
UserInfo: &sdkUser.User{},
|
||||||
|
refreshTokenFunc: func(token string) error {
|
||||||
|
d.Addition.RefreshToken = token
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.Addition.RefreshToken != "" {
|
||||||
|
d.halalCommon.SetRefreshToken(d.Addition.RefreshToken)
|
||||||
|
}
|
||||||
|
timeout := d.Addition.TimeOut
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 60
|
||||||
|
}
|
||||||
|
host := d.Addition.Host
|
||||||
|
if host == "" {
|
||||||
|
host = "openapi.2dland.cn"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := apiclient.NewClient(nil, host, d.Addition.ClientID, d.Addition.ClientSecret, d.halalCommon, apiclient.WithTimeout(time.Second*time.Duration(timeout)))
|
||||||
|
d.sdkClient = client
|
||||||
|
d.sdkUserFileService = sdkUserFile.NewUserFileService(client)
|
||||||
|
d.sdkUserService = sdkUser.NewUserService(client)
|
||||||
|
userInfo, err := d.sdkUserService.Get(ctx, &sdkUser.User{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.halalCommon.UserInfo = userInfo
|
||||||
|
// 能够获取到用户信息,已经检查了 RefreshToken 的有效性,无需再次检查
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
drivers/halalcloud_open/driver_interface.go
Normal file
48
drivers/halalcloud_open/driver_interface.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
return d.getFiles(ctx, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
return d.getLink(ctx, file, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
return d.makeDir(ctx, parentDir, dirName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return d.move(ctx, srcObj, dstDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
return d.rename(ctx, srcObj, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return d.copy(ctx, srcObj, dstDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
return d.remove(ctx, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
return d.put(ctx, dstDir, stream, up)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
|
||||||
|
return d.details(ctx)
|
||||||
|
}
|
||||||
258
drivers/halalcloud_open/halalcloud_upload.go
Normal file
258
drivers/halalcloud_open/halalcloud_upload.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *HalalCloudOpen) put(ctx context.Context, dstDir model.Obj, fileStream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
|
||||||
|
newPath := path.Join(dstDir.GetPath(), fileStream.GetName())
|
||||||
|
|
||||||
|
uploadTask, err := d.sdkUserFileService.CreateUploadTask(ctx, &sdkUserFile.File{
|
||||||
|
Path: newPath,
|
||||||
|
Size: fileStream.GetSize(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadTask.Created {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slicesList := make([]string, 0)
|
||||||
|
codec := uint64(0x55)
|
||||||
|
if uploadTask.BlockCodec > 0 {
|
||||||
|
codec = uint64(uploadTask.BlockCodec)
|
||||||
|
}
|
||||||
|
blockHashType := uploadTask.BlockHashType
|
||||||
|
mhType := uint64(0x12)
|
||||||
|
if blockHashType > 0 {
|
||||||
|
mhType = uint64(blockHashType)
|
||||||
|
}
|
||||||
|
prefix := cid.Prefix{
|
||||||
|
Codec: codec,
|
||||||
|
MhLength: -1,
|
||||||
|
MhType: mhType,
|
||||||
|
Version: 1,
|
||||||
|
}
|
||||||
|
blockSize := uploadTask.BlockSize
|
||||||
|
useSingleUpload := true
|
||||||
|
//
|
||||||
|
if fileStream.GetSize() <= int64(blockSize) || d.uploadThread <= 1 {
|
||||||
|
useSingleUpload = true
|
||||||
|
}
|
||||||
|
// Not sure whether FileStream supports concurrent read and write operations, so currently using single-threaded upload to ensure safety.
|
||||||
|
// read file
|
||||||
|
if useSingleUpload {
|
||||||
|
bufferSize := int(blockSize)
|
||||||
|
buffer := make([]byte, bufferSize)
|
||||||
|
reader := driver.NewLimitedUploadStream(ctx, fileStream)
|
||||||
|
teeReader := io.TeeReader(reader, driver.NewProgress(fileStream.GetSize(), up))
|
||||||
|
// fileStream.Seek(0, os.SEEK_SET)
|
||||||
|
for {
|
||||||
|
n, err := teeReader.Read(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
data := buffer[:n]
|
||||||
|
uploadCid, err := postFileSlice(ctx, data, uploadTask.Task, uploadTask.UploadAddress, prefix, retryTimes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
slicesList = append(slicesList, uploadCid.String())
|
||||||
|
}
|
||||||
|
if err == io.EOF || n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: implement multipart upload, currently using single-threaded upload to ensure safety.
|
||||||
|
bufferSize := int(blockSize)
|
||||||
|
buffer := make([]byte, bufferSize)
|
||||||
|
reader := driver.NewLimitedUploadStream(ctx, fileStream)
|
||||||
|
teeReader := io.TeeReader(reader, driver.NewProgress(fileStream.GetSize(), up))
|
||||||
|
for {
|
||||||
|
n, err := teeReader.Read(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
data := buffer[:n]
|
||||||
|
uploadCid, err := postFileSlice(ctx, data, uploadTask.Task, uploadTask.UploadAddress, prefix, retryTimes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
slicesList = append(slicesList, uploadCid.String())
|
||||||
|
}
|
||||||
|
if err == io.EOF || n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newFile, err := makeFile(ctx, slicesList, uploadTask.Task, uploadTask.UploadAddress, retryTimes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewObjFile(newFile), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFile(ctx context.Context, fileSlice []string, taskID string, uploadAddress string, retry int) (*sdkUserFile.File, error) {
|
||||||
|
var lastError error = nil
|
||||||
|
for range retry {
|
||||||
|
newFile, err := doMakeFile(fileSlice, taskID, uploadAddress)
|
||||||
|
if err == nil {
|
||||||
|
return newFile, nil
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lastError = err
|
||||||
|
time.Sleep(slicePostErrorRetryInterval)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("mk file slice failed after %d times, error: %s", retry, lastError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func doMakeFile(fileSlice []string, taskID string, uploadAddress string) (*sdkUserFile.File, error) {
|
||||||
|
accessUrl := uploadAddress + "/" + taskID
|
||||||
|
getTimeOut := time.Minute * 2
|
||||||
|
u, err := url.Parse(accessUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n, _ := json.Marshal(fileSlice)
|
||||||
|
httpRequest := http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: u,
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Accept": {"application/json"},
|
||||||
|
"Content-Type": {"application/json"},
|
||||||
|
//"Content-Length": {strconv.Itoa(len(n))},
|
||||||
|
},
|
||||||
|
Body: io.NopCloser(bytes.NewReader(n)),
|
||||||
|
}
|
||||||
|
httpClient := http.Client{
|
||||||
|
Timeout: getTimeOut,
|
||||||
|
}
|
||||||
|
httpResponse, err := httpClient.Do(&httpRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer httpResponse.Body.Close()
|
||||||
|
if httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated {
|
||||||
|
b, _ := io.ReadAll(httpResponse.Body)
|
||||||
|
message := string(b)
|
||||||
|
return nil, fmt.Errorf("mk file slice failed, status code: %d, message: %s", httpResponse.StatusCode, message)
|
||||||
|
}
|
||||||
|
b, _ := io.ReadAll(httpResponse.Body)
|
||||||
|
var result *sdkUserFile.File
|
||||||
|
err = json.Unmarshal(b, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
func postFileSlice(ctx context.Context, fileSlice []byte, taskID string, uploadAddress string, preix cid.Prefix, retry int) (cid.Cid, error) {
|
||||||
|
var lastError error = nil
|
||||||
|
for range retry {
|
||||||
|
newCid, err := doPostFileSlice(fileSlice, taskID, uploadAddress, preix)
|
||||||
|
if err == nil {
|
||||||
|
return newCid, nil
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
time.Sleep(slicePostErrorRetryInterval)
|
||||||
|
lastError = err
|
||||||
|
}
|
||||||
|
return cid.Undef, fmt.Errorf("upload file slice failed after %d times, error: %s", retry, lastError.Error())
|
||||||
|
}
|
||||||
|
func doPostFileSlice(fileSlice []byte, taskID string, uploadAddress string, preix cid.Prefix) (cid.Cid, error) {
|
||||||
|
// 1. sum file slice
|
||||||
|
newCid, err := preix.Sum(fileSlice)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
// 2. post file slice
|
||||||
|
sliceCidString := newCid.String()
|
||||||
|
// /{taskID}/{sliceID}
|
||||||
|
accessUrl := uploadAddress + "/" + taskID + "/" + sliceCidString
|
||||||
|
getTimeOut := time.Second * 30
|
||||||
|
// get {accessUrl} in {getTimeOut}
|
||||||
|
u, err := url.Parse(accessUrl)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
// header: accept: application/json
|
||||||
|
// header: content-type: application/octet-stream
|
||||||
|
// header: content-length: {fileSlice.length}
|
||||||
|
// header: x-content-cid: {sliceCidString}
|
||||||
|
// header: x-task-id: {taskID}
|
||||||
|
httpRequest := http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: u,
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Accept": {"application/json"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
httpClient := http.Client{
|
||||||
|
Timeout: getTimeOut,
|
||||||
|
}
|
||||||
|
httpResponse, err := httpClient.Do(&httpRequest)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
if httpResponse.StatusCode != http.StatusOK {
|
||||||
|
return cid.Undef, fmt.Errorf("upload file slice failed, status code: %d", httpResponse.StatusCode)
|
||||||
|
}
|
||||||
|
var result bool
|
||||||
|
b, err := io.ReadAll(httpResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &result)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
if result {
|
||||||
|
return newCid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequest = http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: u,
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Accept": {"application/json"},
|
||||||
|
"Content-Type": {"application/octet-stream"},
|
||||||
|
// "Content-Length": {strconv.Itoa(len(fileSlice))},
|
||||||
|
},
|
||||||
|
Body: io.NopCloser(bytes.NewReader(fileSlice)),
|
||||||
|
}
|
||||||
|
httpResponse, err = httpClient.Do(&httpRequest)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
defer httpResponse.Body.Close()
|
||||||
|
if httpResponse.StatusCode != http.StatusOK && httpResponse.StatusCode != http.StatusCreated {
|
||||||
|
b, _ := io.ReadAll(httpResponse.Body)
|
||||||
|
message := string(b)
|
||||||
|
return cid.Undef, fmt.Errorf("upload file slice failed, status code: %d, message: %s", httpResponse.StatusCode, message)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
|
||||||
|
return newCid, nil
|
||||||
|
}
|
||||||
32
drivers/halalcloud_open/meta.go
Normal file
32
drivers/halalcloud_open/meta.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
// Usually one of two
|
||||||
|
driver.RootPath
|
||||||
|
// define other
|
||||||
|
RefreshToken string `json:"refresh_token" required:"false" help:"If using a personal API approach, the RefreshToken is not required."`
|
||||||
|
UploadThread int `json:"upload_thread" type:"number" default:"3" help:"1 <= thread <= 32"`
|
||||||
|
|
||||||
|
ClientID string `json:"client_id" required:"true" default:""`
|
||||||
|
ClientSecret string `json:"client_secret" required:"true" default:""`
|
||||||
|
Host string `json:"host" required:"false" default:"openapi.2dland.cn"`
|
||||||
|
TimeOut int `json:"timeout" type:"number" default:"60" help:"timeout in seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "HalalCloudOpen",
|
||||||
|
OnlyProxy: false,
|
||||||
|
DefaultRoot: "/",
|
||||||
|
NoLinkURL: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &HalalCloudOpen{}
|
||||||
|
})
|
||||||
|
}
|
||||||
60
drivers/halalcloud_open/obj_file.go
Normal file
60
drivers/halalcloud_open/obj_file.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ObjFile struct {
|
||||||
|
sdkFile *sdkUserFile.File
|
||||||
|
fileSize int64
|
||||||
|
modTime time.Time
|
||||||
|
createTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewObjFile(f *sdkUserFile.File) model.Obj {
|
||||||
|
ofile := &ObjFile{sdkFile: f}
|
||||||
|
ofile.fileSize = f.Size
|
||||||
|
modTimeTs := f.UpdateTs
|
||||||
|
ofile.modTime = time.UnixMilli(modTimeTs)
|
||||||
|
createTimeTs := f.CreateTs
|
||||||
|
ofile.createTime = time.UnixMilli(createTimeTs)
|
||||||
|
return ofile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) GetSize() int64 {
|
||||||
|
return f.fileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) GetName() string {
|
||||||
|
return f.sdkFile.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) ModTime() time.Time {
|
||||||
|
return f.modTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) IsDir() bool {
|
||||||
|
return f.sdkFile.Dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) GetHash() utils.HashInfo {
|
||||||
|
return utils.HashInfo{
|
||||||
|
// TODO: support more hash types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) GetID() string {
|
||||||
|
return f.sdkFile.Identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) GetPath() string {
|
||||||
|
return f.sdkFile.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ObjFile) CreateTime() time.Time {
|
||||||
|
return f.createTime
|
||||||
|
}
|
||||||
185
drivers/halalcloud_open/utils.go
Normal file
185
drivers/halalcloud_open/utils.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package halalcloudopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
sdkUserFile "github.com/halalcloud/golang-sdk-lite/halalcloud/services/userfile"
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// get the next chunk
|
||||||
|
func (oo *openObject) getChunk(_ context.Context) (err error) {
|
||||||
|
if oo.id >= len(oo.chunks) {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
var chunk []byte
|
||||||
|
err = utils.Retry(3, time.Second, func() (err error) {
|
||||||
|
chunk, err = getRawFiles(oo.d[oo.id])
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oo.id++
|
||||||
|
oo.chunk = chunk
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads up to len(p) bytes into p.
|
||||||
|
func (oo *openObject) Read(p []byte) (n int, err error) {
|
||||||
|
oo.mu.Lock()
|
||||||
|
defer oo.mu.Unlock()
|
||||||
|
if oo.closed {
|
||||||
|
return 0, fmt.Errorf("read on closed file")
|
||||||
|
}
|
||||||
|
// Skip data at the start if requested
|
||||||
|
for oo.skip > 0 {
|
||||||
|
//size := 1024 * 1024
|
||||||
|
_, size, err := oo.ChunkLocation(oo.id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if oo.skip < int64(size) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
oo.id++
|
||||||
|
oo.skip -= int64(size)
|
||||||
|
}
|
||||||
|
if len(oo.chunk) == 0 {
|
||||||
|
err = oo.getChunk(oo.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if oo.skip > 0 {
|
||||||
|
oo.chunk = (oo.chunk)[oo.skip:]
|
||||||
|
oo.skip = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = copy(p, oo.chunk)
|
||||||
|
oo.shaTemp.Write(p[:n])
|
||||||
|
oo.chunk = (oo.chunk)[n:]
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closed the file - MAC errors are reported here
|
||||||
|
func (oo *openObject) Close() (err error) {
|
||||||
|
oo.mu.Lock()
|
||||||
|
defer oo.mu.Unlock()
|
||||||
|
if oo.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 校验Sha1
|
||||||
|
if string(oo.shaTemp.Sum(nil)) != oo.sha {
|
||||||
|
return fmt.Errorf("failed to finish download: SHA mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMD5Hash(text string) string {
|
||||||
|
tHash := md5.Sum([]byte(text))
|
||||||
|
return hex.EncodeToString(tHash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type chunkSize struct {
|
||||||
|
position int64
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
type openObject struct {
|
||||||
|
ctx context.Context
|
||||||
|
mu sync.Mutex
|
||||||
|
d []*sdkUserFile.SliceDownloadInfo
|
||||||
|
id int
|
||||||
|
skip int64
|
||||||
|
chunk []byte
|
||||||
|
chunks []chunkSize
|
||||||
|
closed bool
|
||||||
|
sha string
|
||||||
|
shaTemp hash.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChunkSizes(sliceSize []*sdkUserFile.SliceSize) (chunks []chunkSize) {
|
||||||
|
chunks = make([]chunkSize, 0)
|
||||||
|
for _, s := range sliceSize {
|
||||||
|
// 对最后一个做特殊处理
|
||||||
|
endIndex := s.EndIndex
|
||||||
|
startIndex := s.StartIndex
|
||||||
|
if endIndex == 0 {
|
||||||
|
endIndex = startIndex
|
||||||
|
}
|
||||||
|
for j := startIndex; j <= endIndex; j++ {
|
||||||
|
size := s.Size
|
||||||
|
chunks = append(chunks, chunkSize{position: j, size: int(size)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oo *openObject) ChunkLocation(id int) (position int64, size int, err error) {
|
||||||
|
if id < 0 || id >= len(oo.chunks) {
|
||||||
|
return 0, 0, errors.New("invalid arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (oo.chunks)[id].position, (oo.chunks)[id].size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRawFiles(addr *sdkUserFile.SliceDownloadInfo) ([]byte, error) {
|
||||||
|
|
||||||
|
if addr == nil {
|
||||||
|
return nil, errors.New("addr is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: time.Duration(60 * time.Second), // Set timeout to 60 seconds
|
||||||
|
}
|
||||||
|
resp, err := client.Get(addr.DownloadAddress)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("bad status: %s, body: %s", resp.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Encrypt > 0 {
|
||||||
|
cd := uint8(addr.Encrypt)
|
||||||
|
for idx := 0; idx < len(body); idx++ {
|
||||||
|
body[idx] = body[idx] ^ cd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storeType := addr.StoreType
|
||||||
|
if storeType != 10 {
|
||||||
|
|
||||||
|
sourceCid, err := cid.Decode(addr.Identity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
checkCid, err := sourceCid.Prefix().Sum(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !checkCid.Equals(sourceCid) {
|
||||||
|
return nil, fmt.Errorf("bad cid: %s, body: %s", checkCid.String(), body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
|
||||||
|
}
|
||||||
@@ -409,13 +409,12 @@ func (d *ILanZou) GetDetails(ctx context.Context) (*model.StorageDetails, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
total := utils.Json.Get(res, "map", "totalSize").ToUint64() * 1024
|
totalSize := utils.Json.Get(res, "map", "totalSize").ToUint64() * 1024
|
||||||
|
rewardSize := utils.Json.Get(res, "map", "rewardSize").ToUint64() * 1024
|
||||||
|
total := totalSize + rewardSize
|
||||||
used := utils.Json.Get(res, "map", "usedSize").ToUint64() * 1024
|
used := utils.Json.Get(res, "map", "usedSize").ToUint64() * 1024
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: total,
|
|
||||||
FreeSpace: total - used,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
drivers/local/copy_namedpipes.go
Normal file
16
drivers/local/copy_namedpipes.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build !windows && !plan9 && !netbsd && !aix && !illumos && !solaris && !js
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func copyNamedPipe(dstPath string, mode os.FileMode, dirMode os.FileMode) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstPath), dirMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return syscall.Mkfifo(dstPath, uint32(mode))
|
||||||
|
}
|
||||||
9
drivers/local/copy_namedpipes_x.go
Normal file
9
drivers/local/copy_namedpipes_x.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build windows || plan9 || netbsd || aix || illumos || solaris || js
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func copyNamedPipe(_ string, _, _ os.FileMode) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||||
"github.com/OpenListTeam/times"
|
"github.com/OpenListTeam/times"
|
||||||
cp "github.com/otiai10/copy"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
@@ -235,6 +234,7 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
|
|||||||
func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
fullPath := file.GetPath()
|
fullPath := file.GetPath()
|
||||||
link := &model.Link{}
|
link := &model.Link{}
|
||||||
|
var MFile model.File
|
||||||
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
|
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
|
||||||
var buf *bytes.Buffer
|
var buf *bytes.Buffer
|
||||||
var thumbPath *string
|
var thumbPath *string
|
||||||
@@ -261,9 +261,9 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
link.ContentLength = int64(stat.Size())
|
link.ContentLength = int64(stat.Size())
|
||||||
link.MFile = open
|
MFile = open
|
||||||
} else {
|
} else {
|
||||||
link.MFile = bytes.NewReader(buf.Bytes())
|
MFile = bytes.NewReader(buf.Bytes())
|
||||||
link.ContentLength = int64(buf.Len())
|
link.ContentLength = int64(buf.Len())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -272,13 +272,11 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
link.ContentLength = file.GetSize()
|
link.ContentLength = file.GetSize()
|
||||||
link.MFile = open
|
MFile = open
|
||||||
}
|
|
||||||
link.AddIfCloser(link.MFile)
|
|
||||||
if !d.Config().OnlyLinkMFile {
|
|
||||||
link.RangeReader = stream.GetRangeReaderFromMFile(link.ContentLength, link.MFile)
|
|
||||||
link.MFile = nil
|
|
||||||
}
|
}
|
||||||
|
link.SyncClosers.AddIfCloser(MFile)
|
||||||
|
link.RangeReader = stream.GetRangeReaderFromMFile(link.ContentLength, MFile)
|
||||||
|
link.RequireReference = link.SyncClosers.Length() > 0
|
||||||
return link, nil
|
return link, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,16 +296,9 @@ func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
||||||
}
|
}
|
||||||
err := os.Rename(srcPath, dstPath)
|
err := os.Rename(srcPath, dstPath)
|
||||||
if err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
|
if isCrossDeviceError(err) {
|
||||||
// 跨设备移动,先复制再删除
|
// 跨设备移动,变更为移动任务
|
||||||
if err := d.Copy(ctx, srcObj, dstDir); err != nil {
|
return errs.NotImplement
|
||||||
return err
|
|
||||||
}
|
|
||||||
// 复制成功后直接删除源文件/文件夹
|
|
||||||
if srcObj.IsDir() {
|
|
||||||
return os.RemoveAll(srcObj.GetPath())
|
|
||||||
}
|
|
||||||
return os.Remove(srcObj.GetPath())
|
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
srcParent := filepath.Dir(srcPath)
|
srcParent := filepath.Dir(srcPath)
|
||||||
@@ -348,15 +339,14 @@ func (d *Local) Copy(_ context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
if utils.IsSubPath(srcPath, dstPath) {
|
if utils.IsSubPath(srcPath, dstPath) {
|
||||||
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
return fmt.Errorf("the destination folder is a subfolder of the source folder")
|
||||||
}
|
}
|
||||||
// Copy using otiai10/copy to perform more secure & efficient copy
|
info, err := os.Lstat(srcPath)
|
||||||
err := cp.Copy(srcPath, dstPath, cp.Options{
|
|
||||||
Sync: true, // Sync file to disk after copy, may have performance penalty in filesystem such as ZFS
|
|
||||||
PreserveTimes: true,
|
|
||||||
PreserveOwner: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 复制regular文件会返回errs.NotImplement, 转为复制任务
|
||||||
|
if err = d.tryCopy(srcPath, dstPath, info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if d.directoryMap.Has(filepath.Dir(dstPath)) {
|
if d.directoryMap.Has(filepath.Dir(dstPath)) {
|
||||||
d.directoryMap.UpdateDirSize(filepath.Dir(dstPath))
|
d.directoryMap.UpdateDirSize(filepath.Dir(dstPath))
|
||||||
@@ -375,18 +365,26 @@ func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
err = os.Remove(obj.GetPath())
|
err = os.Remove(obj.GetPath())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !utils.Exists(d.RecycleBinPath) {
|
objPath := obj.GetPath()
|
||||||
err = os.MkdirAll(d.RecycleBinPath, 0o755)
|
objName := obj.GetName()
|
||||||
|
var relPath string
|
||||||
|
relPath, err = filepath.Rel(d.GetRootPath(), filepath.Dir(objPath))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recycleBinPath := filepath.Join(d.RecycleBinPath, relPath)
|
||||||
|
if !utils.Exists(recycleBinPath) {
|
||||||
|
err = os.MkdirAll(recycleBinPath, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dstPath := filepath.Join(d.RecycleBinPath, obj.GetName())
|
dstPath := filepath.Join(recycleBinPath, objName)
|
||||||
if utils.Exists(dstPath) {
|
if utils.Exists(dstPath) {
|
||||||
dstPath = filepath.Join(d.RecycleBinPath, obj.GetName()+"_"+time.Now().Format("20060102150405"))
|
dstPath = filepath.Join(recycleBinPath, objName+"_"+time.Now().Format("20060102150405"))
|
||||||
}
|
}
|
||||||
err = os.Rename(obj.GetPath(), dstPath)
|
err = os.Rename(objPath, dstPath)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "Local",
|
Name: "Local",
|
||||||
OnlyLinkMFile: false,
|
LocalSort: true,
|
||||||
LocalSort: true,
|
OnlyProxy: true,
|
||||||
NoCache: true,
|
NoCache: true,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
NoLinkURL: true,
|
NoLinkURL: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package local
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
@@ -14,7 +15,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/KarpelesLab/reflink"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
@@ -148,7 +151,7 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if d.ThumbCacheFolder != "" {
|
if d.ThumbCacheFolder != "" {
|
||||||
err = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0666)
|
err = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0o666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -405,3 +408,79 @@ func (m *DirectoryMap) DeleteDirNode(dirname string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Local) tryCopy(srcPath, dstPath string, info os.FileInfo) error {
|
||||||
|
if info.Mode()&os.ModeDevice != 0 {
|
||||||
|
return errors.New("cannot copy a device")
|
||||||
|
} else if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return d.copySymlink(srcPath, dstPath)
|
||||||
|
} else if info.Mode()&os.ModeNamedPipe != 0 {
|
||||||
|
return copyNamedPipe(dstPath, info.Mode(), os.FileMode(d.mkdirPerm))
|
||||||
|
} else if info.IsDir() {
|
||||||
|
return d.recurAndTryCopy(srcPath, dstPath)
|
||||||
|
} else {
|
||||||
|
return tryReflinkCopy(srcPath, dstPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Local) copySymlink(srcPath, dstPath string) error {
|
||||||
|
linkOrig, err := os.Readlink(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dstDir := filepath.Dir(dstPath)
|
||||||
|
if !filepath.IsAbs(linkOrig) {
|
||||||
|
srcDir := filepath.Dir(srcPath)
|
||||||
|
rel, err := filepath.Rel(dstDir, srcDir)
|
||||||
|
if err != nil {
|
||||||
|
rel, err = filepath.Abs(srcDir)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
linkOrig = filepath.Clean(filepath.Join(rel, linkOrig))
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(dstDir, os.FileMode(d.mkdirPerm))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Symlink(linkOrig, dstPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Local) recurAndTryCopy(srcPath, dstPath string) error {
|
||||||
|
err := os.MkdirAll(dstPath, os.FileMode(d.mkdirPerm))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
files, err := readDir(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() {
|
||||||
|
sp := filepath.Join(srcPath, f.Name())
|
||||||
|
dp := filepath.Join(dstPath, f.Name())
|
||||||
|
if err = d.tryCopy(sp, dp, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
if f.IsDir() {
|
||||||
|
sp := filepath.Join(srcPath, f.Name())
|
||||||
|
dp := filepath.Join(dstPath, f.Name())
|
||||||
|
if err = d.recurAndTryCopy(sp, dp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryReflinkCopy(srcPath, dstPath string) error {
|
||||||
|
err := reflink.Always(srcPath, dstPath)
|
||||||
|
if errors.Is(err, reflink.ErrReflinkUnsupported) || errors.Is(err, reflink.ErrReflinkFailed) || isCrossDeviceError(err) {
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
package local
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isHidden(f fs.FileInfo, _ string) bool {
|
func isHidden(f fs.FileInfo, _ string) bool {
|
||||||
@@ -27,3 +29,7 @@ func getDiskUsage(path string) (model.DiskUsage, error) {
|
|||||||
FreeSpace: free,
|
FreeSpace: free,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCrossDeviceError(err error) bool {
|
||||||
|
return errors.Is(err, unix.EXDEV)
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,3 +49,7 @@ func getDiskUsage(path string) (model.DiskUsage, error) {
|
|||||||
FreeSpace: freeBytes,
|
FreeSpace: freeBytes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCrossDeviceError(err error) bool {
|
||||||
|
return errors.Is(err, windows.ERROR_NOT_SAME_DEVICE)
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ type Addition struct {
|
|||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "MediaFire",
|
Name: "MediaFire",
|
||||||
LocalSort: false,
|
LocalSort: false,
|
||||||
OnlyLinkMFile: false,
|
|
||||||
OnlyProxy: false,
|
OnlyProxy: false,
|
||||||
NoCache: false,
|
NoCache: false,
|
||||||
NoUpload: false,
|
NoUpload: false,
|
||||||
|
|||||||
@@ -57,18 +57,22 @@ func setBody(body interface{}) base.ReqCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleFolderId(dir model.Obj) interface{} {
|
func handleFolderId(dir model.Obj) interface{} {
|
||||||
if dir.GetID() == "" {
|
if isRootFolder(dir) {
|
||||||
return nil
|
return nil // Root folder doesn't need folderId
|
||||||
}
|
}
|
||||||
return dir.GetID()
|
return dir.GetID()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isRootFolder(dir model.Obj) bool {
|
||||||
|
return dir.GetID() == ""
|
||||||
|
}
|
||||||
|
|
||||||
// API layer methods
|
// API layer methods
|
||||||
|
|
||||||
func (d *Misskey) getFiles(dir model.Obj) ([]model.Obj, error) {
|
func (d *Misskey) getFiles(dir model.Obj) ([]model.Obj, error) {
|
||||||
var files []MFile
|
var files []MFile
|
||||||
var body map[string]string
|
var body map[string]string
|
||||||
if dir.GetPath() != "/" {
|
if !isRootFolder(dir) {
|
||||||
body = map[string]string{"folderId": dir.GetID()}
|
body = map[string]string{"folderId": dir.GetID()}
|
||||||
} else {
|
} else {
|
||||||
body = map[string]string{}
|
body = map[string]string{}
|
||||||
@@ -85,7 +89,7 @@ func (d *Misskey) getFiles(dir model.Obj) ([]model.Obj, error) {
|
|||||||
func (d *Misskey) getFolders(dir model.Obj) ([]model.Obj, error) {
|
func (d *Misskey) getFolders(dir model.Obj) ([]model.Obj, error) {
|
||||||
var folders []MFolder
|
var folders []MFolder
|
||||||
var body map[string]string
|
var body map[string]string
|
||||||
if dir.GetPath() != "/" {
|
if !isRootFolder(dir) {
|
||||||
body = map[string]string{"folderId": dir.GetID()}
|
body = map[string]string{"folderId": dir.GetID()}
|
||||||
} else {
|
} else {
|
||||||
body = map[string]string{}
|
body = map[string]string{}
|
||||||
@@ -197,16 +201,24 @@ func (d *Misskey) put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
Reader: stream,
|
Reader: stream,
|
||||||
UpdateProgress: up,
|
UpdateProgress: up,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Build form data, only add folderId if not root folder
|
||||||
|
formData := map[string]string{
|
||||||
|
"name": stream.GetName(),
|
||||||
|
"comment": "",
|
||||||
|
"isSensitive": "false",
|
||||||
|
"force": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
folderId := handleFolderId(dstDir)
|
||||||
|
if folderId != nil {
|
||||||
|
formData["folderId"] = folderId.(string)
|
||||||
|
}
|
||||||
|
|
||||||
req := base.RestyClient.R().
|
req := base.RestyClient.R().
|
||||||
SetContext(ctx).
|
SetContext(ctx).
|
||||||
SetFileReader("file", stream.GetName(), reader).
|
SetFileReader("file", stream.GetName(), reader).
|
||||||
SetFormData(map[string]string{
|
SetFormData(formData).
|
||||||
"folderId": handleFolderId(dstDir).(string),
|
|
||||||
"name": stream.GetName(),
|
|
||||||
"comment": "",
|
|
||||||
"isSensitive": "false",
|
|
||||||
"force": "false",
|
|
||||||
}).
|
|
||||||
SetResult(&file).
|
SetResult(&file).
|
||||||
SetAuthToken(d.AccessToken)
|
SetAuthToken(d.AccessToken)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Onedrive struct {
|
|||||||
AccessToken string
|
AccessToken string
|
||||||
root *Object
|
root *Object
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
ref *Onedrive
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Onedrive) Config() driver.Config {
|
func (d *Onedrive) Config() driver.Config {
|
||||||
@@ -36,10 +37,22 @@ func (d *Onedrive) Init(ctx context.Context) error {
|
|||||||
if d.ChunkSize < 1 {
|
if d.ChunkSize < 1 {
|
||||||
d.ChunkSize = 5
|
d.ChunkSize = 5
|
||||||
}
|
}
|
||||||
|
if d.ref != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return d.refreshToken()
|
return d.refreshToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Onedrive) InitReference(refStorage driver.Driver) error {
|
||||||
|
if ref, ok := refStorage.(*Onedrive); ok {
|
||||||
|
d.ref = ref
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Onedrive) Drop(ctx context.Context) error {
|
func (d *Onedrive) Drop(ctx context.Context) error {
|
||||||
|
d.ref = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,4 +236,19 @@ func (d *Onedrive) GetDetails(ctx context.Context) (*model.StorageDetails, error
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Onedrive) GetDirectUploadTools() []string {
|
||||||
|
if !d.EnableDirectUpload {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{"HttpDirect"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDirectUploadInfo returns the direct upload info for OneDrive
|
||||||
|
func (d *Onedrive) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
|
||||||
|
if !d.EnableDirectUpload {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
return d.getDirectUploadInfo(ctx, path.Join(dstDir.GetPath(), fileName))
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*Onedrive)(nil)
|
var _ driver.Driver = (*Onedrive)(nil)
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
|
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
|
||||||
IsSharepoint bool `json:"is_sharepoint"`
|
IsSharepoint bool `json:"is_sharepoint"`
|
||||||
UseOnlineAPI bool `json:"use_online_api" default:"true"`
|
UseOnlineAPI bool `json:"use_online_api" default:"true"`
|
||||||
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
|
APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
ClientSecret string `json:"client_secret"`
|
ClientSecret string `json:"client_secret"`
|
||||||
RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
|
RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
SiteId string `json:"site_id"`
|
SiteId string `json:"site_id"`
|
||||||
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
|
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
|
||||||
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
|
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
|
||||||
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
|
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
|
||||||
|
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -133,7 +133,10 @@ func (d *Onedrive) _refreshToken() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref.Request(url, method, callback, resp)
|
||||||
|
}
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
@@ -149,7 +152,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.Error.Code != "" {
|
if e.Error.Code != "" {
|
||||||
if e.Error.Code == "InvalidAuthenticationToken" {
|
if e.Error.Code == "InvalidAuthenticationToken" && !utils.IsBool(noRetry...) {
|
||||||
err = d.refreshToken()
|
err = d.refreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -307,9 +310,36 @@ func (d *Onedrive) getDrive(ctx context.Context) (*DriveResp, error) {
|
|||||||
var resp DriveResp
|
var resp DriveResp
|
||||||
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
|
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
}, &resp)
|
}, &resp, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Onedrive) getDirectUploadInfo(ctx context.Context, path string) (*model.HttpDirectUploadInfo, error) {
|
||||||
|
// Create upload session
|
||||||
|
url := d.GetMetaUrl(false, path) + "/createUploadSession"
|
||||||
|
metadata := map[string]any{
|
||||||
|
"item": map[string]any{
|
||||||
|
"@microsoft.graph.conflictBehavior": "rename",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(metadata).SetContext(ctx)
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
||||||
|
if uploadUrl == "" {
|
||||||
|
return nil, fmt.Errorf("failed to get upload URL from response")
|
||||||
|
}
|
||||||
|
return &model.HttpDirectUploadInfo{
|
||||||
|
UploadURL: uploadUrl,
|
||||||
|
ChunkSize: d.ChunkSize * 1024 * 1024, // Convert MB to bytes
|
||||||
|
Method: "PUT",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -222,4 +222,18 @@ func (d *OnedriveAPP) GetDetails(ctx context.Context) (*model.StorageDetails, er
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *OnedriveAPP) GetDirectUploadTools() []string {
|
||||||
|
if !d.EnableDirectUpload {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{"HttpDirect"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *OnedriveAPP) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
|
||||||
|
if !d.EnableDirectUpload {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
return d.getDirectUploadInfo(ctx, path.Join(dstDir.GetPath(), fileName))
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*OnedriveAPP)(nil)
|
var _ driver.Driver = (*OnedriveAPP)(nil)
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
|
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
|
||||||
ClientID string `json:"client_id" required:"true"`
|
ClientID string `json:"client_id" required:"true"`
|
||||||
ClientSecret string `json:"client_secret" required:"true"`
|
ClientSecret string `json:"client_secret" required:"true"`
|
||||||
TenantID string `json:"tenant_id"`
|
TenantID string `json:"tenant_id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
|
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
|
||||||
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
|
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
|
||||||
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
|
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
|
||||||
|
EnableDirectUpload bool `json:"enable_direct_upload" default:"false" help:"Enable direct upload from client to OneDrive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (d *OnedriveAPP) _accessToken() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallback, resp interface{}, noRetry ...bool) ([]byte, error) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
@@ -104,7 +104,7 @@ func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallba
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.Error.Code != "" {
|
if e.Error.Code != "" {
|
||||||
if e.Error.Code == "InvalidAuthenticationToken" {
|
if e.Error.Code == "InvalidAuthenticationToken" && !utils.IsBool(noRetry...) {
|
||||||
err = d.accessToken()
|
err = d.accessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -216,9 +216,36 @@ func (d *OnedriveAPP) getDrive(ctx context.Context) (*DriveResp, error) {
|
|||||||
var resp DriveResp
|
var resp DriveResp
|
||||||
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
|
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
}, &resp)
|
}, &resp, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *OnedriveAPP) getDirectUploadInfo(ctx context.Context, path string) (*model.HttpDirectUploadInfo, error) {
|
||||||
|
// Create upload session
|
||||||
|
url := d.GetMetaUrl(false, path) + "/createUploadSession"
|
||||||
|
metadata := map[string]any{
|
||||||
|
"item": map[string]any{
|
||||||
|
"@microsoft.graph.conflictBehavior": "rename",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(metadata).SetContext(ctx)
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
|
||||||
|
if uploadUrl == "" {
|
||||||
|
return nil, fmt.Errorf("failed to get upload URL from response")
|
||||||
|
}
|
||||||
|
return &model.HttpDirectUploadInfo{
|
||||||
|
UploadURL: uploadUrl,
|
||||||
|
ChunkSize: d.ChunkSize * 1024 * 1024, // Convert MB to bytes
|
||||||
|
Method: "PUT",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,19 +110,29 @@ func (d *OpenList) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||||||
|
|
||||||
func (d *OpenList) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *OpenList) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
var resp common.Resp[FsGetResp]
|
var resp common.Resp[FsGetResp]
|
||||||
|
headers := map[string]string{
|
||||||
|
"User-Agent": base.UserAgent,
|
||||||
|
}
|
||||||
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
|
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
|
||||||
userAgent := base.UserAgent
|
|
||||||
if d.PassUAToUpsteam {
|
if d.PassUAToUpsteam {
|
||||||
userAgent = args.Header.Get("user-agent")
|
userAgent := args.Header.Get("user-agent")
|
||||||
if userAgent == "" {
|
if userAgent != "" {
|
||||||
userAgent = base.UserAgent
|
headers["User-Agent"] = userAgent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if PassIPToUpsteam is true, then pass the ip address to the upstream
|
||||||
|
if d.PassIPToUpsteam {
|
||||||
|
ip := args.IP
|
||||||
|
if ip != "" {
|
||||||
|
headers["X-Forwarded-For"] = ip
|
||||||
|
headers["X-Real-Ip"] = ip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
_, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetResult(&resp).SetBody(FsGetReq{
|
req.SetResult(&resp).SetBody(FsGetReq{
|
||||||
Path: file.GetPath(),
|
Path: file.GetPath(),
|
||||||
Password: d.MetaPassword,
|
Password: d.MetaPassword,
|
||||||
}).SetHeader("user-agent", userAgent)
|
}).SetHeaders(headers)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -355,8 +365,15 @@ func (d *OpenList) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.O
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (d *OpenList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
func (d *OpenList) ResolveLinkCacheMode(_ string) driver.LinkCacheMode {
|
||||||
// return nil, errs.NotSupport
|
var mode driver.LinkCacheMode
|
||||||
//}
|
if d.PassIPToUpsteam {
|
||||||
|
mode |= driver.LinkCacheIP
|
||||||
|
}
|
||||||
|
if d.PassUAToUpsteam {
|
||||||
|
mode |= driver.LinkCacheUA
|
||||||
|
}
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*OpenList)(nil)
|
var _ driver.Driver = (*OpenList)(nil)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Addition struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
PassIPToUpsteam bool `json:"pass_ip_to_upsteam" default:"true"`
|
||||||
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
|
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
|
||||||
ForwardArchiveReq bool `json:"forward_archive_requests" default:"true"`
|
ForwardArchiveReq bool `json:"forward_archive_requests" default:"true"`
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@ var config = driver.Config{
|
|||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
CheckStatus: true,
|
CheckStatus: true,
|
||||||
ProxyRangeOption: true,
|
ProxyRangeOption: true,
|
||||||
|
LinkCacheMode: driver.LinkCacheAuto,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ func (d *PikPak) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *PikPak) Init(ctx context.Context) (err error) {
|
func (d *PikPak) Init(ctx context.Context) (err error) {
|
||||||
|
|
||||||
if d.Common == nil {
|
if d.Common == nil {
|
||||||
d.Common = &Common{
|
d.Common = &Common{
|
||||||
client: base.NewRestyClient(),
|
client: base.NewRestyClient(),
|
||||||
@@ -247,7 +246,7 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
}
|
}
|
||||||
|
|
||||||
params := resp.Resumable.Params
|
params := resp.Resumable.Params
|
||||||
//endpoint := strings.Join(strings.Split(params.Endpoint, ".")[1:], ".")
|
// endpoint := strings.Join(strings.Split(params.Endpoint, ".")[1:], ".")
|
||||||
// web 端上传 返回的endpoint 为 `mypikpak.net` | android 端上传 返回的endpoint 为 `vip-lixian-07.mypikpak.net`·
|
// web 端上传 返回的endpoint 为 `mypikpak.net` | android 端上传 返回的endpoint 为 `vip-lixian-07.mypikpak.net`·
|
||||||
if d.Addition.Platform == "android" {
|
if d.Addition.Platform == "android" {
|
||||||
params.Endpoint = "mypikpak.net"
|
params.Endpoint = "mypikpak.net"
|
||||||
@@ -260,6 +259,27 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return d.UploadByMultipart(ctx, ¶ms, stream.GetSize(), stream, up)
|
return d.UploadByMultipart(ctx, ¶ms, stream.GetSize(), stream, up)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *PikPak) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
|
||||||
|
var about AboutResponse
|
||||||
|
_, err := d.request("https://api-drive.mypikpak.com/drive/v1/about", http.MethodGet, func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
}, &about)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
total, err := strconv.ParseUint(about.Quota.Limit, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
used, err := strconv.ParseUint(about.Quota.Usage, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.StorageDetails{
|
||||||
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// 离线下载文件
|
// 离线下载文件
|
||||||
func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
|
func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir model.Obj, fileName string) (*OfflineTask, error) {
|
||||||
requestBody := base.Json{
|
requestBody := base.Json{
|
||||||
@@ -278,7 +298,6 @@ func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir
|
|||||||
req.SetContext(ctx).
|
req.SetContext(ctx).
|
||||||
SetBody(requestBody)
|
SetBody(requestBody)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -325,7 +344,6 @@ func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []
|
|||||||
req.SetContext(ctx).
|
req.SetContext(ctx).
|
||||||
SetQueryParams(params)
|
SetQueryParams(params)
|
||||||
}, &resp)
|
}, &resp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get offline list: %w", err)
|
return nil, fmt.Errorf("failed to get offline list: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ type Media struct {
|
|||||||
|
|
||||||
type UploadTaskData struct {
|
type UploadTaskData struct {
|
||||||
UploadType string `json:"upload_type"`
|
UploadType string `json:"upload_type"`
|
||||||
//UPLOAD_TYPE_RESUMABLE
|
// UPLOAD_TYPE_RESUMABLE
|
||||||
Resumable *struct {
|
Resumable *struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Params S3Params `json:"params"`
|
Params S3Params `json:"params"`
|
||||||
@@ -195,3 +195,15 @@ type CaptchaTokenResponse struct {
|
|||||||
ExpiresIn int64 `json:"expires_in"`
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AboutResponse struct {
|
||||||
|
Quota struct {
|
||||||
|
Limit string `json:"limit"`
|
||||||
|
Usage string `json:"usage"`
|
||||||
|
UsageInTrash string `json:"usage_in_trash"`
|
||||||
|
IsUnlimited bool `json:"is_unlimited"`
|
||||||
|
Complimentary string `json:"complimentary"`
|
||||||
|
} `json:"quota"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
UserType int `json:"user_type"`
|
||||||
|
}
|
||||||
|
|||||||
288
drivers/proton_drive/driver.go
Normal file
288
drivers/proton_drive/driver.go
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
package protondrive
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package protondrive
|
||||||
|
Author: Da3zKi7<da3zki7@duck.com>
|
||||||
|
Date: 2025-09-18
|
||||||
|
|
||||||
|
Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge
|
||||||
|
|
||||||
|
The power of open-source, the force of teamwork and the magic of reverse engineering!
|
||||||
|
|
||||||
|
|
||||||
|
D@' 3z K!7 - The King Of Cracking
|
||||||
|
|
||||||
|
Да здравствует Родина))
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/setting"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
proton_api_bridge "github.com/henrybear327/Proton-API-Bridge"
|
||||||
|
"github.com/henrybear327/Proton-API-Bridge/common"
|
||||||
|
"github.com/henrybear327/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtonDrive struct {
|
||||||
|
model.Storage
|
||||||
|
Addition
|
||||||
|
|
||||||
|
protonDrive *proton_api_bridge.ProtonDrive
|
||||||
|
|
||||||
|
apiBase string
|
||||||
|
appVersion string
|
||||||
|
protonJson string
|
||||||
|
userAgent string
|
||||||
|
sdkVersion string
|
||||||
|
webDriveAV string
|
||||||
|
|
||||||
|
c *proton.Client
|
||||||
|
|
||||||
|
// userKR *crypto.KeyRing
|
||||||
|
addrKRs map[string]*crypto.KeyRing
|
||||||
|
addrData map[string]proton.Address
|
||||||
|
|
||||||
|
MainShare *proton.Share
|
||||||
|
|
||||||
|
DefaultAddrKR *crypto.KeyRing
|
||||||
|
MainShareKR *crypto.KeyRing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) GetAddition() driver.Additional {
|
||||||
|
return &d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Init(ctx context.Context) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); err == nil && r != nil {
|
||||||
|
err = fmt.Errorf("ProtonDrive initialization panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if d.Email == "" {
|
||||||
|
return fmt.Errorf("email is required")
|
||||||
|
}
|
||||||
|
if d.Password == "" {
|
||||||
|
return fmt.Errorf("password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &common.Config{
|
||||||
|
AppVersion: d.appVersion,
|
||||||
|
UserAgent: d.userAgent,
|
||||||
|
FirstLoginCredential: &common.FirstLoginCredentialData{
|
||||||
|
Username: d.Email,
|
||||||
|
Password: d.Password,
|
||||||
|
TwoFA: d.TwoFACode,
|
||||||
|
},
|
||||||
|
EnableCaching: true,
|
||||||
|
ConcurrentBlockUploadCount: setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers),
|
||||||
|
//ConcurrentFileCryptoCount: 2,
|
||||||
|
UseReusableLogin: d.UseReusableLogin && d.ReusableCredential != (common.ReusableCredentialData{}),
|
||||||
|
ReplaceExistingDraft: true,
|
||||||
|
ReusableCredential: &d.ReusableCredential,
|
||||||
|
}
|
||||||
|
|
||||||
|
protonDrive, _, err := proton_api_bridge.NewProtonDrive(
|
||||||
|
ctx,
|
||||||
|
config,
|
||||||
|
d.authHandler,
|
||||||
|
func() {},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil && config.UseReusableLogin {
|
||||||
|
config.UseReusableLogin = false
|
||||||
|
protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx,
|
||||||
|
config,
|
||||||
|
d.authHandler,
|
||||||
|
func() {},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize ProtonDrive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.initClient(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.protonDrive = protonDrive
|
||||||
|
d.MainShare = protonDrive.MainShare
|
||||||
|
if d.RootFolderID == "root" || d.RootFolderID == "" {
|
||||||
|
d.RootFolderID = protonDrive.RootLink.LinkID
|
||||||
|
}
|
||||||
|
d.MainShareKR = protonDrive.MainShareKR
|
||||||
|
d.DefaultAddrKR = protonDrive.DefaultAddrKR
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
entries, err := d.protonDrive.ListDirectory(ctx, dir.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objects := make([]model.Obj, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
obj := &model.Object{
|
||||||
|
ID: entry.Link.LinkID,
|
||||||
|
Name: entry.Name,
|
||||||
|
Size: entry.Link.Size,
|
||||||
|
Modified: time.Unix(entry.Link.ModifyTime, 0),
|
||||||
|
IsFolder: entry.IsFolder,
|
||||||
|
}
|
||||||
|
objects = append(objects, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
link, err := d.getLink(ctx, file.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed get file link: %+v", err)
|
||||||
|
}
|
||||||
|
fileSystemAttrs, err := d.protonDrive.GetActiveRevisionAttrs(ctx, link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed get file revision: %+v", err)
|
||||||
|
}
|
||||||
|
// 解密后的文件大小
|
||||||
|
size := fileSystemAttrs.Size
|
||||||
|
|
||||||
|
rangeReaderFunc := func(rangeCtx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
||||||
|
length := httpRange.Length
|
||||||
|
if length < 0 || httpRange.Start+length > size {
|
||||||
|
length = size - httpRange.Start
|
||||||
|
}
|
||||||
|
reader, _, _, err := d.protonDrive.DownloadFile(rangeCtx, link, httpRange.Start)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed start download: %+v", err)
|
||||||
|
}
|
||||||
|
return utils.ReadCloser{
|
||||||
|
Reader: io.LimitReader(reader, length),
|
||||||
|
Closer: reader,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expiration := time.Minute
|
||||||
|
return &model.Link{
|
||||||
|
RangeReader: stream.RateLimitRangeReaderFunc(rangeReaderFunc),
|
||||||
|
ContentLength: size,
|
||||||
|
Expiration: &expiration,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
|
id, err := d.protonDrive.CreateNewFolderByID(ctx, parentDir.GetID(), dirName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newDir := &model.Object{
|
||||||
|
ID: id,
|
||||||
|
Name: dirName,
|
||||||
|
IsFolder: true,
|
||||||
|
Modified: time.Now(),
|
||||||
|
}
|
||||||
|
return newDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
return d.DirectMove(ctx, srcObj, dstDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
if d.protonDrive == nil {
|
||||||
|
return nil, fmt.Errorf("protonDrive bridge is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.DirectRename(ctx, srcObj, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
if srcObj.IsDir() {
|
||||||
|
return nil, fmt.Errorf("directory copy not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
srcLink, err := d.getLink(ctx, srcObj.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, linkSize, fileSystemAttrs, err := d.protonDrive.DownloadFile(ctx, srcLink, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download source file: %w", err)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
actualSize := linkSize
|
||||||
|
if fileSystemAttrs != nil && fileSystemAttrs.Size > 0 {
|
||||||
|
actualSize = fileSystemAttrs.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
file := &stream.FileStream{
|
||||||
|
Ctx: ctx,
|
||||||
|
Obj: &model.Object{
|
||||||
|
Name: srcObj.GetName(),
|
||||||
|
// Use the accurate and real size
|
||||||
|
Size: actualSize,
|
||||||
|
Modified: srcObj.ModTime(),
|
||||||
|
},
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return d.Put(ctx, dstDir, file, func(percentage float64) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
if obj.IsDir() {
|
||||||
|
return d.protonDrive.MoveFolderToTrashByID(ctx, obj.GetID(), false)
|
||||||
|
} else {
|
||||||
|
return d.protonDrive.MoveFileToTrashByID(ctx, obj.GetID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
return d.uploadFile(ctx, dstDir.GetID(), file, up)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
|
||||||
|
about, err := d.protonDrive.About(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
total := uint64(about.MaxSpace)
|
||||||
|
free := total - uint64(about.UsedSpace)
|
||||||
|
return &model.StorageDetails{
|
||||||
|
DiskUsage: model.DiskUsage{
|
||||||
|
TotalSpace: total,
|
||||||
|
FreeSpace: free,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*ProtonDrive)(nil)
|
||||||
56
drivers/proton_drive/meta.go
Normal file
56
drivers/proton_drive/meta.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package protondrive
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package protondrive
|
||||||
|
Author: Da3zKi7<da3zki7@duck.com>
|
||||||
|
Date: 2025-09-18
|
||||||
|
|
||||||
|
Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge
|
||||||
|
|
||||||
|
The power of open-source, the force of teamwork and the magic of reverse engineering!
|
||||||
|
|
||||||
|
|
||||||
|
D@' 3z K!7 - The King Of Cracking
|
||||||
|
|
||||||
|
Да здравствует Родина))
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
"github.com/henrybear327/Proton-API-Bridge/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
driver.RootID
|
||||||
|
Email string `json:"email" required:"true" type:"string"`
|
||||||
|
Password string `json:"password" required:"true" type:"string"`
|
||||||
|
TwoFACode string `json:"two_fa_code" type:"string"`
|
||||||
|
ChunkSize int64 `json:"chunk_size" type:"number" default:"100"`
|
||||||
|
UseReusableLogin bool `json:"use_reusable_login" type:"bool" default:"true" help:"Use reusable login credentials instead of username/password"`
|
||||||
|
ReusableCredential common.ReusableCredentialData
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "ProtonDrive",
|
||||||
|
LocalSort: true,
|
||||||
|
OnlyProxy: true,
|
||||||
|
DefaultRoot: "root",
|
||||||
|
NoLinkURL: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(func() driver.Driver {
|
||||||
|
return &ProtonDrive{
|
||||||
|
Addition: Addition{
|
||||||
|
UseReusableLogin: true,
|
||||||
|
},
|
||||||
|
apiBase: "https://drive.proton.me/api",
|
||||||
|
appVersion: "windows-drive@1.11.3+rclone+proton",
|
||||||
|
protonJson: "application/vnd.protonmail.v1+json",
|
||||||
|
sdkVersion: "js@0.3.0",
|
||||||
|
userAgent: "ProtonDrive/v1.70.0 (Windows NT 10.0.22000; Win64; x64)",
|
||||||
|
webDriveAV: "web-drive@5.2.0+0f69f7a8",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
38
drivers/proton_drive/types.go
Normal file
38
drivers/proton_drive/types.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package protondrive
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package protondrive
|
||||||
|
Author: Da3zKi7<da3zki7@duck.com>
|
||||||
|
Date: 2025-09-18
|
||||||
|
|
||||||
|
Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge
|
||||||
|
|
||||||
|
The power of open-source, the force of teamwork and the magic of reverse engineering!
|
||||||
|
|
||||||
|
|
||||||
|
D@' 3z K!7 - The King Of Cracking
|
||||||
|
|
||||||
|
Да здравствует Родина))
|
||||||
|
*/
|
||||||
|
|
||||||
|
type MoveRequest struct {
|
||||||
|
ParentLinkID string `json:"ParentLinkID"`
|
||||||
|
NodePassphrase string `json:"NodePassphrase"`
|
||||||
|
NodePassphraseSignature *string `json:"NodePassphraseSignature"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
NameSignatureEmail string `json:"NameSignatureEmail"`
|
||||||
|
Hash string `json:"Hash"`
|
||||||
|
OriginalHash string `json:"OriginalHash"`
|
||||||
|
ContentHash *string `json:"ContentHash"` // Maybe null
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenameRequest struct {
|
||||||
|
Name string `json:"Name"` // PGP encrypted name
|
||||||
|
NameSignatureEmail string `json:"NameSignatureEmail"` // User's signature email
|
||||||
|
Hash string `json:"Hash"` // New name hash
|
||||||
|
OriginalHash string `json:"OriginalHash"` // Current name hash
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenameResponse struct {
|
||||||
|
Code int `json:"Code"`
|
||||||
|
}
|
||||||
670
drivers/proton_drive/util.go
Normal file
670
drivers/proton_drive/util.go
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
package protondrive
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package protondrive
|
||||||
|
Author: Da3zKi7<da3zki7@duck.com>
|
||||||
|
Date: 2025-09-18
|
||||||
|
|
||||||
|
Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge
|
||||||
|
|
||||||
|
The power of open-source, the force of teamwork and the magic of reverse engineering!
|
||||||
|
|
||||||
|
|
||||||
|
D@' 3z K!7 - The King Of Cracking
|
||||||
|
|
||||||
|
Да здравствует Родина))
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/henrybear327/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
|
_, err := d.getLink(ctx, parentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get parent link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader io.Reader
|
||||||
|
// Use buffered reader with larger buffer for better performance
|
||||||
|
var bufferSize int
|
||||||
|
|
||||||
|
// File > 100MB (default)
|
||||||
|
if file.GetSize() > d.ChunkSize*1024*1024 {
|
||||||
|
// 256KB for large files
|
||||||
|
bufferSize = 256 * 1024
|
||||||
|
// File > 10MB
|
||||||
|
} else if file.GetSize() > 10*1024*1024 {
|
||||||
|
// 128KB for medium files
|
||||||
|
bufferSize = 128 * 1024
|
||||||
|
} else {
|
||||||
|
// 64KB for small files
|
||||||
|
bufferSize = 64 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
// reader = bufio.NewReader(file)
|
||||||
|
reader = bufio.NewReaderSize(file, bufferSize)
|
||||||
|
reader = &driver.ReaderUpdatingProgress{
|
||||||
|
Reader: &stream.SimpleReaderWithSize{
|
||||||
|
Reader: reader,
|
||||||
|
Size: file.GetSize(),
|
||||||
|
},
|
||||||
|
UpdateProgress: up,
|
||||||
|
}
|
||||||
|
reader = driver.NewLimitedUploadStream(ctx, reader)
|
||||||
|
|
||||||
|
id, _, err := d.protonDrive.UploadFileByReader(ctx, parentLinkID, file.GetName(), file.ModTime(), reader, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Object{
|
||||||
|
ID: id,
|
||||||
|
Name: file.GetName(),
|
||||||
|
Size: file.GetSize(),
|
||||||
|
Modified: file.ModTime(),
|
||||||
|
IsFolder: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLinkID string) (string, error) {
|
||||||
|
parentLink, err := d.getLink(ctx, parentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent node keyring
|
||||||
|
parentNodeKR, err := d.getLinkKR(ctx, parentLink)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary file (request)
|
||||||
|
tempReq := proton.CreateFileReq{
|
||||||
|
SignatureAddress: d.MainShare.Creator,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the filename
|
||||||
|
err = tempReq.SetName(name, d.DefaultAddrKR, parentNodeKR)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to encrypt filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempReq.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) generateFileNameHash(ctx context.Context, name string, parentLinkID string) (string, error) {
|
||||||
|
parentLink, err := d.getLink(ctx, parentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent node keyring
|
||||||
|
parentNodeKR, err := d.getLinkKR(ctx, parentLink)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get signature verification keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent hash key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameHash, err := proton.GetNameHash(name, parentHashKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate name hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) getOriginalNameHash(link *proton.Link) (string, error) {
|
||||||
|
if link == nil {
|
||||||
|
return "", fmt.Errorf("link cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if link.Hash == "" {
|
||||||
|
return "", fmt.Errorf("link hash is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return link.Hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) getLink(ctx context.Context, linkID string) (*proton.Link, error) {
|
||||||
|
if linkID == "" {
|
||||||
|
return nil, fmt.Errorf("linkID cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := d.c.GetLink(ctx, d.MainShare.ShareID, linkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) getLinkKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) {
|
||||||
|
if link == nil {
|
||||||
|
return nil, fmt.Errorf("link cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root Link or Root Dir
|
||||||
|
if link.ParentLinkID == "" {
|
||||||
|
signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{link.SignatureEmail})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return link.GetKeyRing(d.MainShareKR, signatureVerificationKR)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent keyring recursively
|
||||||
|
parentLink, err := d.getLink(ctx, link.ParentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parentNodeKR, err := d.getLinkKR(ctx, parentLink)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{link.SignatureEmail})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return link.GetKeyRing(parentNodeKR, signatureVerificationKR)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrKeyPassOrSaltedKeyPassMustBeNotNil = errors.New("either keyPass or saltedKeyPass must be not nil")
|
||||||
|
ErrFailedToUnlockUserKeys = errors.New("failed to unlock user keys")
|
||||||
|
)
|
||||||
|
|
||||||
|
func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, map[string]proton.Address, []byte, error) {
|
||||||
|
user, err := c.GetUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
// fmt.Printf("user %#v", user)
|
||||||
|
|
||||||
|
addrsArr, err := c.GetAddresses(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
// fmt.Printf("addr %#v", addr)
|
||||||
|
|
||||||
|
if saltedKeyPass == nil {
|
||||||
|
if keyPass == nil {
|
||||||
|
return nil, nil, nil, nil, ErrKeyPassOrSaltedKeyPassMustBeNotNil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Due to limitations, salts are stored using cacheCredentialToFile
|
||||||
|
salts, err := c.GetSalts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
// fmt.Printf("salts %#v", salts)
|
||||||
|
|
||||||
|
saltedKeyPass, err = salts.SaltForKey(keyPass, user.Keys.Primary().ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
// fmt.Printf("saltedKeyPass ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
userKR, addrKRs, err := proton.Unlock(user, addrsArr, saltedKeyPass, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
} else if userKR.CountDecryptionEntities() == 0 {
|
||||||
|
return nil, nil, nil, nil, ErrFailedToUnlockUserKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs := make(map[string]proton.Address)
|
||||||
|
for _, addr := range addrsArr {
|
||||||
|
addrs[addr.Email] = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
return userKR, addrKRs, addrs, saltedKeyPass, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) getSignatureVerificationKeyring(emailAddresses []string, verificationAddrKRs ...*crypto.KeyRing) (*crypto.KeyRing, error) {
|
||||||
|
ret, err := crypto.NewKeyRing(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, emailAddress := range emailAddresses {
|
||||||
|
if addr, ok := d.addrData[emailAddress]; ok {
|
||||||
|
if addrKR, exists := d.addrKRs[addr.ID]; exists {
|
||||||
|
err = d.addKeysFromKR(ret, addrKR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kr := range verificationAddrKRs {
|
||||||
|
err = d.addKeysFromKR(ret, kr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret.CountEntities() == 0 {
|
||||||
|
return nil, fmt.Errorf("no keyring for signature verification")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) addKeysFromKR(kr *crypto.KeyRing, newKRs ...*crypto.KeyRing) error {
|
||||||
|
for i := range newKRs {
|
||||||
|
for _, key := range newKRs[i].GetKeys() {
|
||||||
|
err := kr.AddKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
|
// fmt.Printf("DEBUG DirectRename: path=%s, newName=%s", srcObj.GetPath(), newName)
|
||||||
|
|
||||||
|
if d.MainShare == nil || d.DefaultAddrKR == nil {
|
||||||
|
return nil, fmt.Errorf("missing required fields: MainShare=%v, DefaultAddrKR=%v",
|
||||||
|
d.MainShare != nil, d.DefaultAddrKR != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.protonDrive == nil {
|
||||||
|
return nil, fmt.Errorf("protonDrive bridge is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
srcLink, err := d.getLink(ctx, srcObj.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentLinkID := srcLink.ParentLinkID
|
||||||
|
if parentLinkID == "" {
|
||||||
|
return nil, fmt.Errorf("cannot rename root folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedName, err := d.encryptFileName(ctx, newName, parentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encrypt filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newHash, err := d.generateFileNameHash(ctx, newName, parentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate new hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalHash, err := d.getOriginalNameHash(srcLink)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get original hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renameReq := RenameRequest{
|
||||||
|
Name: encryptedName,
|
||||||
|
NameSignatureEmail: d.MainShare.Creator,
|
||||||
|
Hash: newHash,
|
||||||
|
OriginalHash: originalHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.executeRenameAPI(ctx, srcLink.LinkID, renameReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rename API call failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Object{
|
||||||
|
ID: srcLink.LinkID,
|
||||||
|
Name: newName,
|
||||||
|
Size: srcObj.GetSize(),
|
||||||
|
Modified: srcObj.ModTime(),
|
||||||
|
IsFolder: srcObj.IsDir(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) executeRenameAPI(ctx context.Context, linkID string, req RenameRequest) error {
|
||||||
|
renameURL := fmt.Sprintf(d.apiBase+"/drive/v2/volumes/%s/links/%s/rename",
|
||||||
|
d.MainShare.VolumeID, linkID)
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal rename request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "PUT", renameURL, bytes.NewReader(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Accept", d.protonJson)
|
||||||
|
httpReq.Header.Set("X-Pm-Appversion", d.webDriveAV)
|
||||||
|
httpReq.Header.Set("X-Pm-Drive-Sdk-Version", d.sdkVersion)
|
||||||
|
httpReq.Header.Set("X-Pm-Uid", d.ReusableCredential.UID)
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+d.ReusableCredential.AccessToken)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute rename request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("rename failed with status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var renameResp RenameResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&renameResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode rename response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if renameResp.Code != 1000 {
|
||||||
|
return fmt.Errorf("rename failed with code %d", renameResp.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req MoveRequest) error {
|
||||||
|
// fmt.Printf("DEBUG Move Request - Name: %s\n", req.Name)
|
||||||
|
// fmt.Printf("DEBUG Move Request - Hash: %s\n", req.Hash)
|
||||||
|
// fmt.Printf("DEBUG Move Request - OriginalHash: %s\n", req.OriginalHash)
|
||||||
|
// fmt.Printf("DEBUG Move Request - ParentLinkID: %s\n", req.ParentLinkID)
|
||||||
|
|
||||||
|
// fmt.Printf("DEBUG Move Request - Name length: %d\n", len(req.Name))
|
||||||
|
// fmt.Printf("DEBUG Move Request - NameSignatureEmail: %s\n", req.NameSignatureEmail)
|
||||||
|
// fmt.Printf("DEBUG Move Request - ContentHash: %v\n", req.ContentHash)
|
||||||
|
// fmt.Printf("DEBUG Move Request - NodePassphrase length: %d\n", len(req.NodePassphrase))
|
||||||
|
// fmt.Printf("DEBUG Move Request - NodePassphraseSignature length: %d\n", len(req.NodePassphraseSignature))
|
||||||
|
|
||||||
|
// fmt.Printf("DEBUG Move Request - SrcLinkID: %s\n", linkID)
|
||||||
|
// fmt.Printf("DEBUG Move Request - DstParentLinkID: %s\n", req.ParentLinkID)
|
||||||
|
// fmt.Printf("DEBUG Move Request - ShareID: %s\n", d.MainShare.ShareID)
|
||||||
|
|
||||||
|
srcLink, _ := d.getLink(ctx, linkID)
|
||||||
|
if srcLink != nil && srcLink.ParentLinkID == req.ParentLinkID {
|
||||||
|
return fmt.Errorf("cannot move to same parent directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
moveURL := fmt.Sprintf(d.apiBase+"/drive/v2/volumes/%s/links/%s/move",
|
||||||
|
d.MainShare.VolumeID, linkID)
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal move request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "PUT", moveURL, bytes.NewReader(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+d.ReusableCredential.AccessToken)
|
||||||
|
httpReq.Header.Set("Accept", d.protonJson)
|
||||||
|
httpReq.Header.Set("X-Pm-Appversion", d.webDriveAV)
|
||||||
|
httpReq.Header.Set("X-Pm-Drive-Sdk-Version", d.sdkVersion)
|
||||||
|
httpReq.Header.Set("X-Pm-Uid", d.ReusableCredential.UID)
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute move request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var moveResp RenameResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&moveResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode move response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if moveResp.Code != 1000 {
|
||||||
|
return fmt.Errorf("move operation failed with code: %d", moveResp.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir model.Obj) (model.Obj, error) {
|
||||||
|
// fmt.Printf("DEBUG DirectMove: srcPath=%s, dstPath=%s", srcObj.GetPath(), dstDir.GetPath())
|
||||||
|
|
||||||
|
srcLink, err := d.getLink(ctx, srcObj.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstParentLinkID := dstDir.GetID()
|
||||||
|
|
||||||
|
if srcObj.IsDir() {
|
||||||
|
// Check if destination is a descendant of source
|
||||||
|
if err := d.checkCircularMove(ctx, srcLink.LinkID, dstParentLinkID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the filename for the new location
|
||||||
|
encryptedName, err := d.encryptFileName(ctx, srcObj.GetName(), dstParentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encrypt filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newHash, err := d.generateNameHash(ctx, srcObj.GetName(), dstParentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate new hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalHash, err := d.getOriginalNameHash(srcLink)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get original hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encrypt node passphrase for new parent context
|
||||||
|
reencryptedPassphrase, err := d.reencryptNodePassphrase(ctx, srcLink, dstParentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to re-encrypt node passphrase: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveReq := MoveRequest{
|
||||||
|
ParentLinkID: dstParentLinkID,
|
||||||
|
NodePassphrase: reencryptedPassphrase,
|
||||||
|
Name: encryptedName,
|
||||||
|
NameSignatureEmail: d.MainShare.Creator,
|
||||||
|
Hash: newHash,
|
||||||
|
OriginalHash: originalHash,
|
||||||
|
ContentHash: nil,
|
||||||
|
|
||||||
|
// *** Causes rejection ***
|
||||||
|
/* NodePassphraseSignature: srcLink.NodePassphraseSignature, */
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("DEBUG MoveRequest validation:\n")
|
||||||
|
//fmt.Printf(" Name length: %d\n", len(moveReq.Name))
|
||||||
|
//fmt.Printf(" Hash: %s\n", moveReq.Hash)
|
||||||
|
//fmt.Printf(" OriginalHash: %s\n", moveReq.OriginalHash)
|
||||||
|
//fmt.Printf(" NodePassphrase length: %d\n", len(moveReq.NodePassphrase))
|
||||||
|
/* fmt.Printf(" NodePassphraseSignature length: %d\n", len(moveReq.NodePassphraseSignature)) */
|
||||||
|
//fmt.Printf(" NameSignatureEmail: %s\n", moveReq.NameSignatureEmail)
|
||||||
|
|
||||||
|
err = d.executeMoveAPI(ctx, srcLink.LinkID, moveReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("move API call failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Object{
|
||||||
|
ID: srcLink.LinkID,
|
||||||
|
Name: srcObj.GetName(),
|
||||||
|
Size: srcObj.GetSize(),
|
||||||
|
Modified: srcObj.ModTime(),
|
||||||
|
IsFolder: srcObj.IsDir(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) reencryptNodePassphrase(ctx context.Context, srcLink *proton.Link, dstParentLinkID string) (string, error) {
|
||||||
|
// Get source parent link with metadata
|
||||||
|
srcParentLink, err := d.getLink(ctx, srcLink.ParentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get source parent link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get source parent keyring using link object
|
||||||
|
srcParentKR, err := d.getLinkKR(ctx, srcParentLink)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get source parent keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get destination parent link with metadata
|
||||||
|
dstParentLink, err := d.getLink(ctx, dstParentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get destination parent link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get destination parent keyring using link object
|
||||||
|
dstParentKR, err := d.getLinkKR(ctx, dstParentLink)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get destination parent keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encrypt the node passphrase from source parent context to destination parent context
|
||||||
|
reencryptedPassphrase, err := reencryptKeyPacket(srcParentKR, dstParentKR, d.DefaultAddrKR, srcLink.NodePassphrase)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to re-encrypt key packet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reencryptedPassphrase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) generateNameHash(ctx context.Context, name string, parentLinkID string) (string, error) {
|
||||||
|
parentLink, err := d.getLink(ctx, parentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent node keyring
|
||||||
|
parentNodeKR, err := d.getLinkKR(ctx, parentLink)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get signature verification keyring
|
||||||
|
signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get signature verification keyring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get parent hash key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameHash, err := proton.GetNameHash(name, parentHashKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate name hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reencryptKeyPacket(srcKR, dstKR, _ *crypto.KeyRing, passphrase string) (string, error) { // addrKR (3)
|
||||||
|
oldSplitMessage, err := crypto.NewPGPSplitMessageFromArmored(passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionKey, err := srcKR.DecryptSessionKey(oldSplitMessage.KeyPacket)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeyPacket, err := dstKR.EncryptSessionKey(sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
newSplitMessage := crypto.NewPGPSplitMessage(newKeyPacket, oldSplitMessage.DataPacket)
|
||||||
|
|
||||||
|
return newSplitMessage.GetArmored()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) checkCircularMove(ctx context.Context, srcLinkID, dstParentLinkID string) error {
|
||||||
|
currentLinkID := dstParentLinkID
|
||||||
|
|
||||||
|
for currentLinkID != "" && currentLinkID != d.RootFolderID {
|
||||||
|
if currentLinkID == srcLinkID {
|
||||||
|
return fmt.Errorf("cannot move folder into itself or its subfolder")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLink, err := d.getLink(ctx, currentLinkID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentLinkID = currentLink.ParentLinkID
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) authHandler(auth proton.Auth) {
|
||||||
|
if auth.AccessToken != d.ReusableCredential.AccessToken || auth.RefreshToken != d.ReusableCredential.RefreshToken {
|
||||||
|
d.ReusableCredential.UID = auth.UID
|
||||||
|
d.ReusableCredential.AccessToken = auth.AccessToken
|
||||||
|
d.ReusableCredential.RefreshToken = auth.RefreshToken
|
||||||
|
|
||||||
|
if err := d.initClient(context.Background()); err != nil {
|
||||||
|
fmt.Printf("ProtonDrive: failed to reinitialize client after auth refresh: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtonDrive) initClient(ctx context.Context) error {
|
||||||
|
clientOptions := []proton.Option{
|
||||||
|
proton.WithAppVersion(d.appVersion),
|
||||||
|
proton.WithUserAgent(d.userAgent),
|
||||||
|
}
|
||||||
|
manager := proton.New(clientOptions...)
|
||||||
|
d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken)
|
||||||
|
|
||||||
|
saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode salted key pass: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addrKRs, addrs, _, err := getAccountKRs(ctx, d.c, nil, saltedKeyPassBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get account keyrings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.addrKRs = addrKRs
|
||||||
|
d.addrData = addrs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -217,11 +217,10 @@ func (d *QuarkOrUC) GetDetails(ctx context.Context) (*model.StorageDetails, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
used := memberInfo.Data.UseCapacity
|
||||||
|
total := memberInfo.Data.TotalCapacity
|
||||||
return &model.StorageDetails{
|
return &model.StorageDetails{
|
||||||
DiskUsage: model.DiskUsage{
|
DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
|
||||||
TotalSpace: memberInfo.Data.TotalCapacity,
|
|
||||||
FreeSpace: memberInfo.Data.TotalCapacity - memberInfo.Data.UseCapacity,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
|
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
|
||||||
@@ -24,9 +25,10 @@ import (
|
|||||||
type S3 struct {
|
type S3 struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
Session *session.Session
|
Session *session.Session
|
||||||
client *s3.S3
|
client *s3.S3
|
||||||
linkClient *s3.S3
|
linkClient *s3.S3
|
||||||
|
directUploadClient *s3.S3
|
||||||
|
|
||||||
config driver.Config
|
config driver.Config
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
@@ -52,16 +54,18 @@ func (d *S3) Init(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Doge init session error:", err)
|
log.Errorln("Doge init session error:", err)
|
||||||
}
|
}
|
||||||
d.client = d.getClient(false)
|
d.client = d.getClient(ClientTypeNormal)
|
||||||
d.linkClient = d.getClient(true)
|
d.linkClient = d.getClient(ClientTypeLink)
|
||||||
|
d.directUploadClient = d.getClient(ClientTypeDirectUpload)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
err := d.initSession()
|
err := d.initSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.client = d.getClient(false)
|
d.client = d.getClient(ClientTypeNormal)
|
||||||
d.linkClient = d.getClient(true)
|
d.linkClient = d.getClient(ClientTypeLink)
|
||||||
|
d.directUploadClient = d.getClient(ClientTypeDirectUpload)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,4 +214,33 @@ func (d *S3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *S3) GetDirectUploadTools() []string {
|
||||||
|
if !d.EnableDirectUpload {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{"HttpDirect"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *S3) GetDirectUploadInfo(ctx context.Context, _ string, dstDir model.Obj, fileName string, _ int64) (any, error) {
|
||||||
|
if !d.EnableDirectUpload {
|
||||||
|
return nil, errs.NotImplement
|
||||||
|
}
|
||||||
|
path := getKey(stdpath.Join(dstDir.GetPath(), fileName), false)
|
||||||
|
req, _ := d.directUploadClient.PutObjectRequest(&s3.PutObjectInput{
|
||||||
|
Bucket: &d.Bucket,
|
||||||
|
Key: &path,
|
||||||
|
})
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("failed to create PutObject request")
|
||||||
|
}
|
||||||
|
link, err := req.Presign(time.Hour * time.Duration(d.SignURLExpire))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.HttpDirectUploadInfo{
|
||||||
|
UploadURL: link,
|
||||||
|
Method: "PUT",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ driver.Driver = (*S3)(nil)
|
var _ driver.Driver = (*S3)(nil)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type Addition struct {
|
|||||||
ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"`
|
ListObjectVersion string `json:"list_object_version" type:"select" options:"v1,v2" default:"v1"`
|
||||||
RemoveBucket bool `json:"remove_bucket" help:"Remove bucket name from path when using custom host."`
|
RemoveBucket bool `json:"remove_bucket" help:"Remove bucket name from path when using custom host."`
|
||||||
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
|
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
|
||||||
|
EnableDirectUpload bool `json:"enable_direct_upload" default:"false"`
|
||||||
|
DirectUploadHost string `json:"direct_upload_host" required:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -41,9 +41,15 @@ func (d *S3) initSession() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *S3) getClient(link bool) *s3.S3 {
|
const (
|
||||||
|
ClientTypeNormal = iota
|
||||||
|
ClientTypeLink
|
||||||
|
ClientTypeDirectUpload
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *S3) getClient(clientType int) *s3.S3 {
|
||||||
client := s3.New(d.Session)
|
client := s3.New(d.Session)
|
||||||
if link && d.CustomHost != "" {
|
if clientType == ClientTypeLink && d.CustomHost != "" {
|
||||||
client.Handlers.Build.PushBack(func(r *request.Request) {
|
client.Handlers.Build.PushBack(func(r *request.Request) {
|
||||||
if r.HTTPRequest.Method != http.MethodGet {
|
if r.HTTPRequest.Method != http.MethodGet {
|
||||||
return
|
return
|
||||||
@@ -58,6 +64,20 @@ func (d *S3) getClient(link bool) *s3.S3 {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if clientType == ClientTypeDirectUpload && d.DirectUploadHost != "" {
|
||||||
|
client.Handlers.Build.PushBack(func(r *request.Request) {
|
||||||
|
if r.HTTPRequest.Method != http.MethodPut {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
split := strings.SplitN(d.DirectUploadHost, "://", 2)
|
||||||
|
if utils.SliceContains([]string{"http", "https"}, split[0]) {
|
||||||
|
r.HTTPRequest.URL.Scheme = split[0]
|
||||||
|
r.HTTPRequest.URL.Host = split[1]
|
||||||
|
} else {
|
||||||
|
r.HTTPRequest.URL.Host = d.DirectUploadHost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,15 +69,10 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
|
|||||||
Limiter: stream.ServerDownloadLimit,
|
Limiter: stream.ServerDownloadLimit,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
}
|
}
|
||||||
if !d.Config().OnlyLinkMFile {
|
|
||||||
return &model.Link{
|
|
||||||
RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
|
|
||||||
SyncClosers: utils.NewSyncClosers(remoteFile),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
MFile: mFile,
|
RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
|
||||||
SyncClosers: utils.NewSyncClosers(remoteFile),
|
SyncClosers: utils.NewSyncClosers(remoteFile),
|
||||||
|
RequireReference: true,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "SFTP",
|
Name: "SFTP",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
OnlyLinkMFile: false,
|
OnlyProxy: true,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
CheckStatus: true,
|
CheckStatus: true,
|
||||||
NoLinkURL: true,
|
NoLinkURL: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -86,15 +86,10 @@ func (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
|
|||||||
Limiter: stream.ServerDownloadLimit,
|
Limiter: stream.ServerDownloadLimit,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
}
|
}
|
||||||
if !d.Config().OnlyLinkMFile {
|
|
||||||
return &model.Link{
|
|
||||||
RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
|
|
||||||
SyncClosers: utils.NewSyncClosers(remoteFile),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
MFile: mFile,
|
RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
|
||||||
SyncClosers: utils.NewSyncClosers(remoteFile),
|
SyncClosers: utils.NewSyncClosers(remoteFile),
|
||||||
|
RequireReference: true,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "SMB",
|
Name: "SMB",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
OnlyLinkMFile: false,
|
OnlyProxy: true,
|
||||||
DefaultRoot: ".",
|
DefaultRoot: ".",
|
||||||
NoCache: true,
|
NoCache: true,
|
||||||
NoLinkURL: true,
|
NoLinkURL: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/fs"
|
"github.com/OpenListTeam/OpenList/v4/internal/fs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/sign"
|
"github.com/OpenListTeam/OpenList/v4/internal/sign"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/OpenListTeam/OpenList/v4/server/common"
|
"github.com/OpenListTeam/OpenList/v4/server/common"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Strm struct {
|
type Strm struct {
|
||||||
@@ -39,6 +41,9 @@ func (d *Strm) Init(ctx context.Context) error {
|
|||||||
if d.Paths == "" {
|
if d.Paths == "" {
|
||||||
return errors.New("paths is required")
|
return errors.New("paths is required")
|
||||||
}
|
}
|
||||||
|
if d.SaveStrmToLocal && len(d.SaveStrmLocalPath) <= 0 {
|
||||||
|
return errors.New("SaveStrmLocalPath is required")
|
||||||
|
}
|
||||||
d.pathMap = make(map[string][]string)
|
d.pathMap = make(map[string][]string)
|
||||||
for _, path := range strings.Split(d.Paths, "\n") {
|
for _, path := range strings.Split(d.Paths, "\n") {
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
@@ -47,6 +52,13 @@ func (d *Strm) Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
k, v := getPair(path)
|
k, v := getPair(path)
|
||||||
d.pathMap[k] = append(d.pathMap[k], v)
|
d.pathMap[k] = append(d.pathMap[k], v)
|
||||||
|
if d.SaveStrmToLocal {
|
||||||
|
err := InsertStrm(utils.FixAndCleanPath(strings.TrimSpace(path)), d)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("insert strmTrie error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(d.pathMap) == 1 {
|
if len(d.pathMap) == 1 {
|
||||||
for k := range d.pathMap {
|
for k := range d.pathMap {
|
||||||
@@ -58,26 +70,52 @@ func (d *Strm) Init(ctx context.Context) error {
|
|||||||
d.autoFlatten = false
|
d.autoFlatten = false
|
||||||
}
|
}
|
||||||
|
|
||||||
d.supportSuffix = supportSuffix()
|
var supportTypes []string
|
||||||
if d.FilterFileTypes != "" {
|
if d.FilterFileTypes == "" {
|
||||||
types := strings.Split(d.FilterFileTypes, ",")
|
d.FilterFileTypes = "mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac"
|
||||||
for _, ext := range types {
|
}
|
||||||
ext = strings.ToLower(strings.TrimSpace(ext))
|
supportTypes = strings.Split(d.FilterFileTypes, ",")
|
||||||
if ext != "" {
|
d.supportSuffix = map[string]struct{}{}
|
||||||
d.supportSuffix[ext] = struct{}{}
|
for _, ext := range supportTypes {
|
||||||
}
|
ext = strings.ToLower(strings.TrimSpace(ext))
|
||||||
|
if ext != "" {
|
||||||
|
d.supportSuffix[ext] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d.downloadSuffix = downloadSuffix()
|
var downloadTypes []string
|
||||||
if d.DownloadFileTypes != "" {
|
if d.DownloadFileTypes == "" {
|
||||||
downloadTypes := strings.Split(d.DownloadFileTypes, ",")
|
d.DownloadFileTypes = "ass,srt,vtt,sub,strm"
|
||||||
for _, ext := range downloadTypes {
|
}
|
||||||
ext = strings.ToLower(strings.TrimSpace(ext))
|
downloadTypes = strings.Split(d.DownloadFileTypes, ",")
|
||||||
if ext != "" {
|
d.downloadSuffix = map[string]struct{}{}
|
||||||
d.downloadSuffix[ext] = struct{}{}
|
for _, ext := range downloadTypes {
|
||||||
|
ext = strings.ToLower(strings.TrimSpace(ext))
|
||||||
|
if ext != "" {
|
||||||
|
d.downloadSuffix[ext] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Version != 5 {
|
||||||
|
types := strings.Split("mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac", ",")
|
||||||
|
for _, ext := range types {
|
||||||
|
if _, ok := d.supportSuffix[ext]; !ok {
|
||||||
|
d.supportSuffix[ext] = struct{}{}
|
||||||
|
supportTypes = append(supportTypes, ext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
d.FilterFileTypes = strings.Join(supportTypes, ",")
|
||||||
|
|
||||||
|
types = strings.Split("ass,srt,vtt,sub,strm", ",")
|
||||||
|
for _, ext := range types {
|
||||||
|
if _, ok := d.downloadSuffix[ext]; !ok {
|
||||||
|
d.downloadSuffix[ext] = struct{}{}
|
||||||
|
downloadTypes = append(downloadTypes, ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.DownloadFileTypes = strings.Join(downloadTypes, ",")
|
||||||
|
d.PathPrefix = "/d"
|
||||||
|
d.Version = 5
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -86,6 +124,9 @@ func (d *Strm) Drop(ctx context.Context) error {
|
|||||||
d.pathMap = nil
|
d.pathMap = nil
|
||||||
d.downloadSuffix = nil
|
d.downloadSuffix = nil
|
||||||
d.supportSuffix = nil
|
d.supportSuffix = nil
|
||||||
|
for _, path := range strings.Split(d.Paths, "\n") {
|
||||||
|
RemoveStrm(utils.FixAndCleanPath(strings.TrimSpace(path)), d)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +197,7 @@ func (d *Strm) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
|
|||||||
if file.GetID() == "strm" {
|
if file.GetID() == "strm" {
|
||||||
link := d.getLink(ctx, file.GetPath())
|
link := d.getLink(ctx, file.GetPath())
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
MFile: strings.NewReader(link),
|
RangeReader: stream.GetRangeReaderFromMFile(int64(len(link)), strings.NewReader(link)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
// ftp,s3
|
// ftp,s3
|
||||||
|
|||||||
175
drivers/strm/hook.go
Normal file
175
drivers/strm/hook.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package strm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
stdpath "path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/tchap/go-patricia/v2/patricia"
|
||||||
|
)
|
||||||
|
|
||||||
|
var strmTrie = patricia.NewTrie()
|
||||||
|
|
||||||
|
func UpdateLocalStrm(ctx context.Context, path string, objs []model.Obj) {
|
||||||
|
path = utils.FixAndCleanPath(path)
|
||||||
|
updateLocal := func(driver *Strm, basePath string, objs []model.Obj) {
|
||||||
|
relParent := strings.TrimPrefix(basePath, driver.MountPath)
|
||||||
|
localParentPath := stdpath.Join(driver.SaveStrmLocalPath, relParent)
|
||||||
|
for _, obj := range objs {
|
||||||
|
localPath := stdpath.Join(localParentPath, obj.GetName())
|
||||||
|
generateStrm(ctx, driver, obj, localPath)
|
||||||
|
}
|
||||||
|
deleteExtraFiles(localParentPath, objs)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = strmTrie.VisitPrefixes(patricia.Prefix(path), func(needPathPrefix patricia.Prefix, item patricia.Item) error {
|
||||||
|
strmDrivers := item.([]*Strm)
|
||||||
|
needPath := string(needPathPrefix)
|
||||||
|
restPath := strings.TrimPrefix(path, needPath)
|
||||||
|
if len(restPath) > 0 && restPath[0] != '/' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, strmDriver := range strmDrivers {
|
||||||
|
strmObjs := strmDriver.convert2strmObjs(ctx, path, objs)
|
||||||
|
updateLocal(strmDriver, stdpath.Join(stdpath.Base(needPath), restPath), strmObjs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertStrm(dstPath string, d *Strm) error {
|
||||||
|
prefix := patricia.Prefix(strings.TrimRight(dstPath, "/"))
|
||||||
|
existing := strmTrie.Get(prefix)
|
||||||
|
|
||||||
|
if existing == nil {
|
||||||
|
if !strmTrie.Insert(prefix, []*Strm{d}) {
|
||||||
|
return errors.New("failed to insert strm")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if lst, ok := existing.([]*Strm); ok {
|
||||||
|
strmTrie.Set(prefix, append(lst, d))
|
||||||
|
} else {
|
||||||
|
return errors.New("invalid trie item type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveStrm(dstPath string, d *Strm) {
|
||||||
|
prefix := patricia.Prefix(strings.TrimRight(dstPath, "/"))
|
||||||
|
existing := strmTrie.Get(prefix)
|
||||||
|
if existing == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lst, ok := existing.([]*Strm)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(lst) == 1 && lst[0] == d {
|
||||||
|
strmTrie.Delete(prefix)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, di := range lst {
|
||||||
|
if di == d {
|
||||||
|
newList := append(lst[:i], lst[i+1:]...)
|
||||||
|
strmTrie.Set(prefix, newList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateStrm(ctx context.Context, driver *Strm, obj model.Obj, localPath string) {
|
||||||
|
if obj.IsDir() {
|
||||||
|
err := utils.CreateNestedDirectory(localPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to generate strm dir %s: failed to create dir: %v", localPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
link, err := driver.Link(ctx, obj, model.LinkArgs{})
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to generate strm of obj %s: failed to link: %v", localPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer link.Close()
|
||||||
|
size := link.ContentLength
|
||||||
|
if size <= 0 {
|
||||||
|
size = obj.GetSize()
|
||||||
|
}
|
||||||
|
rrf, err := stream.GetRangeReaderFromLink(size, link)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to generate strm of obj %s: failed to get range reader: %v", localPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rc, err := rrf.RangeRead(ctx, http_range.Range{Length: -1})
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to generate strm of obj %s: failed to read range: %v", localPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
file, err := utils.CreateNestedFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to generate strm of obj %s: failed to create local file: %v", localPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
if _, err := utils.CopyWithBuffer(file, rc); err != nil {
|
||||||
|
log.Warnf("failed to generate strm of obj %s: copy failed: %v", localPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteExtraFiles(localPath string, objs []model.Obj) {
|
||||||
|
localFiles, err := getLocalFiles(localPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to read local files from %s: %v", localPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
objsSet := make(map[string]struct{})
|
||||||
|
for _, obj := range objs {
|
||||||
|
if obj.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
objsSet[stdpath.Join(localPath, obj.GetName())] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, localFile := range localFiles {
|
||||||
|
if _, exists := objsSet[localFile]; !exists {
|
||||||
|
err := os.Remove(localFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to delete file: %s, error: %v\n", localFile, err)
|
||||||
|
} else {
|
||||||
|
log.Infof("Deleted file %s", localFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocalFiles(localPath string) ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
entries, err := os.ReadDir(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
files = append(files, stdpath.Join(localPath, entry.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterObjsUpdateHook(UpdateLocalStrm)
|
||||||
|
}
|
||||||
@@ -8,21 +8,24 @@ import (
|
|||||||
type Addition struct {
|
type Addition struct {
|
||||||
Paths string `json:"paths" required:"true" type:"text"`
|
Paths string `json:"paths" required:"true" type:"text"`
|
||||||
SiteUrl string `json:"siteUrl" type:"text" required:"false" help:"The prefix URL of the strm file"`
|
SiteUrl string `json:"siteUrl" type:"text" required:"false" help:"The prefix URL of the strm file"`
|
||||||
FilterFileTypes string `json:"filterFileTypes" type:"text" default:"strm" required:"false" help:"Supports suffix name of strm file"`
|
PathPrefix string `json:"PathPrefix" type:"text" required:"false" default:"/d" help:"Path prefix"`
|
||||||
DownloadFileTypes string `json:"downloadFileTypes" type:"text" default:"ass" required:"false" help:"Files need to download with strm (usally subtitles)"`
|
DownloadFileTypes string `json:"downloadFileTypes" type:"text" default:"ass,srt,vtt,sub,strm" required:"false" help:"Files need to download with strm (usally subtitles)"`
|
||||||
|
FilterFileTypes string `json:"filterFileTypes" type:"text" default:"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac" required:"false" help:"Supports suffix name of strm file"`
|
||||||
EncodePath bool `json:"encodePath" default:"true" required:"true" help:"encode the path in the strm file"`
|
EncodePath bool `json:"encodePath" default:"true" required:"true" help:"encode the path in the strm file"`
|
||||||
LocalModel bool `json:"localModel" default:"false" help:"enable local mode"`
|
WithoutUrl bool `json:"withoutUrl" default:"false" help:"strm file content without URL prefix"`
|
||||||
|
SaveStrmToLocal bool `json:"SaveStrmToLocal" default:"false" help:"save strm file locally"`
|
||||||
|
SaveStrmLocalPath string `json:"SaveStrmLocalPath" type:"text" help:"save strm file local path"`
|
||||||
|
Version int
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "Strm",
|
Name: "Strm",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
NoCache: true,
|
OnlyProxy: true,
|
||||||
NoUpload: true,
|
NoCache: true,
|
||||||
DefaultRoot: "/",
|
NoUpload: true,
|
||||||
OnlyLinkMFile: true,
|
DefaultRoot: "/",
|
||||||
OnlyProxy: true,
|
NoLinkURL: true,
|
||||||
NoLinkURL: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package strm
|
|
||||||
|
|
||||||
func supportSuffix() map[string]struct{} {
|
|
||||||
return map[string]struct{}{
|
|
||||||
// video
|
|
||||||
"mp4": {},
|
|
||||||
"mkv": {},
|
|
||||||
"flv": {},
|
|
||||||
"avi": {},
|
|
||||||
"wmv": {},
|
|
||||||
"ts": {},
|
|
||||||
"rmvb": {},
|
|
||||||
"webm": {},
|
|
||||||
// audio
|
|
||||||
"mp3": {},
|
|
||||||
"flac": {},
|
|
||||||
"aac": {},
|
|
||||||
"wav": {},
|
|
||||||
"ogg": {},
|
|
||||||
"m4a": {},
|
|
||||||
"wma": {},
|
|
||||||
"alac": {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadSuffix() map[string]struct{} {
|
|
||||||
return map[string]struct{}{
|
|
||||||
// strm
|
|
||||||
"strm": {},
|
|
||||||
// subtitles
|
|
||||||
"ass": {},
|
|
||||||
"srt": {},
|
|
||||||
"vtt": {},
|
|
||||||
"sub": {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user