diff --git a/cmd/flags/config.go b/cmd/flags/config.go
index f74e2cb4..0409731e 100644
--- a/cmd/flags/config.go
+++ b/cmd/flags/config.go
@@ -2,6 +2,7 @@ package flags
var (
DataDir string
+ ConfigPath string
Debug bool
NoPrefix bool
Dev bool
diff --git a/cmd/root.go b/cmd/root.go
index 86e45a35..42121aac 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -27,7 +27,8 @@ func Execute() {
}
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.NoPrefix, "no-prefix", false, "disable env prefix")
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
diff --git a/cmd/server.go b/cmd/server.go
index 3758009f..9f45161d 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -27,6 +27,8 @@ import (
"github.com/spf13/cobra"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
+
+ "github.com/quic-go/quic-go/http3"
)
// ServerCmd represents the server command
@@ -63,6 +65,7 @@ the address is defined in config file`,
httpHandler = h2c.NewHandler(r, &http2.Server{})
}
var httpSrv, httpsSrv, unixSrv *http.Server
+ var quicSrv *http3.Server
if conf.Conf.Scheme.HttpPort != -1 {
httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
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())
}
}()
+ 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 != "" {
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)
}
}()
+ 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 != "" {
wg.Add(1)
diff --git a/drivers/115/driver.go b/drivers/115/driver.go
index a9a9d520..bae8bf13 100644
--- a/drivers/115/driver.go
+++ b/drivers/115/driver.go
@@ -266,4 +266,17 @@ func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, delete
return d.client.DeleteOfflineTasks(hashes, deleteFiles)
}
+func (d *Pan115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ info, err := d.client.GetInfo()
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: uint64(info.SpaceInfo.AllTotal.Size),
+ FreeSpace: uint64(info.SpaceInfo.AllRemain.Size),
+ },
+ }, nil
+}
+
var _ driver.Driver = (*Pan115)(nil)
diff --git a/drivers/115/meta.go b/drivers/115/meta.go
index c14dc0b8..7ea573e4 100644
--- a/drivers/115/meta.go
+++ b/drivers/115/meta.go
@@ -19,10 +19,9 @@ type Addition struct {
}
var config = driver.Config{
- Name: "115 Cloud",
- DefaultRoot: "0",
- // OnlyProxy: true,
- // NoOverwriteUpload: true,
+ Name: "115 Cloud",
+ DefaultRoot: "0",
+ LinkCacheMode: driver.LinkCacheUA,
}
func init() {
diff --git a/drivers/115_open/driver.go b/drivers/115_open/driver.go
index 0f5b44e1..88981ffa 100644
--- a/drivers/115_open/driver.go
+++ b/drivers/115_open/driver.go
@@ -344,6 +344,27 @@ func (d *Open115) OfflineList(ctx context.Context) (*sdk.OfflineTaskListResp, er
return resp, nil
}
+func (d *Open115) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ userInfo, err := d.client.UserInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ total, err := userInfo.RtSpaceInfo.AllTotal.Size.Int64()
+ if err != nil {
+ return nil, err
+ }
+ free, err := userInfo.RtSpaceInfo.AllRemain.Size.Int64()
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: uint64(total),
+ FreeSpace: uint64(free),
+ },
+ }, nil
+}
+
// func (d *Open115) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
// // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
// return nil, errs.NotImplement
diff --git a/drivers/115_open/meta.go b/drivers/115_open/meta.go
index c8777132..34722d91 100644
--- a/drivers/115_open/meta.go
+++ b/drivers/115_open/meta.go
@@ -20,8 +20,9 @@ type Addition struct {
}
var config = driver.Config{
- Name: "115 Open",
- DefaultRoot: "0",
+ Name: "115 Open",
+ DefaultRoot: "0",
+ LinkCacheMode: driver.LinkCacheUA,
}
func init() {
diff --git a/drivers/123/driver.go b/drivers/123/driver.go
index 1f58e7d4..9de6c5a2 100644
--- a/drivers/123/driver.go
+++ b/drivers/123/driver.go
@@ -288,4 +288,15 @@ func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
return limiter.Wait(ctx)
}
+func (d *Pan123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ userInfo, err := d.getUserInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ total := userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp
+ return &model.StorageDetails{
+ DiskUsage: driver.DiskUsageFromUsedAndTotal(userInfo.Data.SpaceUsed, total),
+ }, nil
+}
+
var _ driver.Driver = (*Pan123)(nil)
diff --git a/drivers/123/types.go b/drivers/123/types.go
index 17510d94..587eaf71 100644
--- a/drivers/123/types.go
+++ b/drivers/123/types.go
@@ -28,7 +28,7 @@ func (f File) CreateTime() time.Time {
}
func (f File) GetHash() utils.HashInfo {
- return utils.HashInfo{}
+ return utils.NewHashInfo(utils.MD5, f.Etag)
}
func (f File) GetPath() string {
@@ -140,3 +140,14 @@ type QrCodeResultResp struct {
ScanPlatform int `json:"scanPlatform"`
} `json:"data"`
}
+
+type UserInfoResp struct {
+ Data struct {
+ Uid int64 `json:"UID"`
+ Nickname string `json:"Nickname"`
+ SpaceUsed uint64 `json:"SpaceUsed"`
+ SpacePermanent uint64 `json:"SpacePermanent"`
+ SpaceTemp uint64 `json:"SpaceTemp"`
+ FileCount int `json:"FileCount"`
+ } `json:"data"`
+}
diff --git a/drivers/123/upload.go b/drivers/123/upload.go
index eb05e094..a4ae988d 100644
--- a/drivers/123/upload.go
+++ b/drivers/123/upload.go
@@ -124,7 +124,7 @@ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.Fi
if cur == chunkCount {
curSize = lastChunkSize
}
- var reader *stream.SectionReader
+ var reader io.ReadSeeker
var rateLimitedRd io.Reader
threadG.GoWithLifecycle(errgroup.Lifecycle{
Before: func(ctx context.Context) error {
diff --git a/drivers/123/util.go b/drivers/123/util.go
index 019027e0..bbaf78b0 100644
--- a/drivers/123/util.go
+++ b/drivers/123/util.go
@@ -432,3 +432,14 @@ func generateAuthKey() string {
uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "") // 去掉 UUID 中的所有 -
return fmt.Sprintf("%d-%09d-%s", timestamp, randomInt, uuidStr) // 确保随机整数是9位
}
+
+func (d *Pan123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {
+ var resp UserInfoResp
+ _, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/123_open/driver.go b/drivers/123_open/driver.go
index 04785ac1..ac75e51d 100644
--- a/drivers/123_open/driver.go
+++ b/drivers/123_open/driver.go
@@ -17,6 +17,7 @@ import (
type Open123 struct {
model.Storage
Addition
+ UID uint64
}
func (d *Open123) Config() driver.Config {
@@ -83,7 +84,7 @@ func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
}, nil
}
- u, err := d.getUserInfo()
+ uid, err := d.getUID(ctx)
if err != nil {
return nil, err
}
@@ -91,7 +92,7 @@ func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
duration := time.Duration(d.DirectLinkValidDuration) * time.Minute
newURL, err := d.SignURL(res.Data.URL, d.DirectLinkPrivateKey,
- u.Data.UID, duration)
+ uid, duration)
if err != nil {
return nil, err
}
@@ -213,5 +214,30 @@ func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStre
return nil, fmt.Errorf("upload complete timeout")
}
-var _ driver.Driver = (*Open123)(nil)
-var _ driver.PutResult = (*Open123)(nil)
+func (d *Open123) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ userInfo, err := d.getUserInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ total := userInfo.Data.SpacePermanent + userInfo.Data.SpaceTemp
+ free := total - userInfo.Data.SpaceUsed
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: free,
+ },
+ }, 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 (
+ _ driver.Driver = (*Open123)(nil)
+ _ driver.PutResult = (*Open123)(nil)
+)
diff --git a/drivers/123_open/meta.go b/drivers/123_open/meta.go
index effc7ecf..db4ccc18 100644
--- a/drivers/123_open/meta.go
+++ b/drivers/123_open/meta.go
@@ -24,7 +24,7 @@ type Addition struct {
UploadThread int `json:"UploadThread" type:"number" default:"3" help:"the threads of upload"`
// 使用直链
- DirectLink bool `json:"DirectLink" type:"boolean" default:"false" required:"false" help:"use direct link when download file"`
+ DirectLink bool `json:"DirectLink" type:"bool" default:"false" required:"false" help:"use direct link when download file"`
DirectLinkPrivateKey string `json:"DirectLinkPrivateKey" required:"false" help:"private key for direct link, if URL authentication is enabled"`
DirectLinkValidDuration int64 `json:"DirectLinkValidDuration" type:"number" default:"30" required:"false" help:"minutes, if URL authentication is enabled"`
diff --git a/drivers/123_open/types.go b/drivers/123_open/types.go
index eb08529f..dd51cb2a 100644
--- a/drivers/123_open/types.go
+++ b/drivers/123_open/types.go
@@ -19,6 +19,7 @@ func (a *ApiInfo) Require() {
a.token <- struct{}{}
}
}
+
func (a *ApiInfo) Release() {
if a.qps > 0 {
time.AfterFunc(time.Second, func() {
@@ -26,13 +27,16 @@ func (a *ApiInfo) Release() {
})
}
}
+
func (a *ApiInfo) SetQPS(qps int) {
a.qps = qps
a.token = make(chan struct{}, qps)
}
+
func (a *ApiInfo) NowLen() int {
return len(a.token)
}
+
func InitApiInfo(url string, qps int) *ApiInfo {
return &ApiInfo{
url: url,
@@ -127,19 +131,19 @@ type RefreshTokenResp struct {
type UserInfoResp struct {
BaseResp
Data struct {
- UID uint64 `json:"uid"`
- Username string `json:"username"`
- DisplayName string `json:"displayName"`
- HeadImage string `json:"headImage"`
- Passport string `json:"passport"`
- Mail string `json:"mail"`
- SpaceUsed int64 `json:"spaceUsed"`
- SpacePermanent int64 `json:"spacePermanent"`
- SpaceTemp int64 `json:"spaceTemp"`
- SpaceTempExpr string `json:"spaceTempExpr"`
- Vip bool `json:"vip"`
- DirectTraffic int64 `json:"directTraffic"`
- IsHideUID bool `json:"isHideUID"`
+ UID uint64 `json:"uid"`
+ // Username string `json:"username"`
+ // DisplayName string `json:"displayName"`
+ // HeadImage string `json:"headImage"`
+ // Passport string `json:"passport"`
+ // Mail string `json:"mail"`
+ SpaceUsed uint64 `json:"spaceUsed"`
+ SpacePermanent uint64 `json:"spacePermanent"`
+ SpaceTemp uint64 `json:"spaceTemp"`
+ // SpaceTempExpr int64 `json:"spaceTempExpr"`
+ // Vip bool `json:"vip"`
+ // DirectTraffic int64 `json:"directTraffic"`
+ // IsHideUID bool `json:"isHideUID"`
} `json:"data"`
}
@@ -185,3 +189,18 @@ type UploadCompleteResp struct {
FileID int64 `json:"fileID"`
} `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"`
+}
diff --git a/drivers/123_open/upload.go b/drivers/123_open/upload.go
index abcde2aa..73395fcf 100644
--- a/drivers/123_open/upload.go
+++ b/drivers/123_open/upload.go
@@ -67,7 +67,7 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
partNumber := partIndex + 1 // 分片号从1开始
offset := partIndex * chunkSize
size := min(chunkSize, size-offset)
- var reader *stream.SectionReader
+ var reader io.ReadSeeker
var rateLimitedRd io.Reader
sliceMD5 := ""
// 表单
diff --git a/drivers/123_open/util.go b/drivers/123_open/util.go
index 52bb5ee8..b09d9eb8 100644
--- a/drivers/123_open/util.go
+++ b/drivers/123_open/util.go
@@ -1,6 +1,7 @@
package _123_open
import (
+ "context"
"crypto/md5"
"encoding/json"
"errors"
@@ -18,7 +19,7 @@ import (
log "github.com/sirupsen/logrus"
)
-var ( //不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
+var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
Api = "https://open-api.123pan.com"
AccessToken = InitApiInfo(Api+"/api/v1/access_token", 1)
@@ -33,6 +34,9 @@ var ( //不同情况下获取的AccessTokenQPS限制不同 如下模块化易于
Trash = InitApiInfo(Api+"/api/v1/file/trash", 2)
UploadCreate = InitApiInfo(Api+"/upload/v2/file/create", 2)
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) {
@@ -82,12 +86,27 @@ func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCall
return nil, errors.New(baseResp.Message)
}
}
-
}
func (d *Open123) flushAccessToken() error {
- if d.Addition.ClientID != "" {
- if d.Addition.ClientSecret != "" {
+ if d.ClientID != "" {
+ if d.RefreshToken != "" {
+ var resp RefreshTokenResp
+ _, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
+ req.SetQueryParam("client_id", d.ClientID)
+ if d.ClientSecret != "" {
+ req.SetQueryParam("client_secret", d.ClientSecret)
+ }
+ req.SetQueryParam("grant_type", "refresh_token")
+ req.SetQueryParam("refresh_token", d.RefreshToken)
+ }, &resp)
+ if err != nil {
+ return err
+ }
+ d.AccessToken = resp.AccessToken
+ d.RefreshToken = resp.RefreshToken
+ op.MustSaveDriverStorage(d)
+ } else if d.ClientSecret != "" {
var resp AccessTokenResp
_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
@@ -100,19 +119,6 @@ func (d *Open123) flushAccessToken() error {
}
d.AccessToken = resp.Data.AccessToken
op.MustSaveDriverStorage(d)
- } else if d.Addition.RefreshToken != "" {
- var resp RefreshTokenResp
- _, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
- req.SetQueryParam("client_id", d.ClientID)
- req.SetQueryParam("grant_type", "refresh_token")
- req.SetQueryParam("refresh_token", d.Addition.RefreshToken)
- }, &resp)
- if err != nil {
- return err
- }
- d.AccessToken = resp.AccessToken
- d.RefreshToken = resp.RefreshToken
- op.MustSaveDriverStorage(d)
}
}
return nil
@@ -145,16 +151,30 @@ func (d *Open123) SignURL(originURL, privateKey string, uid uint64, validDuratio
return objURL.String(), nil
}
-func (d *Open123) getUserInfo() (*UserInfoResp, error) {
+func (d *Open123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {
var resp UserInfoResp
- if _, err := d.Request(UserInfo, http.MethodGet, nil, &resp); err != nil {
+ if _, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp); err != nil {
return nil, err
}
return &resp, nil
}
+func (d *Open123) getUID(ctx context.Context) (uint64, error) {
+ if d.UID != 0 {
+ return d.UID, nil
+ }
+ resp, err := d.getUserInfo(ctx)
+ if err != nil {
+ return 0, err
+ }
+ d.UID = resp.Data.UID
+ return resp.Data.UID, nil
+}
+
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
var resp FileListResp
@@ -169,7 +189,6 @@ func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*Fi
"searchData": "",
})
}, &resp)
-
if err != nil {
return nil, err
}
@@ -197,7 +216,7 @@ func (d *Open123) getDirectLink(fileId int64) (*DirectLinkResp, error) {
_, err := d.Request(DirectLink, http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
- "fileId": strconv.FormatInt(fileId, 10),
+ "fileID": strconv.FormatInt(fileId, 10),
})
}, &resp)
if err != nil {
@@ -261,3 +280,34 @@ func (d *Open123) trash(fileId int64) error {
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
+}
diff --git a/drivers/123_share/types.go b/drivers/123_share/types.go
index 6062e846..3919c5fa 100644
--- a/drivers/123_share/types.go
+++ b/drivers/123_share/types.go
@@ -24,7 +24,7 @@ type File struct {
}
func (f File) GetHash() utils.HashInfo {
- return utils.HashInfo{}
+ return utils.NewHashInfo(utils.MD5, f.Etag)
}
func (f File) GetPath() string {
diff --git a/drivers/139/driver.go b/drivers/139/driver.go
index 4fbe1c0a..c95d7fc0 100644
--- a/drivers/139/driver.go
+++ b/drivers/139/driver.go
@@ -54,7 +54,8 @@ func (d *Yun139) Init(ctx context.Context) error {
"userInfo": base.Json{
"userType": 1,
"accountType": 1,
- "accountName": d.Account},
+ "accountName": d.Account,
+ },
"modAddrType": 1,
}, &resp)
if err != nil {
@@ -540,16 +541,15 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
if size > partSize {
part = (size + partSize - 1) / partSize
}
+
+ // 生成所有 partInfos
partInfos := make([]PartInfo, 0, part)
for i := int64(0); i < part; i++ {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
start := i * partSize
- byteSize := size - start
- if byteSize > partSize {
- byteSize = partSize
- }
+ byteSize := min(size-start, partSize)
partNumber := i + 1
partInfo := PartInfo{
PartNumber: partNumber,
@@ -597,17 +597,20 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
// resp.Data.RapidUpload: true 支持快传,但此处直接检测是否返回分片的上传地址
// 快传的情况下同样需要手动处理冲突
if resp.Data.PartInfos != nil {
- // 读取前100个分片的上传地址
- uploadPartInfos := resp.Data.PartInfos
-
- // 获取后续分片的上传地址
- for i := 101; i < len(partInfos); i += 100 {
- end := i + 100
- if end > len(partInfos) {
- end = len(partInfos)
- }
- batchPartInfos := partInfos[i:end]
+ // Progress
+ p := driver.NewProgress(size, up)
+ rateLimited := driver.NewLimitedUploadStream(ctx, stream)
+
+ // 先上传前100个分片
+ err = d.uploadPersonalParts(ctx, partInfos, resp.Data.PartInfos, rateLimited, p)
+ if err != nil {
+ return err
+ }
+ // 如果还有剩余分片,分批获取上传地址并上传
+ for i := 100; i < len(partInfos); i += 100 {
+ end := min(i+100, len(partInfos))
+ batchPartInfos := partInfos[i:end]
moredata := base.Json{
"fileId": resp.Data.FileId,
"uploadId": resp.Data.UploadId,
@@ -623,44 +626,13 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
if err != nil {
return err
}
- uploadPartInfos = append(uploadPartInfos, moreresp.Data.PartInfos...)
- }
-
- // Progress
- p := driver.NewProgress(size, up)
-
- rateLimited := driver.NewLimitedUploadStream(ctx, stream)
- // 上传所有分片
- for _, uploadPartInfo := range uploadPartInfos {
- index := uploadPartInfo.PartNumber - 1
- partSize := partInfos[index].PartSize
- log.Debugf("[139] uploading part %+v/%+v", index, len(uploadPartInfos))
- limitReader := io.LimitReader(rateLimited, partSize)
-
- // Update Progress
- r := io.TeeReader(limitReader, p)
-
- req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, r)
+ err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, rateLimited, p)
if err != nil {
return err
}
- req.Header.Set("Content-Type", "application/octet-stream")
- req.Header.Set("Content-Length", fmt.Sprint(partSize))
- req.Header.Set("Origin", "https://yun.139.com")
- req.Header.Set("Referer", "https://yun.139.com/")
- req.ContentLength = partSize
-
- res, err := base.HttpClient.Do(req)
- if err != nil {
- return err
- }
- _ = res.Body.Close()
- log.Debugf("[139] uploaded: %+v", res)
- if res.StatusCode != http.StatusOK {
- return fmt.Errorf("unexpected status code: %d", res.StatusCode)
- }
}
+ // 全部分片上传完毕后,complete
data = base.Json{
"contentHash": fullHash,
"contentHashAlgorithm": "SHA256",
@@ -767,7 +739,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
"manualRename": 2,
"operation": 0,
"path": path.Join(dstDir.GetPath(), dstDir.GetID()),
- "seqNo": random.String(32), //序列号不能为空
+ "seqNo": random.String(32), // 序列号不能为空
"totalSize": reportSize,
"uploadContentList": []base.Json{{
"contentName": stream.GetName(),
@@ -869,4 +841,48 @@ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{},
}
}
+func (d *Yun139) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ if d.UserDomainID == "" {
+ return nil, errs.NotImplement
+ }
+ var total, free uint64
+ if d.isFamily() {
+ diskInfo, err := d.getFamilyDiskInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ totalMb, err := strconv.ParseUint(diskInfo.Data.DiskSize, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("failed convert disk size into integer: %+v", err)
+ }
+ usedMb, err := strconv.ParseUint(diskInfo.Data.UsedSize, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("failed convert used size into integer: %+v", err)
+ }
+ total = totalMb * 1024 * 1024
+ free = total - (usedMb * 1024 * 1024)
+ } else {
+ diskInfo, err := d.getPersonalDiskInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ totalMb, err := strconv.ParseUint(diskInfo.Data.DiskSize, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("failed convert disk size into integer: %+v", err)
+ }
+ freeMb, err := strconv.ParseUint(diskInfo.Data.FreeDiskSize, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("failed convert free size into integer: %+v", err)
+ }
+ total = totalMb * 1024 * 1024
+ free = freeMb * 1024 * 1024
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: free,
+ },
+ }, nil
+}
+
var _ driver.Driver = (*Yun139)(nil)
diff --git a/drivers/139/meta.go b/drivers/139/meta.go
index d0c713c7..6082d798 100644
--- a/drivers/139/meta.go
+++ b/drivers/139/meta.go
@@ -11,6 +11,7 @@ type Addition struct {
driver.RootID
Type string `json:"type" type:"select" options:"personal_new,family,group,personal" default:"personal_new"`
CloudID string `json:"cloud_id"`
+ UserDomainID string `json:"user_domain_id" help:"ud_id in Cookie, fill in to show disk usage"`
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
ReportRealSize bool `json:"report_real_size" type:"bool" default:"true" help:"Enable to report the real file size during upload"`
UseLargeThumbnail bool `json:"use_large_thumbnail" type:"bool" default:"false" help:"Enable to use large thumbnail for images"`
diff --git a/drivers/139/types.go b/drivers/139/types.go
index d5f025a1..118655de 100644
--- a/drivers/139/types.go
+++ b/drivers/139/types.go
@@ -312,3 +312,20 @@ type RefreshTokenResp struct {
AccessToken string `xml:"accessToken"`
Desc string `xml:"desc"`
}
+
+type PersonalDiskInfoResp struct {
+ BaseResp
+ Data struct {
+ FreeDiskSize string `json:"freeDiskSize"`
+ DiskSize string `json:"diskSize"`
+ IsInfinitePicStorage *bool `json:"isInfinitePicStorage"`
+ } `json:"data"`
+}
+
+type FamilyDiskInfoResp struct {
+ BaseResp
+ Data struct {
+ UsedSize string `json:"usedSize"`
+ DiskSize string `json:"diskSize"`
+ } `json:"data"`
+}
diff --git a/drivers/139/util.go b/drivers/139/util.go
index d29dd305..3665660d 100644
--- a/drivers/139/util.go
+++ b/drivers/139/util.go
@@ -1,11 +1,13 @@
package _139
import (
+ "context"
"encoding/base64"
"errors"
"fmt"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/token"
+ "io"
"net/http"
"net/url"
"path"
@@ -15,6 +17,7 @@ import (
"time"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
+ "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/pkg/utils"
@@ -107,8 +110,7 @@ func (d *Yun139) refreshToken() error {
return nil
}
-func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- url := "https://yun.139.com" + pathname
+func (d *Yun139) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
randStr := random.String(16)
ts := time.Now().Format("2006-01-02 15:04:05")
@@ -219,7 +221,7 @@ func (d *Yun139) requestRoute(data interface{}, resp interface{}) ([]byte, error
}
func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
- return d.request(pathname, http.MethodPost, func(req *resty.Request) {
+ return d.request("https://yun.139.com"+pathname, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, resp)
}
@@ -268,7 +270,7 @@ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
HashInfo: utils.NewHashInfo(utils.MD5, content.Digest),
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
- //Thumbnail: content.BigthumbnailURL,
+ // Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
@@ -335,7 +337,7 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
Path: path, // 文件所在目录的Path
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
- //Thumbnail: content.BigthumbnailURL,
+ // Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
@@ -390,7 +392,7 @@ func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) {
Path: path, // 文件所在目录的Path
},
Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
- //Thumbnail: content.BigthumbnailURL,
+ // Thumbnail: content.BigthumbnailURL,
}
files = append(files, &f)
}
@@ -418,6 +420,7 @@ func (d *Yun139) getLink(contentId string) (string, error) {
}
return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
}
+
func (d *Yun139) familyGetLink(contentId string, path string) (string, error) {
data := d.newJson(base.Json{
"contentID": contentId,
@@ -510,6 +513,7 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R
}
return res.Body(), nil
}
+
func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
@@ -545,7 +549,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
}
nextPageCursor = resp.Data.NextPageCursor
for _, item := range resp.Data.Items {
- var isFolder = (item.Type == "folder")
+ isFolder := (item.Type == "folder")
var f model.Obj
if isFolder {
f = &model.Object{
@@ -557,7 +561,7 @@ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
IsFolder: isFolder,
}
} else {
- var Thumbnails = item.Thumbnails
+ Thumbnails := item.Thumbnails
var ThumbnailUrl string
if d.UseLargeThumbnail {
for _, thumb := range Thumbnails {
@@ -600,7 +604,7 @@ func (d *Yun139) personalGetLink(fileId string) (string, error) {
if err != nil {
return "", err
}
- var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString()
+ cdnUrl := jsoniter.Get(res, "data", "cdnUrl").ToString()
if cdnUrl != "" {
return cdnUrl, nil
} else {
@@ -614,15 +618,91 @@ func (d *Yun139) getAuthorization() string {
}
return d.Authorization
}
+
func (d *Yun139) getAccount() string {
if d.ref != nil {
return d.ref.getAccount()
}
return d.Account
}
+
func (d *Yun139) getPersonalCloudHost() string {
if d.ref != nil {
return d.ref.getPersonalCloudHost()
}
return d.PersonalCloudHost
}
+
+func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, uploadPartInfos []PersonalPartInfo, rateLimited *driver.RateLimitReader, p *driver.Progress) error {
+ // 确保数组以 PartNumber 从小到大排序
+ sort.Slice(uploadPartInfos, func(i, j int) bool {
+ return uploadPartInfos[i].PartNumber < uploadPartInfos[j].PartNumber
+ })
+
+ for _, uploadPartInfo := range uploadPartInfos {
+ index := uploadPartInfo.PartNumber - 1
+ if index < 0 || index >= len(partInfos) {
+ return fmt.Errorf("invalid PartNumber %d: index out of bounds (partInfos length: %d)", uploadPartInfo.PartNumber, len(partInfos))
+ }
+ partSize := partInfos[index].PartSize
+ log.Debugf("[139] uploading part %+v/%+v", index, len(partInfos))
+ limitReader := io.LimitReader(rateLimited, partSize)
+ r := io.TeeReader(limitReader, p)
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, r)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.Header.Set("Content-Length", fmt.Sprint(partSize))
+ req.Header.Set("Origin", "https://yun.139.com")
+ req.Header.Set("Referer", "https://yun.139.com/")
+ req.ContentLength = partSize
+ err = func() error {
+ res, err := base.HttpClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ log.Debugf("[139] uploaded: %+v", res)
+ if res.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(res.Body)
+ return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(body))
+ }
+ return nil
+ }()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (d *Yun139) getPersonalDiskInfo(ctx context.Context) (*PersonalDiskInfoResp, error) {
+ data := map[string]interface{}{
+ "userDomainId": d.UserDomainID,
+ }
+ var resp PersonalDiskInfoResp
+ _, err := d.request("https://user-njs.yun.139.com/user/disk/getPersonalDiskInfo", http.MethodPost, func(req *resty.Request) {
+ req.SetBody(data)
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
+
+func (d *Yun139) getFamilyDiskInfo(ctx context.Context) (*FamilyDiskInfoResp, error) {
+ data := map[string]interface{}{
+ "userDomainId": d.UserDomainID,
+ }
+ var resp FamilyDiskInfoResp
+ _, err := d.request("https://user-njs.yun.139.com/user/disk/getFamilyDiskInfo", http.MethodPost, func(req *resty.Request) {
+ req.SetBody(data)
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/189/driver.go b/drivers/189/driver.go
index 62287c73..acefeae8 100644
--- a/drivers/189/driver.go
+++ b/drivers/189/driver.go
@@ -196,4 +196,17 @@ func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return d.newUpload(ctx, dstDir, stream, up)
}
+func (d *Cloud189) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ capacityInfo, err := d.getCapacityInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: capacityInfo.CloudCapacityInfo.TotalSize,
+ FreeSpace: capacityInfo.CloudCapacityInfo.FreeSize,
+ },
+ }, nil
+}
+
var _ driver.Driver = (*Cloud189)(nil)
diff --git a/drivers/189/types.go b/drivers/189/types.go
index 5354db95..4aee16e1 100644
--- a/drivers/189/types.go
+++ b/drivers/189/types.go
@@ -66,3 +66,21 @@ type DownResp struct {
ResMessage string `json:"res_message"`
FileDownloadUrl string `json:"downloadUrl"`
}
+
+type CapacityResp struct {
+ ResCode int `json:"res_code"`
+ ResMessage string `json:"res_message"`
+ Account string `json:"account"`
+ CloudCapacityInfo struct {
+ FreeSize uint64 `json:"freeSize"`
+ MailUsedSize uint64 `json:"mail189UsedSize"`
+ TotalSize uint64 `json:"totalSize"`
+ UsedSize uint64 `json:"usedSize"`
+ } `json:"cloudCapacityInfo"`
+ FamilyCapacityInfo struct {
+ FreeSize uint64 `json:"freeSize"`
+ TotalSize uint64 `json:"totalSize"`
+ UsedSize uint64 `json:"usedSize"`
+ } `json:"familyCapacityInfo"`
+ TotalSize uint64 `json:"totalSize"`
+}
diff --git a/drivers/189/util.go b/drivers/189/util.go
index d10c7e8b..6e0682ea 100644
--- a/drivers/189/util.go
+++ b/drivers/189/util.go
@@ -157,7 +157,7 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
if err != nil {
return nil, err
}
- //log.Debug(res.String())
+ // log.Debug(res.String())
if e.ErrorCode != "" {
if e.ErrorCode == "InvalidSessionKey" {
err = d.newLogin()
@@ -186,8 +186,8 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
"mediaType": "0",
"folderId": fileId,
"iconOption": "5",
- "orderBy": "lastOpTime", //account.OrderBy
- "descending": "true", //account.OrderDirection
+ "orderBy": "lastOpTime", // account.OrderBy
+ "descending": "true", // account.OrderDirection
})
}, &resp)
if err != nil {
@@ -311,7 +311,7 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
}
d.sessionKey = sessionKey
const DEFAULT int64 = 10485760
- var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
+ count := int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
res, err := d.uploadRequest("/person/initMultiUpload", map[string]string{
"parentFolderId": dstDir.GetID(),
@@ -340,10 +340,10 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
if DEFAULT < byteSize {
byteSize = DEFAULT
}
- //log.Debugf("%d,%d", byteSize, finish)
+ // log.Debugf("%d,%d", byteSize, finish)
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
- //log.Debug(err, n)
+ // log.Debug(err, n)
if err != nil {
return err
}
@@ -395,3 +395,14 @@ func (d *Cloud189) newUpload(ctx context.Context, dstDir model.Obj, file model.F
}, nil)
return err
}
+
+func (d *Cloud189) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {
+ var resp CapacityResp
+ _, err := d.request("https://cloud.189.cn/api/portal/getUserSizeInfo.action", http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/189_tv/driver.go b/drivers/189_tv/driver.go
index cf943a42..6ee69c7e 100644
--- a/drivers/189_tv/driver.go
+++ b/drivers/189_tv/driver.go
@@ -1,7 +1,6 @@
package _189_tv
import (
- "container/ring"
"context"
"net/http"
"strconv"
@@ -12,18 +11,20 @@ import (
"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/pkg/cron"
"github.com/go-resty/resty/v2"
)
type Cloud189TV struct {
model.Storage
Addition
- client *resty.Client
- tokenInfo *AppSessionResp
- uploadThread int
- familyTransferFolder *ring.Ring
- cleanFamilyTransferFile func()
- storageConfig driver.Config
+ client *resty.Client
+ tokenInfo *AppSessionResp
+ uploadThread int
+ storageConfig driver.Config
+
+ TempUuid string
+ cron *cron.Cron // 新增 cron 字段
}
func (y *Cloud189TV) Config() driver.Config {
@@ -68,7 +69,7 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) {
// 避免重复登陆
if !y.isLogin() || y.Addition.AccessToken == "" {
if err = y.login(); err != nil {
- return
+ return err
}
}
@@ -79,10 +80,17 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) {
}
}
- return
+ y.cron = cron.NewCron(time.Minute * 5)
+ y.cron.Do(y.keepAlive)
+
+ return err
}
func (y *Cloud189TV) Drop(ctx context.Context) error {
+ if y.cron != nil {
+ y.cron.Stop()
+ y.cron = nil
+ }
return nil
}
@@ -236,7 +244,6 @@ func (y *Cloud189TV) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
-
if err != nil {
return err
}
@@ -270,5 +277,25 @@ func (y *Cloud189TV) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
}
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
+}
+func (y *Cloud189TV) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ capacityInfo, err := y.getCapacityInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var total, free uint64
+ if y.isFamily() {
+ total = capacityInfo.FamilyCapacityInfo.TotalSize
+ free = capacityInfo.FamilyCapacityInfo.FreeSize
+ } else {
+ total = capacityInfo.CloudCapacityInfo.TotalSize
+ free = capacityInfo.CloudCapacityInfo.FreeSize
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: free,
+ },
+ }, nil
}
diff --git a/drivers/189_tv/meta.go b/drivers/189_tv/meta.go
index efe344e3..f50fe7ea 100644
--- a/drivers/189_tv/meta.go
+++ b/drivers/189_tv/meta.go
@@ -8,7 +8,6 @@ import (
type Addition struct {
driver.RootID
AccessToken string `json:"access_token"`
- TempUuid string
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
diff --git a/drivers/189_tv/types.go b/drivers/189_tv/types.go
index fbec8016..686a090e 100644
--- a/drivers/189_tv/types.go
+++ b/drivers/189_tv/types.go
@@ -316,3 +316,21 @@ type BatchTaskConflictTaskInfoResp struct {
TaskInfos []BatchTaskInfo
TaskType int `json:"taskType"`
}
+
+type CapacityResp struct {
+ ResCode int `json:"res_code"`
+ ResMessage string `json:"res_message"`
+ Account string `json:"account"`
+ CloudCapacityInfo struct {
+ FreeSize uint64 `json:"freeSize"`
+ MailUsedSize uint64 `json:"mail189UsedSize"`
+ TotalSize uint64 `json:"totalSize"`
+ UsedSize uint64 `json:"usedSize"`
+ } `json:"cloudCapacityInfo"`
+ FamilyCapacityInfo struct {
+ FreeSize uint64 `json:"freeSize"`
+ TotalSize uint64 `json:"totalSize"`
+ UsedSize uint64 `json:"usedSize"`
+ } `json:"familyCapacityInfo"`
+ TotalSize uint64 `json:"totalSize"`
+}
diff --git a/drivers/189_tv/utils.go b/drivers/189_tv/utils.go
index fd4d74df..cea9411e 100644
--- a/drivers/189_tv/utils.go
+++ b/drivers/189_tv/utils.go
@@ -66,6 +66,13 @@ func (y *Cloud189TV) AppKeySignatureHeader(url, method string) map[string]string
}
func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {
+ return y.requestWithRetry(url, method, callback, params, resp, 0, isFamily...)
+}
+
+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())
if params != nil {
@@ -91,7 +98,22 @@ func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, para
if strings.Contains(res.String(), "userSessionBO is null") ||
strings.Contains(res.String(), "InvalidSessionKey") {
- return nil, errors.New("session expired")
+ // 限制重试次数,避免无限递归
+ if retryCount >= 3 {
+ y.Addition.AccessToken = ""
+ op.MustSaveDriverStorage(y)
+ return nil, errors.New("session expired after retry")
+ }
+
+ // 尝试刷新会话
+ if err := y.refreshSession(); err != nil {
+ // 如果刷新失败,说明AccessToken也已过期,需要重新登录
+ y.Addition.AccessToken = ""
+ op.MustSaveDriverStorage(y)
+ return nil, errors.New("session expired")
+ }
+ // 如果刷新成功,则重试原始请求(增加重试计数)
+ return y.requestWithRetry(url, method, callback, params, resp, retryCount+1, isFamily...)
}
// 处理错误
@@ -131,6 +153,7 @@ func (y *Cloud189TV) put(ctx context.Context, url string, headers map[string]str
}
}
+ // 请求完成后http.Client会Close Request.Body
resp, err := base.HttpClient.Do(req)
if err != nil {
return nil, err
@@ -153,6 +176,7 @@ func (y *Cloud189TV) put(ctx context.Context, url string, headers map[string]str
}
return body, nil
}
+
func (y *Cloud189TV) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
fullUrl := ApiUrl
if isFamily {
@@ -210,7 +234,7 @@ func (y *Cloud189TV) login() (err error) {
var erron RespErr
var tokenInfo AppSessionResp
if y.Addition.AccessToken == "" {
- if y.Addition.TempUuid == "" {
+ if y.TempUuid == "" {
// 获取登录参数
var uuidInfo UuidInfoResp
req.SetResult(&uuidInfo).SetError(&erron)
@@ -218,9 +242,8 @@ func (y *Cloud189TV) login() (err error) {
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/getQrCodeUUID.action",
http.MethodGet))
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/getQrCodeUUID.action")
-
if err != nil {
- return
+ return err
}
if erron.HasError() {
return &erron
@@ -229,7 +252,7 @@ func (y *Cloud189TV) login() (err error) {
if uuidInfo.Uuid == "" {
return errors.New("uuidInfo is empty")
}
- y.Addition.TempUuid = uuidInfo.Uuid
+ y.TempUuid = uuidInfo.Uuid
op.MustSaveDriverStorage(y)
// 展示二维码
@@ -257,10 +280,10 @@ func (y *Cloud189TV) login() (err error) {
// Signature
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action",
http.MethodGet))
- req.SetQueryParam("uuid", y.Addition.TempUuid)
+ req.SetQueryParam("uuid", y.TempUuid)
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action")
if err != nil {
- return
+ return err
}
if erron.HasError() {
return &erron
@@ -269,7 +292,6 @@ func (y *Cloud189TV) login() (err error) {
return errors.New("E189AccessToken is empty")
}
y.Addition.AccessToken = accessTokenResp.E189AccessToken
- y.Addition.TempUuid = ""
}
}
// 获取SessionKey 和 SessionSecret
@@ -281,7 +303,7 @@ func (y *Cloud189TV) login() (err error) {
reqb.SetQueryParam("e189AccessToken", y.Addition.AccessToken)
_, err = reqb.Execute(http.MethodGet, ApiUrl+"/family/manage/loginFamilyMerge.action")
if err != nil {
- return
+ return err
}
if erron.HasError() {
@@ -290,7 +312,45 @@ func (y *Cloud189TV) login() (err error) {
y.tokenInfo = &tokenInfo
op.MustSaveDriverStorage(y)
- return
+ return err
+}
+
+// refreshSession 尝试使用现有的 AccessToken 刷新会话
+func (y *Cloud189TV) refreshSession() (err error) {
+ var erron RespErr
+ var tokenInfo AppSessionResp
+ reqb := y.client.R().SetQueryParams(clientSuffix())
+ reqb.SetResult(&tokenInfo).SetError(&erron)
+ // Signature
+ reqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/loginFamilyMerge.action",
+ http.MethodGet))
+ reqb.SetQueryParam("e189AccessToken", y.Addition.AccessToken)
+ _, err = reqb.Execute(http.MethodGet, ApiUrl+"/family/manage/loginFamilyMerge.action")
+ if err != nil {
+ return err
+ }
+
+ if erron.HasError() {
+ return &erron
+ }
+
+ y.tokenInfo = &tokenInfo
+ return nil
+}
+
+func (y *Cloud189TV) keepAlive() {
+ _, err := y.get(ApiUrl+"/keepUserSession.action", func(r *resty.Request) {
+ r.SetQueryParams(clientSuffix())
+ }, nil)
+ if err != nil {
+ utils.Log.Warnf("189tv: Failed to keep user session alive: %v", err)
+ // 如果keepAlive失败,尝试刷新session
+ if refreshErr := y.refreshSession(); refreshErr != nil {
+ utils.Log.Errorf("189tv: Failed to refresh session after keepAlive error: %v", refreshErr)
+ }
+ } else {
+ utils.Log.Debugf("189tv: User session kept alive successfully.")
+ }
}
func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
@@ -314,7 +374,7 @@ func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
// 旧版本上传,家庭云不支持覆盖
func (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
fileMd5 := file.GetHash().GetHash(utils.MD5)
- var tempFile = file.GetFile()
+ tempFile := file.GetFile()
var err error
if len(fileMd5) != utils.MD5.Width {
tempFile, fileMd5, err = stream.CacheFullAndHash(file, &up, utils.MD5)
@@ -333,6 +393,10 @@ func (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model
// 网盘中不存在该文件,开始上传
status := GetUploadFileStatusResp{CreateUploadFileResp: *uploadInfo}
+ // driver.RateLimitReader会尝试Close底层的reader
+ // 但这里的tempFile是一个*os.File,Close后就没法继续读了
+ // 所以这里用io.NopCloser包一层
+ rateLimitedRd := driver.NewLimitedUploadStream(ctx, io.NopCloser(tempFile))
for status.GetSize() < file.GetSize() && status.FileDataExists != 1 {
if utils.IsCanceled(ctx) {
return nil, ctx.Err()
@@ -350,7 +414,7 @@ func (y *Cloud189TV) OldUpload(ctx context.Context, dstDir model.Obj, file model
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
}
- _, err := y.put(ctx, status.FileUploadUrl, header, true, tempFile, isFamily)
+ _, err := y.put(ctx, status.FileUploadUrl, header, true, rateLimitedRd, isFamily)
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return nil, err
}
@@ -413,7 +477,6 @@ func (y *Cloud189TV) OldUploadCreate(ctx context.Context, parentID string, fileM
})
}
}, &uploadInfo, isFamily)
-
if err != nil {
return nil, err
}
@@ -567,3 +630,15 @@ func (y *Cloud189TV) WaitBatchTask(aType string, taskID string, t time.Duration)
time.Sleep(t)
}
}
+
+func (y *Cloud189TV) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {
+ fullUrl := ApiUrl + "/portal/getUserSizeInfo.action"
+ var resp CapacityResp
+ _, err := y.get(fullUrl, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/189pc/driver.go b/drivers/189pc/driver.go
index 00c6b684..8b69556a 100644
--- a/drivers/189pc/driver.go
+++ b/drivers/189pc/driver.go
@@ -27,8 +27,10 @@ type Cloud189PC struct {
client *resty.Client
- loginParam *LoginParam
- tokenInfo *AppSessionResp
+ loginParam *LoginParam
+ qrcodeParam *QRLoginParam
+
+ tokenInfo *AppSessionResp
uploadThread int
@@ -114,6 +116,11 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
return nil
}
}
+
+ // 初始化并启动 cron 任务
+ y.cron = cron.NewCron(time.Duration(time.Minute * 5))
+ // 每5分钟执行一次 keepAlive
+ y.cron.Do(y.keepAlive)
}
// 处理家庭云ID
@@ -160,6 +167,7 @@ func (y *Cloud189PC) Drop(ctx context.Context) error {
y.ref = nil
if y.cron != nil {
y.cron.Stop()
+ y.cron = nil
}
return nil
}
@@ -328,7 +336,6 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
-
if err != nil {
return err
}
@@ -434,3 +441,24 @@ func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
}
+
+func (y *Cloud189PC) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ capacityInfo, err := y.getCapacityInfo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var total, free uint64
+ if y.isFamily() {
+ total = capacityInfo.FamilyCapacityInfo.TotalSize
+ free = capacityInfo.FamilyCapacityInfo.FreeSize
+ } else {
+ total = capacityInfo.CloudCapacityInfo.TotalSize
+ free = capacityInfo.CloudCapacityInfo.FreeSize
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: free,
+ },
+ }, nil
+}
diff --git a/drivers/189pc/help.go b/drivers/189pc/help.go
index 8bd90d47..6f6c59f3 100644
--- a/drivers/189pc/help.go
+++ b/drivers/189pc/help.go
@@ -80,6 +80,20 @@ func timestamp() int64 {
return time.Now().UTC().UnixNano() / 1e6
}
+// formatDate formats a time.Time object into the "YYYY-MM-DDHH:mm:ssSSS" format.
+func formatDate(t time.Time) string {
+ // The layout string "2006-01-0215:04:05.000" corresponds to:
+ // 2006 -> Year (YYYY)
+ // 01 -> Month (MM)
+ // 02 -> Day (DD)
+ // 15 -> Hour (HH)
+ // 04 -> Minute (mm)
+ // 05 -> Second (ss)
+ // 000 -> Millisecond (SSS) with leading zeros
+ // Note the lack of a separator between the date and hour, matching the desired output.
+ return t.Format("2006-01-0215:04:05.000")
+}
+
func MustParseTime(str string) *time.Time {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
return &lastOpTime
diff --git a/drivers/189pc/meta.go b/drivers/189pc/meta.go
index cbfb6498..a3325efe 100644
--- a/drivers/189pc/meta.go
+++ b/drivers/189pc/meta.go
@@ -6,9 +6,11 @@ import (
)
type Addition struct {
- Username string `json:"username" required:"true"`
- Password string `json:"password" required:"true"`
- VCode string `json:"validate_code"`
+ LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"`
+ Username string `json:"username" required:"true"`
+ Password string `json:"password" required:"true"`
+ VCode string `json:"validate_code"`
+ RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
diff --git a/drivers/189pc/types.go b/drivers/189pc/types.go
index 6620483f..fe219d8c 100644
--- a/drivers/189pc/types.go
+++ b/drivers/189pc/types.go
@@ -68,15 +68,7 @@ func (e *RespErr) Error() string {
return ""
}
-// 登陆需要的参数
-type LoginParam struct {
- // 加密后的用户名和密码
- RsaUsername string
- RsaPassword string
-
- // rsa密钥
- jRsaKey string
-
+type BaseLoginParam struct {
// 请求头参数
Lt string
ReqId string
@@ -88,6 +80,27 @@ type LoginParam struct {
CaptchaToken string
}
+// QRLoginParam 用于暂存二维码登录过程中的参数
+type QRLoginParam struct {
+ BaseLoginParam
+
+ UUID string `json:"uuid"`
+ EncodeUUID string `json:"encodeuuid"`
+ EncryUUID string `json:"encryuuid"`
+}
+
+// 登陆需要的参数
+type LoginParam struct {
+ // 加密后的用户名和密码
+ RsaUsername string
+ RsaPassword string
+
+ // rsa密钥
+ jRsaKey string
+
+ BaseLoginParam
+}
+
// 登陆加密相关
type EncryptConfResp struct {
Result int `json:"result"`
@@ -396,3 +409,21 @@ func (p Params) Encode() string {
}
return buf.String()
}
+
+type CapacityResp struct {
+ ResCode int `json:"res_code"`
+ ResMessage string `json:"res_message"`
+ Account string `json:"account"`
+ CloudCapacityInfo struct {
+ FreeSize uint64 `json:"freeSize"`
+ MailUsedSize uint64 `json:"mail189UsedSize"`
+ TotalSize uint64 `json:"totalSize"`
+ UsedSize uint64 `json:"usedSize"`
+ } `json:"cloudCapacityInfo"`
+ FamilyCapacityInfo struct {
+ FreeSize uint64 `json:"freeSize"`
+ TotalSize uint64 `json:"totalSize"`
+ UsedSize uint64 `json:"usedSize"`
+ } `json:"familyCapacityInfo"`
+ TotalSize uint64 `json:"totalSize"`
+}
diff --git a/drivers/189pc/utils.go b/drivers/189pc/utils.go
index c791e755..64a06663 100644
--- a/drivers/189pc/utils.go
+++ b/drivers/189pc/utils.go
@@ -29,6 +29,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
+ "github.com/skip2/go-qrcode"
"github.com/avast/retry-go"
"github.com/go-resty/resty/v2"
@@ -54,6 +55,9 @@ const (
MAC = "TELEMAC"
CHANNEL_ID = "web_cloud.189.cn"
+
+ // Error codes
+ UserInvalidOpenTokenError = "UserInvalidOpenToken"
)
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
@@ -86,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) {
+ if y.getTokenInfo() == nil {
+ return nil, fmt.Errorf("login failed")
+ }
req := y.getClient().R().SetQueryParams(clientSuffix())
// 设置params
@@ -185,6 +192,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
return body, nil
}
+
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
res := make([]model.Obj, 0, 100)
for pageNum := 1; ; pageNum++ {
@@ -264,7 +272,14 @@ func (y *Cloud189PC) findFileByName(ctx context.Context, searchName string, fold
}
}
-func (y *Cloud189PC) login() (err error) {
+func (y *Cloud189PC) login() error {
+ if y.LoginType == "qrcode" {
+ return y.loginByQRCode()
+ }
+ return y.loginByPassword()
+}
+
+func (y *Cloud189PC) loginByPassword() (err error) {
// 初始化登陆所需参数
if y.loginParam == nil {
if err = y.initLoginParam(); err != nil {
@@ -278,10 +293,15 @@ func (y *Cloud189PC) login() (err error) {
// 销毁登陆参数
y.loginParam = nil
// 遇到错误,重新加载登陆参数(刷新验证码)
- if err != nil && y.NoUseOcr {
- if err1 := y.initLoginParam(); err1 != nil {
- err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
+ if err != nil {
+ if y.NoUseOcr {
+ if err1 := y.initLoginParam(); err1 != nil {
+ err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
+ }
}
+
+ y.Status = err.Error()
+ op.MustSaveDriverStorage(y)
}
}()
@@ -326,7 +346,7 @@ func (y *Cloud189PC) login() (err error) {
SetQueryParam("redirectURL", loginresp.ToUrl).
Post(API_URL + "/getSessionForPC.action")
if err != nil {
- return
+ return err
}
if erron.HasError() {
@@ -334,16 +354,106 @@ func (y *Cloud189PC) login() (err error) {
}
if tokenInfo.ResCode != 0 {
err = fmt.Errorf(tokenInfo.ResMessage)
- return
+ return err
}
+ y.Addition.RefreshToken = tokenInfo.RefreshToken
y.tokenInfo = &tokenInfo
- return
+ op.MustSaveDriverStorage(y)
+ return err
}
-/* 初始化登陆需要的参数
-* 如果遇到验证码返回错误
- */
-func (y *Cloud189PC) initLoginParam() error {
+func (y *Cloud189PC) loginByQRCode() error {
+ if y.qrcodeParam == nil {
+ if err := y.initQRCodeParam(); err != nil {
+ // 二维码也通过错误返回
+ return err
+ }
+ }
+
+ var state struct {
+ Status int `json:"status"`
+ RedirectUrl string `json:"redirectUrl"`
+ Msg string `json:"msg"`
+ }
+
+ now := time.Now()
+ _, err := y.client.R().
+ SetHeaders(map[string]string{
+ "Referer": AUTH_URL,
+ "Reqid": y.qrcodeParam.ReqId,
+ "lt": y.qrcodeParam.Lt,
+ }).
+ SetFormData(map[string]string{
+ "appId": APP_ID,
+ "clientType": CLIENT_TYPE,
+ "returnUrl": RETURN_URL,
+ "paramId": y.qrcodeParam.ParamId,
+ "uuid": y.qrcodeParam.UUID,
+ "encryuuid": y.qrcodeParam.EncryUUID,
+ "date": formatDate(now),
+ "timeStamp": fmt.Sprint(now.UTC().UnixNano() / 1e6),
+ }).
+ ForceContentType("application/json;charset=UTF-8").
+ SetResult(&state).
+ Post(AUTH_URL + "/api/logbox/oauth2/qrcodeLoginState.do")
+ if err != nil {
+ return fmt.Errorf("failed to check QR code state: %w", err)
+ }
+
+ switch state.Status {
+ case 0: // 登录成功
+ var tokenInfo AppSessionResp
+ _, err = y.client.R().
+ SetResult(&tokenInfo).
+ SetQueryParams(clientSuffix()).
+ SetQueryParam("redirectURL", state.RedirectUrl).
+ Post(API_URL + "/getSessionForPC.action")
+ if err != nil {
+ return err
+ }
+ if tokenInfo.ResCode != 0 {
+ return fmt.Errorf(tokenInfo.ResMessage)
+ }
+ y.Addition.RefreshToken = tokenInfo.RefreshToken
+ y.tokenInfo = &tokenInfo
+ op.MustSaveDriverStorage(y)
+ return nil
+ case -11001: // 二维码过期
+ y.qrcodeParam = nil
+ return errors.New("QR code expired, please try again")
+ case -106: // 等待扫描
+ return y.genQRCode("QR code has not been scanned yet, please scan and save again")
+ case -11002: // 等待确认
+ return y.genQRCode("QR code has been scanned, please confirm the login on your phone and save again")
+ default: // 其他错误
+ y.qrcodeParam = nil
+ return fmt.Errorf("QR code login failed with status %d: %s", state.Status, state.Msg)
+ }
+}
+
+func (y *Cloud189PC) genQRCode(text string) error {
+ // 展示二维码
+ qrTemplate := `
+ state: %s
+
+
Or Click here: Login
+`
+
+ // Generate QR code
+ qrCode, err := qrcode.Encode(y.qrcodeParam.UUID, qrcode.Medium, 256)
+ if err != nil {
+ return fmt.Errorf("failed to generate QR code: %v", err)
+ }
+
+ // Encode QR code to base64
+ qrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)
+
+ // Create the HTML page
+ qrPage := fmt.Sprintf(qrTemplate, text, qrCodeBase64, y.qrcodeParam.UUID)
+ return fmt.Errorf("need verify: \n%s", qrPage)
+}
+
+func (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) {
// 清除cookie
jar, _ := cookiejar.New(nil)
y.client.SetCookieJar(jar)
@@ -357,17 +467,30 @@ func (y *Cloud189PC) initLoginParam() error {
}).
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
if err != nil {
- return err
+ return nil, err
}
- param := LoginParam{
+ return &BaseLoginParam{
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
- // jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
+ }, nil
+}
+
+/* 初始化登陆需要的参数
+ * 如果遇到验证码返回错误
+ */
+func (y *Cloud189PC) initLoginParam() error {
+ y.loginParam = nil
+
+ baseParam, err := y.initBaseParams()
+ if err != nil {
+ return err
}
+ y.loginParam = &LoginParam{BaseLoginParam: *baseParam}
+
// 获取rsa公钥
var encryptConf EncryptConfResp
_, err = y.client.R().
@@ -378,18 +501,17 @@ func (y *Cloud189PC) initLoginParam() error {
return err
}
- param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
- param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
- param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
- y.loginParam = ¶m
+ y.loginParam.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
+ y.loginParam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Username)
+ y.loginParam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Password)
// 判断是否需要验证码
resp, err := y.client.R().
- SetHeader("REQID", param.ReqId).
+ SetHeader("REQID", y.loginParam.ReqId).
SetFormData(map[string]string{
"appKey": APP_ID,
"accountType": ACCOUNT_TYPE,
- "userName": param.RsaUsername,
+ "userName": y.loginParam.RsaUsername,
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
if err != nil {
return err
@@ -401,8 +523,8 @@ func (y *Cloud189PC) initLoginParam() error {
// 拉取验证码
imgRes, err := y.client.R().
SetQueryParams(map[string]string{
- "token": param.CaptchaToken,
- "REQID": param.ReqId,
+ "token": y.loginParam.CaptchaToken,
+ "REQID": y.loginParam.ReqId,
"rnd": fmt.Sprint(timestamp()),
}).
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
@@ -429,10 +551,38 @@ func (y *Cloud189PC) initLoginParam() error {
return nil
}
+// getQRCode 获取并返回二维码
+func (y *Cloud189PC) initQRCodeParam() (err error) {
+ y.qrcodeParam = nil
+
+ baseParam, err := y.initBaseParams()
+ if err != nil {
+ return err
+ }
+
+ var qrcodeParam QRLoginParam
+ _, err = y.client.R().
+ SetFormData(map[string]string{"appId": APP_ID}).
+ ForceContentType("application/json;charset=UTF-8").
+ SetResult(&qrcodeParam).
+ Post(AUTH_URL + "/api/logbox/oauth2/getUUID.do")
+ if err != nil {
+ return err
+ }
+ qrcodeParam.BaseLoginParam = *baseParam
+ y.qrcodeParam = &qrcodeParam
+
+ return y.genQRCode("please scan the QR code with the 189 Cloud app, then save the settings again.")
+}
+
// 刷新会话
func (y *Cloud189PC) refreshSession() (err error) {
+ return y.refreshSessionWithRetry(0)
+}
+
+func (y *Cloud189PC) refreshSessionWithRetry(retryCount int) (err error) {
if y.ref != nil {
- return y.ref.refreshSession()
+ return y.ref.refreshSessionWithRetry(retryCount)
}
var erron RespErr
var userSessionResp UserSessionResp
@@ -449,37 +599,102 @@ func (y *Cloud189PC) refreshSession() (err error) {
return err
}
- // 错误影响正常访问,下线该储存
- defer func() {
- if err != nil {
- y.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
+ // token生效刷新token
+ if erron.HasError() {
+ if erron.ResCode == UserInvalidOpenTokenError {
+ return y.refreshTokenWithRetry(retryCount)
+ }
+ return &erron
+ }
+ y.tokenInfo.UserSessionResp = userSessionResp
+ return nil
+}
+
+// refreshToken 刷新token,失败时返回错误,不再直接调用login
+func (y *Cloud189PC) refreshToken() (err error) {
+ return y.refreshTokenWithRetry(0)
+}
+
+func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
+ if y.ref != nil {
+ return y.ref.refreshTokenWithRetry(retryCount)
+ }
+
+ // 限制重试次数,避免无限递归
+ if retryCount >= 3 {
+ if y.Addition.RefreshToken != "" {
+ y.Addition.RefreshToken = ""
op.MustSaveDriverStorage(y)
}
- }()
+ return errors.New("refresh token failed after maximum retries")
+ }
+
+ var erron RespErr
+ var tokenInfo AppSessionResp
+ _, err = y.client.R().
+ SetResult(&tokenInfo).
+ ForceContentType("application/json;charset=UTF-8").
+ SetError(&erron).
+ SetFormData(map[string]string{
+ "clientId": APP_ID,
+ "refreshToken": y.tokenInfo.RefreshToken,
+ "grantType": "refresh_token",
+ "format": "json",
+ }).
+ Post(AUTH_URL + "/api/oauth2/refreshToken.do")
+ if err != nil {
+ return err
+ }
+ // 如果刷新失败,返回错误给上层处理
if erron.HasError() {
- if erron.ResCode == "UserInvalidOpenToken" {
- if err = y.login(); err != nil {
- return err
- }
+ if y.Addition.RefreshToken != "" {
+ y.Addition.RefreshToken = ""
+ op.MustSaveDriverStorage(y)
}
- return &erron
+
+ // 根据登录类型决定下一步行为
+ if y.LoginType == "qrcode" {
+ return errors.New("QR code session has expired, please re-scan the code to log in")
+ }
+ // 密码登录模式下,尝试回退到完整登录
+ return y.login()
+ }
+
+ y.Addition.RefreshToken = tokenInfo.RefreshToken
+ y.tokenInfo = &tokenInfo
+ op.MustSaveDriverStorage(y)
+ return y.refreshSessionWithRetry(retryCount + 1)
+}
+
+func (y *Cloud189PC) keepAlive() {
+ _, err := y.get(API_URL+"/keepUserSession.action", func(r *resty.Request) {
+ r.SetQueryParams(clientSuffix())
+ }, nil)
+ if err != nil {
+ utils.Log.Warnf("189pc: Failed to keep user session alive: %v", err)
+ // 如果keepAlive失败,尝试刷新session
+ if refreshErr := y.refreshSession(); refreshErr != nil {
+ utils.Log.Errorf("189pc: Failed to refresh session after keepAlive error: %v", refreshErr)
+ }
+ } else {
+ utils.Log.Debugf("189pc: User session kept alive successfully.")
}
- y.tokenInfo.UserSessionResp = userSessionResp
- return
}
// 普通上传
// 无法上传大小为0的文件
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
- size := file.GetSize()
- sliceSize := min(size, partSize(size))
+ // 文件大小
+ fileSize := file.GetSize()
+ // 分片大小,不得为文件大小
+ sliceSize := partSize(fileSize)
params := Params{
"parentFolderId": dstDir.GetID(),
"fileName": url.QueryEscape(file.GetName()),
- "fileSize": fmt.Sprint(file.GetSize()),
- "sliceSize": fmt.Sprint(sliceSize),
+ "fileSize": fmt.Sprint(fileSize),
+ "sliceSize": fmt.Sprint(sliceSize), // 必须为特定分片大小
"lazyCheck": "1",
}
@@ -488,7 +703,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
params.Set("familyId", y.FamilyID)
fullUrl += "/family"
} else {
- //params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
+ // params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
fullUrl += "/person"
}
@@ -512,10 +727,10 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
retry.DelayType(retry.BackOffDelay))
count := 1
- if size > sliceSize {
- count = int((size + sliceSize - 1) / sliceSize)
+ if fileSize > sliceSize {
+ count = int((fileSize + sliceSize - 1) / sliceSize)
}
- lastPartSize := size % sliceSize
+ lastPartSize := fileSize % sliceSize
if lastPartSize == 0 {
lastPartSize = sliceSize
}
@@ -535,25 +750,25 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
break
}
offset := int64((i)-1) * sliceSize
- size := sliceSize
+ partSize := sliceSize
if i == count {
- size = lastPartSize
+ partSize = lastPartSize
}
partInfo := ""
- var reader *stream.SectionReader
+ var reader io.ReadSeeker
var rateLimitedRd io.Reader
threadG.GoWithLifecycle(errgroup.Lifecycle{
Before: func(ctx context.Context) error {
if reader == nil {
var err error
- reader, err = ss.GetSectionReader(offset, size)
+ reader, err = ss.GetSectionReader(offset, partSize)
if err != nil {
return err
}
silceMd5.Reset()
w, err := utils.CopyWithBuffer(writers, reader)
- if w != size {
- return fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", size, w, err)
+ if w != partSize {
+ return fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", partSize, w, err)
}
// 计算块md5并进行hex和base64编码
md5Bytes := silceMd5.Sum(nil)
@@ -573,8 +788,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
// step.4 上传切片
uploadUrl := uploadUrls[0]
- _, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false,
- driver.NewLimitedUploadStream(ctx, rateLimitedRd), isFamily)
+ _, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, rateLimitedRd, isFamily)
if err != nil {
return err
}
@@ -595,7 +809,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
fileMd5Hex = strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
}
sliceMd5Hex := fileMd5Hex
- if file.GetSize() > sliceSize {
+ if fileSize > sliceSize {
sliceMd5Hex = strings.ToUpper(utils.GetMD5EncodeStr(strings.Join(silceMd5Hexs, "\n")))
}
@@ -665,7 +879,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
lastSliceSize = sliceSize
}
- //step.1 优先计算所需信息
+ // step.1 优先计算所需信息
byteSize := sliceSize
fileMd5 := utils.MD5.NewFunc()
sliceMd5 := utils.MD5.NewFunc()
@@ -716,14 +930,14 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
if isFamily {
fullUrl += "/family"
} else {
- //params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
+ // params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
fullUrl += "/person"
}
// 尝试恢复进度
uploadProgress, ok := base.GetUploadProgress[*UploadProgress](y, y.getTokenInfo().SessionKey, fileMd5Hex)
if !ok {
- //step.2 预上传
+ // step.2 预上传
params := Params{
"parentFolderId": dstDir.GetID(),
"fileName": url.QueryEscape(file.GetName()),
@@ -952,7 +1166,6 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
})
}
}, &uploadInfo, isFamily)
-
if err != nil {
return nil, err
}
@@ -1262,3 +1475,15 @@ func (y *Cloud189PC) getClient() *resty.Client {
}
return y.client
}
+
+func (y *Cloud189PC) getCapacityInfo(ctx context.Context) (*CapacityResp, error) {
+ fullUrl := API_URL + "/portal/getUserSizeInfo.action"
+ var resp CapacityResp
+ _, err := y.get(fullUrl, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/alias/driver.go b/drivers/alias/driver.go
index 6954f2b5..cfa1152d 100644
--- a/drivers/alias/driver.go
+++ b/drivers/alias/driver.go
@@ -23,6 +23,7 @@ import (
type Alias struct {
model.Storage
Addition
+ rootOrder []string
pathMap map[string][]string
autoFlatten bool
oneKey string
@@ -40,13 +41,18 @@ func (d *Alias) Init(ctx context.Context) error {
if d.Paths == "" {
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)
- for _, path := range strings.Split(d.Paths, "\n") {
+ for _, path := range paths {
path = strings.TrimSpace(path)
if path == "" {
continue
}
k, v := getPair(path)
+ if _, ok := d.pathMap[k]; !ok {
+ d.rootOrder = append(d.rootOrder, k)
+ }
d.pathMap[k] = append(d.pathMap[k], v)
}
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 {
+ d.rootOrder = nil
d.pathMap = nil
return nil
}
@@ -79,27 +86,51 @@ func (d *Alias) Get(ctx context.Context, path string) (model.Obj, error) {
if !ok {
return nil, errs.ObjectNotFound
}
+ var ret *model.Object
+ provider := ""
for _, dst := range dsts {
- obj, err := fs.Get(ctx, stdpath.Join(dst, sub), &fs.GetArgs{NoLog: true})
+ rawPath := stdpath.Join(dst, sub)
+ obj, err := fs.Get(ctx, rawPath, &fs.GetArgs{NoLog: true})
if err != nil {
continue
}
- return &model.Object{
- Path: path,
- Name: obj.GetName(),
- Size: obj.GetSize(),
- Modified: obj.ModTime(),
- IsFolder: obj.IsDir(),
- HashInfo: obj.GetHash(),
+ storage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{})
+ if ret == nil {
+ ret = &model.Object{
+ Path: path,
+ Name: obj.GetName(),
+ Size: obj.GetSize(),
+ Modified: obj.ModTime(),
+ IsFolder: obj.IsDir(),
+ HashInfo: obj.GetHash(),
+ }
+ if !d.ProviderPassThrough || err != nil {
+ break
+ }
+ provider = storage.Config().Name
+ } else if err != nil || provider != storage.GetStorage().Driver {
+ provider = ""
+ break
+ }
+ }
+ if ret == nil {
+ return nil, errs.ObjectNotFound
+ }
+ if provider != "" {
+ return &model.ObjectProvider{
+ Object: *ret,
+ Provider: model.Provider{
+ Provider: provider,
+ },
}, nil
}
- return nil, errs.ObjectNotFound
+ return ret, nil
}
func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
path := dir.GetPath()
if utils.PathEqual(path, "/") && !d.autoFlatten {
- return d.listRoot(), nil
+ return d.listRoot(ctx, args.WithStorageDetails && d.DetailsPassThrough, args.Refresh), nil
}
root, sub := d.getRootAndPath(path)
dsts, ok := d.pathMap[root]
@@ -107,27 +138,35 @@ func (d *Alias) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
return nil, errs.ObjectNotFound
}
var objs []model.Obj
- fsArgs := &fs.ListArgs{NoLog: true, Refresh: args.Refresh}
for _, dst := range dsts {
- tmp, err := fs.List(ctx, stdpath.Join(dst, sub), fsArgs)
+ tmp, err := fs.List(ctx, stdpath.Join(dst, sub), &fs.ListArgs{
+ NoLog: true,
+ Refresh: args.Refresh,
+ WithStorageDetails: args.WithStorageDetails && d.DetailsPassThrough,
+ })
if err == nil {
tmp, err = utils.SliceConvert(tmp, func(obj model.Obj) (model.Obj, error) {
- thumb, ok := model.GetThumb(obj)
objRes := model.Object{
Name: obj.GetName(),
Size: obj.GetSize(),
Modified: obj.ModTime(),
IsFolder: obj.IsDir(),
}
- if !ok {
- return &objRes, nil
+ if thumb, ok := model.GetThumb(obj); ok {
+ return &model.ObjThumb{
+ Object: objRes,
+ Thumbnail: model.Thumbnail{
+ Thumbnail: thumb,
+ },
+ }, nil
}
- return &model.ObjThumb{
- Object: objRes,
- Thumbnail: model.Thumbnail{
- Thumbnail: thumb,
- },
- }, nil
+ if details, ok := model.GetStorageDetails(obj); ok {
+ return &model.ObjStorageDetails{
+ Obj: &objRes,
+ StorageDetailsWithName: *details,
+ }, nil
+ }
+ return &objRes, nil
})
}
if err == nil {
@@ -172,9 +211,6 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
if resultLink.ContentLength == 0 {
resultLink.ContentLength = fi.GetSize()
}
- if resultLink.MFile != nil {
- return &resultLink, nil
- }
if d.DownloadConcurrency > 0 {
resultLink.Concurrency = d.DownloadConcurrency
}
@@ -186,6 +222,35 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, errs.ObjectNotFound
}
+func (d *Alias) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
+ root, sub := d.getRootAndPath(args.Obj.GetPath())
+ dsts, ok := d.pathMap[root]
+ if !ok {
+ return nil, errs.ObjectNotFound
+ }
+ for _, dst := range dsts {
+ rawPath := stdpath.Join(dst, sub)
+ storage, actualPath, err := op.GetStorageAndActualPath(rawPath)
+ if err != nil {
+ continue
+ }
+ other, ok := storage.(driver.Other)
+ if !ok {
+ continue
+ }
+ obj, err := op.GetUnwrap(ctx, storage, actualPath)
+ if err != nil {
+ continue
+ }
+ return other.Other(ctx, model.OtherArgs{
+ Obj: obj,
+ Method: args.Method,
+ Data: args.Data,
+ })
+ }
+ return nil, errs.NotImplement
+}
+
func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if !d.Writable {
return errs.PermissionDenied
@@ -197,7 +262,7 @@ func (d *Alias) MakeDir(ctx context.Context, parentDir model.Obj, dirName string
}
return err
}
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name dirs cannot make sub-dir")
}
return err
@@ -208,14 +273,14 @@ func (d *Alias) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.PermissionDenied
}
srcPath, err := d.getReqPath(ctx, srcObj, false)
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name files cannot be moved")
}
if err != nil {
return err
}
dstPath, err := d.getReqPath(ctx, dstDir, true)
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name dirs cannot be moved to")
}
if err != nil {
@@ -243,7 +308,7 @@ func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) er
}
return err
}
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name files cannot be Rename")
}
return err
@@ -254,14 +319,14 @@ func (d *Alias) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.PermissionDenied
}
srcPath, err := d.getReqPath(ctx, srcObj, false)
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name files cannot be copied")
}
if err != nil {
return err
}
dstPath, err := d.getReqPath(ctx, dstDir, true)
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name dirs cannot be copied to")
}
if err != nil {
@@ -295,7 +360,7 @@ func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
}
return err
}
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name files cannot be Delete")
}
return err
@@ -339,7 +404,7 @@ func (d *Alias) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer,
return err
}
}
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name dirs cannot be Put")
}
return err
@@ -356,7 +421,7 @@ func (d *Alias) PutURL(ctx context.Context, dstDir model.Obj, name, url string)
}
return err
}
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name files cannot offline download")
}
return err
@@ -429,14 +494,14 @@ func (d *Alias) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj,
return errs.PermissionDenied
}
srcPath, err := d.getReqPath(ctx, srcObj, false)
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name files cannot be decompressed")
}
if err != nil {
return err
}
dstPath, err := d.getReqPath(ctx, dstDir, true)
- if errs.IsNotImplement(err) {
+ if errs.IsNotImplementError(err) {
return errors.New("same-name dirs cannot be decompressed to")
}
if err != nil {
@@ -459,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)
diff --git a/drivers/alias/meta.go b/drivers/alias/meta.go
index 83e8bba0..763e6647 100644
--- a/drivers/alias/meta.go
+++ b/drivers/alias/meta.go
@@ -15,6 +15,8 @@ type Addition struct {
DownloadConcurrency int `json:"download_concurrency" default:"0" required:"false" type:"number" help:"Need to enable proxy"`
DownloadPartSize int `json:"download_part_size" default:"0" type:"number" required:"false" help:"Need to enable proxy. Unit: KB"`
Writable bool `json:"writable" type:"bool" default:"false"`
+ ProviderPassThrough bool `json:"provider_pass_through" type:"bool" default:"false"`
+ DetailsPassThrough bool `json:"details_pass_through" type:"bool" default:"false"`
}
var config = driver.Config{
@@ -24,6 +26,7 @@ var config = driver.Config{
NoUpload: false,
DefaultRoot: "/",
ProxyRangeOption: true,
+ LinkCacheMode: driver.LinkCacheAuto,
}
func init() {
diff --git a/drivers/alias/util.go b/drivers/alias/util.go
index 11c299e9..80391fda 100644
--- a/drivers/alias/util.go
+++ b/drivers/alias/util.go
@@ -2,8 +2,11 @@ package alias
import (
"context"
+ "errors"
stdpath "path"
"strings"
+ "sync"
+ "time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -11,24 +14,61 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common"
+ log "github.com/sirupsen/logrus"
)
-func (d *Alias) listRoot() []model.Obj {
+func (d *Alias) listRoot(ctx context.Context, withDetails, refresh bool) []model.Obj {
var objs []model.Obj
- for k := range d.pathMap {
+ var wg sync.WaitGroup
+ for _, k := range d.rootOrder {
obj := model.Object{
Name: k,
IsFolder: true,
Modified: d.Modified,
}
+ idx := len(objs)
objs = append(objs, &obj)
+ v := d.pathMap[k]
+ if !withDetails || len(v) != 1 {
+ continue
+ }
+ remoteDriver, err := op.GetStorageByMountPath(v[0])
+ if err != nil {
+ continue
+ }
+ _, ok := remoteDriver.(driver.WithDetails)
+ if !ok {
+ continue
+ }
+ objs[idx] = &model.ObjStorageDetails{
+ Obj: objs[idx],
+ StorageDetailsWithName: model.StorageDetailsWithName{
+ StorageDetails: nil,
+ DriverName: remoteDriver.Config().Name,
+ },
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ c, cancel := context.WithTimeout(ctx, time.Second)
+ defer cancel()
+ details, e := op.GetStorageDetails(c, remoteDriver, refresh)
+ if e != nil {
+ if !errors.Is(e, errs.NotImplement) && !errors.Is(e, errs.StorageNotInit) {
+ log.Errorf("failed get %s storage details: %+v", remoteDriver.GetStorage().MountPath, e)
+ }
+ return
+ }
+ objs[idx].(*model.ObjStorageDetails).StorageDetails = details
+ }()
}
+ wg.Wait()
return objs
}
// do others that not defined in Driver interface
func getPair(path string) (string, string) {
- //path = strings.TrimSpace(path)
+ // path = strings.TrimSpace(path)
if strings.Contains(path, ":") {
pair := strings.SplitN(path, ":", 2)
if !strings.Contains(pair[0], "/") {
diff --git a/drivers/aliyundrive/driver.go b/drivers/aliyundrive/driver.go
index 92df0319..f9093097 100644
--- a/drivers/aliyundrive/driver.go
+++ b/drivers/aliyundrive/driver.go
@@ -45,7 +45,7 @@ func (d *AliDrive) GetAddition() driver.Additional {
func (d *AliDrive) Init(ctx context.Context) error {
// TODO login / refresh token
- //op.MustSaveDriverStorage(d)
+ // op.MustSaveDriverStorage(d)
err := d.refreshToken()
if err != nil {
return err
@@ -171,7 +171,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
Mimetype: streamer.GetMimetype(),
}
const DEFAULT int64 = 10485760
- var count = int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT)))
+ count := int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT)))
partInfoList := make([]base.Json, 0, count)
for i := 1; i <= count; i++ {
@@ -327,6 +327,20 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
return fmt.Errorf("%+v", resp2)
}
+func (d *AliDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ res, err, _ := d.request("https://api.aliyundrive.com/adrive/v1/user/driveCapacityDetails", http.MethodPost, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, nil)
+ if err != nil {
+ return nil, err
+ }
+ used := utils.Json.Get(res, "drive_used_size").ToUint64()
+ total := utils.Json.Get(res, "drive_total_size").ToUint64()
+ return &model.StorageDetails{
+ DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
+ }, nil
+}
+
func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
var resp base.Json
var url string
diff --git a/drivers/baidu_netdisk/driver.go b/drivers/baidu_netdisk/driver.go
index 331cc216..31f35b95 100644
--- a/drivers/baidu_netdisk/driver.go
+++ b/drivers/baidu_netdisk/driver.go
@@ -221,7 +221,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
lastBlockSize = sliceSize
}
- //cal md5 for first 256k data
+ // cal md5 for first 256k data
const SliceSize int64 = 256 * utils.KB
// cal md5
blockList := make([]string, 0, count)
@@ -293,7 +293,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
}
log.Debugf("%+v", precreateResp)
if precreateResp.ReturnType == 2 {
- //rapid upload, since got md5 match from baidu server
+ // rapid upload, since got md5 match from baidu server
// 修复时间,具体原因见 Put 方法注释的 **注意**
precreateResp.File.Ctime = ctime
precreateResp.File.Mtime = mtime
@@ -373,4 +373,12 @@ func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string
return nil
}
+func (d *BaiduNetdisk) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ du, err := d.quota(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{DiskUsage: du}, nil
+}
+
var _ driver.Driver = (*BaiduNetdisk)(nil)
diff --git a/drivers/baidu_netdisk/meta.go b/drivers/baidu_netdisk/meta.go
index 8dd78571..87874f8e 100644
--- a/drivers/baidu_netdisk/meta.go
+++ b/drivers/baidu_netdisk/meta.go
@@ -12,6 +12,8 @@ type Addition struct {
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
DownloadAPI string `json:"download_api" type:"select" options:"official,crack,crack_video" default:"official"`
+ UseOnlineAPI bool `json:"use_online_api" default:"false"`
+ APIAddress string `json:"api_url_address" default:"https://api.oplist.org/baiduyun/renewapi"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
diff --git a/drivers/baidu_netdisk/types.go b/drivers/baidu_netdisk/types.go
index ec8ceabc..0e9ee443 100644
--- a/drivers/baidu_netdisk/types.go
+++ b/drivers/baidu_netdisk/types.go
@@ -189,3 +189,12 @@ type PrecreateResp struct {
// return_type=2
File File `json:"info"`
}
+
+type QuotaResp struct {
+ Errno int `json:"errno"`
+ RequestId int64 `json:"request_id"`
+ Total uint64 `json:"total"`
+ Used uint64 `json:"used"`
+ //Free uint64 `json:"free"`
+ //Expire bool `json:"expire"`
+}
diff --git a/drivers/baidu_netdisk/util.go b/drivers/baidu_netdisk/util.go
index 183de141..a7c930b3 100644
--- a/drivers/baidu_netdisk/util.go
+++ b/drivers/baidu_netdisk/util.go
@@ -1,6 +1,7 @@
package baidu_netdisk
import (
+ "context"
"encoding/hex"
"errors"
"fmt"
@@ -13,6 +14,7 @@ import (
"unicode"
"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/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
@@ -36,15 +38,55 @@ func (d *BaiduNetdisk) refreshToken() error {
}
func (d *BaiduNetdisk) _refreshToken() error {
+ // 使用在线API刷新Token,无需ClientID和ClientSecret
+ if d.UseOnlineAPI && len(d.APIAddress) > 0 {
+ u := d.APIAddress
+ var resp struct {
+ RefreshToken string `json:"refresh_token"`
+ AccessToken string `json:"access_token"`
+ ErrorMessage string `json:"text"`
+ }
+ _, err := base.RestyClient.R().
+ SetHeader("User-Agent", "Mozilla/5.0 (Macintosh; Apple macOS 15_5) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/138.0.0.0 Openlist/425.6.30").
+ SetResult(&resp).
+ SetQueryParams(map[string]string{
+ "refresh_ui": d.RefreshToken,
+ "server_use": "true",
+ "driver_txt": "baiduyun_go",
+ }).
+ Get(u)
+ if err != nil {
+ return err
+ }
+ if resp.RefreshToken == "" || resp.AccessToken == "" {
+ if resp.ErrorMessage != "" {
+ return fmt.Errorf("failed to refresh token: %s", resp.ErrorMessage)
+ }
+ return fmt.Errorf("empty token returned from official API, a wrong refresh token may have been used")
+ }
+ d.AccessToken = resp.AccessToken
+ d.RefreshToken = resp.RefreshToken
+ op.MustSaveDriverStorage(d)
+ return nil
+ }
+ // 使用本地客户端的情况下检查是否为空
+ if d.ClientID == "" || d.ClientSecret == "" {
+ return fmt.Errorf("empty ClientID or ClientSecret")
+ }
+ // 走原有的刷新逻辑
u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp
var e TokenErrResp
- _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
- "grant_type": "refresh_token",
- "refresh_token": d.RefreshToken,
- "client_id": d.ClientID,
- "client_secret": d.ClientSecret,
- }).Get(u)
+ _, err := base.RestyClient.R().
+ SetResult(&resp).
+ SetError(&e).
+ SetQueryParams(map[string]string{
+ "grant_type": "refresh_token",
+ "refresh_token": d.RefreshToken,
+ "client_id": d.ClientID,
+ "client_secret": d.ClientSecret,
+ }).
+ Get(u)
if err != nil {
return err
}
@@ -173,7 +215,7 @@ func (d *BaiduNetdisk) linkOfficial(file model.Obj, _ model.LinkArgs) (*model.Li
if err != nil {
return nil, err
}
- //if res.StatusCode() == 302 {
+ // if res.StatusCode() == 302 {
u = res.Header().Get("location")
//}
@@ -351,6 +393,17 @@ func (d *BaiduNetdisk) getSliceSize(filesize int64) int64 {
return maxSliceSize
}
+func (d *BaiduNetdisk) quota(ctx context.Context) (model.DiskUsage, error) {
+ var resp QuotaResp
+ _, err := d.request("https://pan.baidu.com/api/quota", http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return model.DiskUsage{}, err
+ }
+ return driver.DiskUsageFromUsedAndTotal(resp.Used, resp.Total), nil
+}
+
// func encodeURIComponent(str string) string {
// r := url.QueryEscape(str)
// r = strings.ReplaceAll(r, "+", "%20")
diff --git a/drivers/baidu_photo/meta.go b/drivers/baidu_photo/meta.go
index 91997c9d..6c6ca5c3 100644
--- a/drivers/baidu_photo/meta.go
+++ b/drivers/baidu_photo/meta.go
@@ -18,8 +18,9 @@ type Addition struct {
}
var config = driver.Config{
- Name: "BaiduPhoto",
- LocalSort: true,
+ Name: "BaiduPhoto",
+ LocalSort: true,
+ LinkCacheMode: driver.LinkCacheUA,
}
func init() {
diff --git a/drivers/chaoxing/driver.go b/drivers/chaoxing/driver.go
index cb12b29f..ac9ff115 100644
--- a/drivers/chaoxing/driver.go
+++ b/drivers/chaoxing/driver.go
@@ -10,6 +10,7 @@ import (
"mime/multipart"
"net/http"
"net/url"
+ "strconv"
"strings"
"time"
@@ -239,7 +240,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, file model.FileStr
if err != nil {
return err
}
- err = writer.WriteField("puid", fmt.Sprintf("%d", resp.Msg.Puid))
+ err = writer.WriteField("puid", strconv.Itoa(resp.Msg.Puid))
if err != nil {
fmt.Println("Error writing param2 to request body:", err)
return err
@@ -260,7 +261,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, file model.FileStr
return err
}
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)
if err != nil {
return err
diff --git a/drivers/chaoxing/types.go b/drivers/chaoxing/types.go
index 9e6e270d..ca171c4d 100644
--- a/drivers/chaoxing/types.go
+++ b/drivers/chaoxing/types.go
@@ -258,7 +258,7 @@ type UploadDoneParam struct {
func fileToObj(f File) *model.Object {
if len(f.Content.FolderName) > 0 {
return &model.Object{
- ID: fmt.Sprintf("%d", f.ID),
+ ID: strconv.Itoa(f.ID),
Name: f.Content.FolderName,
Size: 0,
Modified: time.UnixMilli(f.Inserttime),
diff --git a/drivers/chaoxing/util.go b/drivers/chaoxing/util.go
index 715c248a..7e3067ce 100644
--- a/drivers/chaoxing/util.go
+++ b/drivers/chaoxing/util.go
@@ -9,6 +9,7 @@ import (
"fmt"
"mime/multipart"
"net/http"
+ "strconv"
"strings"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
@@ -172,7 +173,7 @@ func (d *ChaoXing) Login() (string, error) {
return "", err
}
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)
if err != nil {
return "", err
diff --git a/drivers/chunk/driver.go b/drivers/chunk/driver.go
new file mode 100644
index 00000000..572f9936
--- /dev/null
+++ b/drivers/chunk/driver.go
@@ -0,0 +1,501 @@
+package chunk
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ stdpath "path"
+ "strconv"
+ "strings"
+
+ "github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/errs"
+ "github.com/OpenListTeam/OpenList/v4/internal/fs"
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
+ "github.com/OpenListTeam/OpenList/v4/internal/op"
+ "github.com/OpenListTeam/OpenList/v4/internal/sign"
+ "github.com/OpenListTeam/OpenList/v4/internal/stream"
+ "github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
+ "github.com/OpenListTeam/OpenList/v4/pkg/http_range"
+ "github.com/OpenListTeam/OpenList/v4/pkg/utils"
+ "github.com/OpenListTeam/OpenList/v4/server/common"
+ "github.com/avast/retry-go"
+)
+
+type Chunk struct {
+ model.Storage
+ Addition
+}
+
+func (d *Chunk) Config() driver.Config {
+ return config
+}
+
+func (d *Chunk) GetAddition() driver.Additional {
+ return &d.Addition
+}
+
+func (d *Chunk) Init(ctx context.Context) error {
+ if d.PartSize <= 0 {
+ return errors.New("part size must be positive")
+ }
+ if len(d.ChunkPrefix) <= 0 {
+ return errors.New("chunk folder prefix must not be empty")
+ }
+ d.RemotePath = utils.FixAndCleanPath(d.RemotePath)
+ return nil
+}
+
+func (d *Chunk) Drop(ctx context.Context) error {
+ return nil
+}
+
+func (d *Chunk) Get(ctx context.Context, path string) (model.Obj, error) {
+ if utils.PathEqual(path, "/") {
+ return &model.Object{
+ Name: "Root",
+ IsFolder: true,
+ Path: "/",
+ }, nil
+ }
+ remoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)
+ if err != nil {
+ return nil, err
+ }
+ remoteActualPath = stdpath.Join(remoteActualPath, path)
+ if remoteObj, err := op.Get(ctx, remoteStorage, remoteActualPath); err == nil {
+ return &model.Object{
+ Path: path,
+ Name: remoteObj.GetName(),
+ Size: remoteObj.GetSize(),
+ Modified: remoteObj.ModTime(),
+ IsFolder: remoteObj.IsDir(),
+ HashInfo: remoteObj.GetHash(),
+ }, nil
+ }
+
+ remoteActualDir, name := stdpath.Split(remoteActualPath)
+ chunkName := d.ChunkPrefix + name
+ chunkObjs, err := op.List(ctx, remoteStorage, stdpath.Join(remoteActualDir, chunkName), model.ListArgs{})
+ if err != nil {
+ return nil, err
+ }
+ var totalSize int64 = 0
+ // 0号块默认为-1 以支持空文件
+ chunkSizes := []int64{-1}
+ h := make(map[*utils.HashType]string)
+ var first model.Obj
+ for _, o := range chunkObjs {
+ if o.IsDir() {
+ continue
+ }
+ if after, ok := strings.CutPrefix(o.GetName(), "hash_"); ok {
+ hn, value, ok := strings.Cut(strings.TrimSuffix(after, d.CustomExt), "_")
+ if ok {
+ ht, ok := utils.GetHashByName(hn)
+ if ok {
+ h[ht] = value
+ }
+ }
+ continue
+ }
+ idx, err := strconv.Atoi(strings.TrimSuffix(o.GetName(), d.CustomExt))
+ if err != nil {
+ continue
+ }
+ totalSize += o.GetSize()
+ if len(chunkSizes) > idx {
+ if idx == 0 {
+ first = o
+ }
+ chunkSizes[idx] = o.GetSize()
+ } else if len(chunkSizes) == idx {
+ chunkSizes = append(chunkSizes, o.GetSize())
+ } else {
+ newChunkSizes := make([]int64, idx+1)
+ copy(newChunkSizes, chunkSizes)
+ chunkSizes = newChunkSizes
+ chunkSizes[idx] = o.GetSize()
+ }
+ }
+ reqDir, _ := stdpath.Split(path)
+ objRes := chunkObject{
+ Object: model.Object{
+ Path: stdpath.Join(reqDir, chunkName),
+ Name: name,
+ Size: totalSize,
+ Modified: first.ModTime(),
+ Ctime: first.CreateTime(),
+ },
+ chunkSizes: chunkSizes,
+ }
+ if len(h) > 0 {
+ objRes.HashInfo = utils.NewHashInfoByMap(h)
+ }
+ return &objRes, nil
+}
+
+func (d *Chunk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+ remoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)
+ if err != nil {
+ return nil, err
+ }
+ remoteActualDir := stdpath.Join(remoteActualPath, dir.GetPath())
+ remoteObjs, err := op.List(ctx, remoteStorage, remoteActualDir, model.ListArgs{
+ ReqPath: args.ReqPath,
+ Refresh: args.Refresh,
+ })
+ if err != nil {
+ return nil, err
+ }
+ result := make([]model.Obj, 0, len(remoteObjs))
+ listG, listCtx := errgroup.NewGroupWithContext(ctx, d.NumListWorkers, retry.Attempts(3))
+ for _, obj := range remoteObjs {
+ if utils.IsCanceled(listCtx) {
+ break
+ }
+ rawName := obj.GetName()
+ if obj.IsDir() {
+ if name, ok := strings.CutPrefix(rawName, d.ChunkPrefix); ok {
+ resultIdx := len(result)
+ result = append(result, nil)
+ listG.Go(func(ctx context.Context) error {
+ chunkObjs, err := op.List(ctx, remoteStorage, stdpath.Join(remoteActualDir, rawName), model.ListArgs{
+ ReqPath: stdpath.Join(args.ReqPath, rawName),
+ Refresh: args.Refresh,
+ })
+ if err != nil {
+ return err
+ }
+ totalSize := int64(0)
+ h := make(map[*utils.HashType]string)
+ first := obj
+ for _, o := range chunkObjs {
+ if o.IsDir() {
+ continue
+ }
+ if after, ok := strings.CutPrefix(strings.TrimSuffix(o.GetName(), d.CustomExt), "hash_"); ok {
+ hn, value, ok := strings.Cut(after, "_")
+ if ok {
+ ht, ok := utils.GetHashByName(hn)
+ if ok {
+ h[ht] = value
+ }
+ continue
+ }
+ }
+ idx, err := strconv.Atoi(strings.TrimSuffix(o.GetName(), d.CustomExt))
+ if err != nil {
+ continue
+ }
+ if idx == 0 {
+ first = o
+ }
+ totalSize += o.GetSize()
+ }
+ objRes := model.Object{
+ Name: name,
+ Size: totalSize,
+ Modified: first.ModTime(),
+ Ctime: first.CreateTime(),
+ }
+ if len(h) > 0 {
+ objRes.HashInfo = utils.NewHashInfoByMap(h)
+ }
+ if !d.Thumbnail {
+ result[resultIdx] = &objRes
+ } else {
+ thumbPath := stdpath.Join(args.ReqPath, ".thumbnails", name+".webp")
+ thumb := fmt.Sprintf("%s/d%s?sign=%s",
+ common.GetApiUrl(ctx),
+ utils.EncodePath(thumbPath, true),
+ sign.Sign(thumbPath))
+ result[resultIdx] = &model.ObjThumb{
+ Object: objRes,
+ Thumbnail: model.Thumbnail{
+ Thumbnail: thumb,
+ },
+ }
+ }
+ return nil
+ })
+ continue
+ }
+ }
+
+ if !d.ShowHidden && strings.HasPrefix(rawName, ".") {
+ continue
+ }
+ thumb, ok := model.GetThumb(obj)
+ objRes := model.Object{
+ Name: rawName,
+ Size: obj.GetSize(),
+ Modified: obj.ModTime(),
+ IsFolder: obj.IsDir(),
+ HashInfo: obj.GetHash(),
+ }
+ if !ok {
+ result = append(result, &objRes)
+ } else {
+ result = append(result, &model.ObjThumb{
+ Object: objRes,
+ Thumbnail: model.Thumbnail{
+ Thumbnail: thumb,
+ },
+ })
+ }
+ }
+ if err = listG.Wait(); err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+func (d *Chunk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+ remoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)
+ if err != nil {
+ return nil, err
+ }
+ chunkFile, ok := file.(*chunkObject)
+ remoteActualPath = stdpath.Join(remoteActualPath, file.GetPath())
+ if !ok {
+ l, _, err := op.Link(ctx, remoteStorage, remoteActualPath, args)
+ if err != nil {
+ return nil, err
+ }
+ resultLink := *l
+ resultLink.SyncClosers = utils.NewSyncClosers(l)
+ return &resultLink, nil
+ }
+ // 检查0号块不等于-1 以支持空文件
+ // 如果块数量大于1 最后一块不可能为0
+ // 只检查中间块是否有0
+ for i, l := 0, len(chunkFile.chunkSizes)-2; ; i++ {
+ if i == 0 {
+ if chunkFile.chunkSizes[i] == -1 {
+ return nil, fmt.Errorf("chunk part[%d] are missing", i)
+ }
+ } else if chunkFile.chunkSizes[i] == 0 {
+ return nil, fmt.Errorf("chunk part[%d] are missing", i)
+ }
+ if i >= l {
+ break
+ }
+ }
+ fileSize := chunkFile.GetSize()
+ mergedRrf := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
+ start := httpRange.Start
+ length := httpRange.Length
+ if length < 0 || start+length > fileSize {
+ length = fileSize - start
+ }
+ if length == 0 {
+ return io.NopCloser(strings.NewReader("")), nil
+ }
+ rs := make([]io.Reader, 0)
+ cs := make(utils.Closers, 0)
+ var (
+ rc io.ReadCloser
+ readFrom bool
+ )
+ for idx, chunkSize := range chunkFile.chunkSizes {
+ if readFrom {
+ l, o, err := op.Link(ctx, remoteStorage, stdpath.Join(remoteActualPath, d.getPartName(idx)), args)
+ if err != nil {
+ _ = cs.Close()
+ return nil, err
+ }
+ cs = append(cs, l)
+ chunkSize2 := l.ContentLength
+ if chunkSize2 <= 0 {
+ chunkSize2 = o.GetSize()
+ }
+ if chunkSize2 != chunkSize {
+ _ = cs.Close()
+ return nil, fmt.Errorf("chunk part[%d] size not match", idx)
+ }
+ rrf, err := stream.GetRangeReaderFromLink(chunkSize2, l)
+ if err != nil {
+ _ = cs.Close()
+ return nil, err
+ }
+ newLength := length - chunkSize2
+ if newLength >= 0 {
+ length = newLength
+ rc, err = rrf.RangeRead(ctx, http_range.Range{Length: -1})
+ } else {
+ rc, err = rrf.RangeRead(ctx, http_range.Range{Length: length})
+ }
+ if err != nil {
+ _ = cs.Close()
+ return nil, err
+ }
+ rs = append(rs, rc)
+ cs = append(cs, rc)
+ if newLength <= 0 {
+ return utils.ReadCloser{
+ Reader: io.MultiReader(rs...),
+ Closer: &cs,
+ }, nil
+ }
+ } else if newStart := start - chunkSize; newStart >= 0 {
+ start = newStart
+ } else {
+ l, o, err := op.Link(ctx, remoteStorage, stdpath.Join(remoteActualPath, d.getPartName(idx)), args)
+ if err != nil {
+ _ = cs.Close()
+ return nil, err
+ }
+ cs = append(cs, l)
+ chunkSize2 := l.ContentLength
+ if chunkSize2 <= 0 {
+ chunkSize2 = o.GetSize()
+ }
+ if chunkSize2 != chunkSize {
+ _ = cs.Close()
+ return nil, fmt.Errorf("chunk part[%d] size not match", idx)
+ }
+ rrf, err := stream.GetRangeReaderFromLink(chunkSize2, l)
+ if err != nil {
+ _ = cs.Close()
+ return nil, err
+ }
+ rc, err = rrf.RangeRead(ctx, http_range.Range{Start: start, Length: -1})
+ if err != nil {
+ _ = cs.Close()
+ return nil, err
+ }
+ length -= chunkSize2 - start
+ cs = append(cs, rc)
+ if length <= 0 {
+ return utils.ReadCloser{
+ Reader: rc,
+ Closer: &cs,
+ }, nil
+ }
+ rs = append(rs, rc)
+ readFrom = true
+ }
+ }
+ return nil, fmt.Errorf("invalid range: start=%d,length=%d,fileSize=%d", httpRange.Start, httpRange.Length, fileSize)
+ }
+ return &model.Link{
+ RangeReader: stream.RangeReaderFunc(mergedRrf),
+ }, nil
+}
+
+func (d *Chunk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
+ path := stdpath.Join(d.RemotePath, parentDir.GetPath(), dirName)
+ return fs.MakeDir(ctx, path)
+}
+
+func (d *Chunk) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
+ src := stdpath.Join(d.RemotePath, srcObj.GetPath())
+ dst := stdpath.Join(d.RemotePath, dstDir.GetPath())
+ _, err := fs.Move(ctx, src, dst)
+ return err
+}
+
+func (d *Chunk) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
+ if _, ok := srcObj.(*chunkObject); ok {
+ newName = d.ChunkPrefix + newName
+ }
+ return fs.Rename(ctx, stdpath.Join(d.RemotePath, srcObj.GetPath()), newName)
+}
+
+func (d *Chunk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
+ dst := stdpath.Join(d.RemotePath, dstDir.GetPath())
+ src := stdpath.Join(d.RemotePath, srcObj.GetPath())
+ _, err := fs.Copy(ctx, src, dst)
+ return err
+}
+
+func (d *Chunk) Remove(ctx context.Context, obj model.Obj) error {
+ return fs.Remove(ctx, stdpath.Join(d.RemotePath, obj.GetPath()))
+}
+
+func (d *Chunk) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
+ remoteStorage, remoteActualPath, err := op.GetStorageAndActualPath(d.RemotePath)
+ if err != nil {
+ return err
+ }
+ if (d.Thumbnail && dstDir.GetName() == ".thumbnails") || (d.ChunkLargeFileOnly && file.GetSize() <= d.PartSize) {
+ return op.Put(ctx, remoteStorage, stdpath.Join(remoteActualPath, dstDir.GetPath()), file, up)
+ }
+ upReader := &driver.ReaderUpdatingProgress{
+ Reader: file,
+ UpdateProgress: up,
+ }
+ dst := stdpath.Join(remoteActualPath, dstDir.GetPath(), d.ChunkPrefix+file.GetName())
+ if d.StoreHash {
+ for ht, value := range file.GetHash().All() {
+ _ = op.Put(ctx, remoteStorage, dst, &stream.FileStream{
+ Obj: &model.Object{
+ Name: fmt.Sprintf("hash_%s_%s%s", ht.Name, value, d.CustomExt),
+ Size: 1,
+ Modified: file.ModTime(),
+ },
+ Mimetype: "application/octet-stream",
+ Reader: bytes.NewReader([]byte{0}), // 兼容不支持空文件的驱动
+ }, nil, true)
+ }
+ }
+ fullPartCount := int(file.GetSize() / d.PartSize)
+ tailSize := file.GetSize() % d.PartSize
+ if tailSize == 0 && fullPartCount > 0 {
+ fullPartCount--
+ tailSize = d.PartSize
+ }
+ partIndex := 0
+ for partIndex < fullPartCount {
+ err = op.Put(ctx, remoteStorage, dst, &stream.FileStream{
+ Obj: &model.Object{
+ Name: d.getPartName(partIndex),
+ Size: d.PartSize,
+ Modified: file.ModTime(),
+ },
+ Mimetype: file.GetMimetype(),
+ Reader: io.LimitReader(upReader, d.PartSize),
+ }, nil, true)
+ if err != nil {
+ _ = op.Remove(ctx, remoteStorage, dst)
+ return err
+ }
+ partIndex++
+ }
+ err = op.Put(ctx, remoteStorage, dst, &stream.FileStream{
+ Obj: &model.Object{
+ Name: d.getPartName(fullPartCount),
+ Size: tailSize,
+ Modified: file.ModTime(),
+ },
+ Mimetype: file.GetMimetype(),
+ Reader: upReader,
+ }, nil)
+ if err != nil {
+ _ = op.Remove(ctx, remoteStorage, dst)
+ }
+ return err
+}
+
+func (d *Chunk) getPartName(part int) string {
+ return fmt.Sprintf("%d%s", part, d.CustomExt)
+}
+
+func (d *Chunk) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ remoteStorage, err := fs.GetStorage(d.RemotePath, &fs.GetStoragesArgs{})
+ if err != nil {
+ return nil, errs.NotImplement
+ }
+ remoteDetails, err := op.GetStorageDetails(ctx, remoteStorage)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: remoteDetails.DiskUsage,
+ }, nil
+}
+
+var _ driver.Driver = (*Chunk)(nil)
diff --git a/drivers/chunk/meta.go b/drivers/chunk/meta.go
new file mode 100644
index 00000000..43e24c89
--- /dev/null
+++ b/drivers/chunk/meta.go
@@ -0,0 +1,39 @@
+package chunk
+
+import (
+ "github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/op"
+)
+
+type Addition struct {
+ RemotePath string `json:"remote_path" required:"true"`
+ PartSize int64 `json:"part_size" required:"true" type:"number" help:"bytes"`
+ ChunkLargeFileOnly bool `json:"chunk_large_file_only" default:"false" help:"chunk only if file size > part_size"`
+ ChunkPrefix string `json:"chunk_prefix" type:"string" default:"[openlist_chunk]" help:"the prefix of chunk folder"`
+ CustomExt string `json:"custom_ext" type:"string"`
+ StoreHash bool `json:"store_hash" type:"bool" default:"true"`
+ NumListWorkers int `json:"num_list_workers" required:"true" type:"number" default:"5"`
+
+ Thumbnail bool `json:"thumbnail" required:"true" default:"false" help:"enable thumbnail which pre-generated under .thumbnails folder"`
+ ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
+}
+
+var config = driver.Config{
+ Name: "Chunk",
+ LocalSort: true,
+ OnlyProxy: true,
+ NoCache: true,
+ DefaultRoot: "/",
+ NoLinkURL: true,
+}
+
+func init() {
+ op.RegisterDriver(func() driver.Driver {
+ return &Chunk{
+ Addition: Addition{
+ ChunkPrefix: "[openlist_chunk]",
+ NumListWorkers: 5,
+ },
+ }
+ })
+}
diff --git a/drivers/chunk/obj.go b/drivers/chunk/obj.go
new file mode 100644
index 00000000..1885a925
--- /dev/null
+++ b/drivers/chunk/obj.go
@@ -0,0 +1,8 @@
+package chunk
+
+import "github.com/OpenListTeam/OpenList/v4/internal/model"
+
+type chunkObject struct {
+ model.Object
+ chunkSizes []int64
+}
diff --git a/drivers/cloudreve_v4/driver.go b/drivers/cloudreve_v4/driver.go
index 11a7e92d..c16309e4 100644
--- a/drivers/cloudreve_v4/driver.go
+++ b/drivers/cloudreve_v4/driver.go
@@ -20,7 +20,9 @@ import (
type CloudreveV4 struct {
model.Storage
Addition
- ref *CloudreveV4
+ ref *CloudreveV4
+ AccessExpires string
+ RefreshExpires string
}
func (d *CloudreveV4) Config() driver.Config {
@@ -44,13 +46,17 @@ func (d *CloudreveV4) Init(ctx context.Context) error {
if d.ref != nil {
return nil
}
- if d.AccessToken == "" && d.RefreshToken != "" {
+ if d.canLogin() {
+ return d.login()
+ }
+ if d.RefreshToken != "" {
return d.refreshToken()
}
- if d.Username != "" {
- return d.login()
+ if d.AccessToken == "" {
+ return errors.New("no way to authenticate. At least AccessToken is required")
}
- return nil
+ // ensure AccessToken is valid
+ return d.parseJWT(d.AccessToken, &AccessJWT{})
}
func (d *CloudreveV4) InitReference(storage driver.Driver) error {
@@ -333,6 +339,20 @@ func (d *CloudreveV4) ArchiveDecompress(ctx context.Context, srcObj, dstDir mode
return nil, errs.NotImplement
}
+func (d *CloudreveV4) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ // TODO return storage details (total space, free space, etc.)
+ var r CapacityResp
+ err := d.request(http.MethodGet, "/user/capacity", func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &r)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: driver.DiskUsageFromUsedAndTotal(r.Used, r.Total),
+ }, nil
+}
+
//func (d *CloudreveV4) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
diff --git a/drivers/cloudreve_v4/types.go b/drivers/cloudreve_v4/types.go
index 455f0a95..a10f9fe1 100644
--- a/drivers/cloudreve_v4/types.go
+++ b/drivers/cloudreve_v4/types.go
@@ -66,11 +66,27 @@ type CaptchaResp struct {
Ticket string `json:"ticket"`
}
+type AccessJWT struct {
+ TokenType string `json:"token_type"`
+ Sub string `json:"sub"`
+ Exp int64 `json:"exp"`
+ Nbf int64 `json:"nbf"`
+}
+
+type RefreshJWT struct {
+ TokenType string `json:"token_type"`
+ Sub string `json:"sub"`
+ Exp int `json:"exp"`
+ Nbf int `json:"nbf"`
+ StateHash string `json:"state_hash"`
+ RootTokenID string `json:"root_token_id"`
+}
+
type Token struct {
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
- AccessExpires time.Time `json:"access_expires"`
- RefreshExpires time.Time `json:"refresh_expires"`
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ AccessExpires string `json:"access_expires"`
+ RefreshExpires string `json:"refresh_expires"`
}
type TokenResponse struct {
@@ -188,3 +204,9 @@ type FolderSummaryResp struct {
CalculatedAt time.Time `json:"calculated_at"`
} `json:"folder_summary"`
}
+
+type CapacityResp struct {
+ Total uint64 `json:"total"`
+ Used uint64 `json:"used"`
+ // StoragePackTotal uint64 `json:"storage_pack_total"`
+}
diff --git a/drivers/cloudreve_v4/util.go b/drivers/cloudreve_v4/util.go
index fc03ee7a..200dfbb6 100644
--- a/drivers/cloudreve_v4/util.go
+++ b/drivers/cloudreve_v4/util.go
@@ -28,6 +28,15 @@ import (
// do others that not defined in Driver interface
+const (
+ CodeLoginRequired = http.StatusUnauthorized
+ CodeCredentialInvalid = 40020 // Failed to issue token
+)
+
+var (
+ ErrorIssueToken = errors.New("failed to issue token")
+)
+
func (d *CloudreveV4) getUA() string {
if d.CustomUA != "" {
return d.CustomUA
@@ -39,6 +48,23 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
if d.ref != nil {
return d.ref.request(method, path, callback, out)
}
+
+ // ensure token
+ if d.isTokenExpired() {
+ err := d.refreshToken()
+ if err != nil {
+ return err
+ }
+ }
+
+ return d._request(method, path, callback, out)
+}
+
+func (d *CloudreveV4) _request(method string, path string, callback base.ReqCallback, out any) error {
+ if d.ref != nil {
+ return d.ref._request(method, path, callback, out)
+ }
+
u := d.Address + "/api/v4" + path
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
@@ -65,15 +91,17 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
}
if r.Code != 0 {
- if r.Code == 401 && d.RefreshToken != "" && path != "/session/token/refresh" {
- // try to refresh token
- err = d.refreshToken()
+ if r.Code == CodeLoginRequired && d.canLogin() && path != "/session/token/refresh" {
+ err = d.login()
if err != nil {
return err
}
return d.request(method, path, callback, out)
}
- return errors.New(r.Msg)
+ if r.Code == CodeCredentialInvalid {
+ return ErrorIssueToken
+ }
+ return fmt.Errorf("%d: %s", r.Code, r.Msg)
}
if out != nil && r.Data != nil {
@@ -91,14 +119,18 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
return nil
}
+func (d *CloudreveV4) canLogin() bool {
+ return d.Username != "" && d.Password != ""
+}
+
func (d *CloudreveV4) login() error {
var siteConfig SiteLoginConfigResp
- err := d.request(http.MethodGet, "/site/config/login", nil, &siteConfig)
+ err := d._request(http.MethodGet, "/site/config/login", nil, &siteConfig)
if err != nil {
return err
}
var prepareLogin PrepareLoginResp
- err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
+ err = d._request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
if err != nil {
return err
}
@@ -128,7 +160,7 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
}
if needCaptcha {
var config BasicConfigResp
- err = d.request(http.MethodGet, "/site/config/basic", nil, &config)
+ err = d._request(http.MethodGet, "/site/config/basic", nil, &config)
if err != nil {
return err
}
@@ -136,7 +168,7 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
return fmt.Errorf("captcha type %s not support", config.CaptchaType)
}
var captcha CaptchaResp
- err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
+ err = d._request(http.MethodGet, "/site/captcha", nil, &captcha)
if err != nil {
return err
}
@@ -162,20 +194,22 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
loginBody["captcha"] = captchaCode
}
var token TokenResponse
- err = d.request(http.MethodPost, "/session/token", func(req *resty.Request) {
+ err = d._request(http.MethodPost, "/session/token", func(req *resty.Request) {
req.SetBody(loginBody)
}, &token)
if err != nil {
return err
}
d.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken
+ d.AccessExpires, d.RefreshExpires = token.Token.AccessExpires, token.Token.RefreshExpires
op.MustSaveDriverStorage(d)
return nil
}
func (d *CloudreveV4) refreshToken() error {
+ // if no refresh token, try to login if possible
if d.RefreshToken == "" {
- if d.Username != "" {
+ if d.canLogin() {
err := d.login()
if err != nil {
return fmt.Errorf("cannot login to get refresh token, error: %s", err)
@@ -183,20 +217,127 @@ func (d *CloudreveV4) refreshToken() error {
}
return nil
}
+
+ // parse jwt to check if refresh token is valid
+ var jwt RefreshJWT
+ err := d.parseJWT(d.RefreshToken, &jwt)
+ if err != nil {
+ // if refresh token is invalid, try to login if possible
+ if d.canLogin() {
+ return d.login()
+ }
+ d.GetStorage().SetStatus(fmt.Sprintf("Invalid RefreshToken: %s", err.Error()))
+ op.MustSaveDriverStorage(d)
+ return fmt.Errorf("invalid refresh token: %w", err)
+ }
+
+ // do refresh token
var token Token
- err := d.request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
+ err = d._request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
req.SetBody(base.Json{
"refresh_token": d.RefreshToken,
})
}, &token)
if err != nil {
+ if errors.Is(err, ErrorIssueToken) {
+ if d.canLogin() {
+ // try to login again
+ return d.login()
+ }
+ d.GetStorage().SetStatus("This session is no longer valid")
+ op.MustSaveDriverStorage(d)
+ return ErrorIssueToken
+ }
return err
}
d.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken
+ d.AccessExpires, d.RefreshExpires = token.AccessExpires, token.RefreshExpires
op.MustSaveDriverStorage(d)
return nil
}
+func (d *CloudreveV4) parseJWT(token string, jwt any) error {
+ split := strings.Split(token, ".")
+ if len(split) != 3 {
+ return fmt.Errorf("invalid token length: %d, ensure the token is a valid JWT", len(split))
+ }
+ data, err := base64.RawURLEncoding.DecodeString(split[1])
+ if err != nil {
+ return fmt.Errorf("invalid token encoding: %w, ensure the token is a valid JWT", err)
+ }
+ err = json.Unmarshal(data, &jwt)
+ if err != nil {
+ return fmt.Errorf("invalid token content: %w, ensure the token is a valid JWT", err)
+ }
+ return nil
+}
+
+// check if token is expired
+// https://github.com/cloudreve/frontend/blob/ddfacc1c31c49be03beb71de4cc114c8811038d6/src/session/index.ts#L177-L200
+func (d *CloudreveV4) isTokenExpired() bool {
+ if d.RefreshToken == "" {
+ // login again if username and password is set
+ if d.canLogin() {
+ return true
+ }
+ // no refresh token, cannot refresh
+ return false
+ }
+ if d.AccessToken == "" {
+ return true
+ }
+ var (
+ err error
+ expires time.Time
+ )
+ // check if token is expired
+ if d.AccessExpires != "" {
+ // use expires field if possible to prevent timezone issue
+ // only available after login or refresh token
+ // 2025-08-28T02:43:07.645109985+08:00
+ expires, err = time.Parse(time.RFC3339Nano, d.AccessExpires)
+ if err != nil {
+ return false
+ }
+ } else {
+ // fallback to parse jwt
+ // if failed, disable the storage
+ var jwt AccessJWT
+ err = d.parseJWT(d.AccessToken, &jwt)
+ if err != nil {
+ d.GetStorage().SetStatus(fmt.Sprintf("Invalid AccessToken: %s", err.Error()))
+ op.MustSaveDriverStorage(d)
+ return false
+ }
+ // may be have timezone issue
+ expires = time.Unix(jwt.Exp, 0)
+ }
+ // add a 10 minutes safe margin
+ ddl := time.Now().Add(10 * time.Minute)
+ if expires.Before(ddl) {
+ // current access token expired, check if refresh token is expired
+ // warning: cannot parse refresh token from jwt, because the exp field is not standard
+ if d.RefreshExpires != "" {
+ refreshExpires, err := time.Parse(time.RFC3339Nano, d.RefreshExpires)
+ if err != nil {
+ return false
+ }
+ if refreshExpires.Before(time.Now()) {
+ // This session is no longer valid
+ if d.canLogin() {
+ // try to login again
+ return true
+ }
+ d.GetStorage().SetStatus("This session is no longer valid")
+ op.MustSaveDriverStorage(d)
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+
func (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
var finish int64 = 0
var chunk int = 0
diff --git a/drivers/cnb_releases/driver.go b/drivers/cnb_releases/driver.go
new file mode 100644
index 00000000..d80e6958
--- /dev/null
+++ b/drivers/cnb_releases/driver.go
@@ -0,0 +1,230 @@
+package cnb_releases
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "time"
+
+ "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/model"
+ "github.com/OpenListTeam/OpenList/v4/pkg/utils"
+ "github.com/go-resty/resty/v2"
+)
+
+type CnbReleases struct {
+ model.Storage
+ Addition
+ ref *CnbReleases
+}
+
+func (d *CnbReleases) Config() driver.Config {
+ return config
+}
+
+func (d *CnbReleases) GetAddition() driver.Additional {
+ return &d.Addition
+}
+
+func (d *CnbReleases) Init(ctx context.Context) error {
+ return nil
+}
+
+func (d *CnbReleases) InitReference(storage driver.Driver) error {
+ refStorage, ok := storage.(*CnbReleases)
+ if ok {
+ d.ref = refStorage
+ return nil
+ }
+ return fmt.Errorf("ref: storage is not CnbReleases")
+}
+
+func (d *CnbReleases) Drop(ctx context.Context) error {
+ d.ref = nil
+ return nil
+}
+
+func (d *CnbReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+ if dir.GetPath() == "/" {
+ // get all releases for root dir
+ var resp ReleaseList
+
+ err := d.Request(http.MethodGet, "/{repo}/-/releases", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return utils.SliceConvert(resp, func(src Release) (model.Obj, error) {
+ name := src.Name
+ if d.UseTagName {
+ name = src.TagName
+ }
+ return &model.Object{
+ ID: src.ID,
+ Name: name,
+ Size: d.sumAssetsSize(src.Assets),
+ Ctime: src.CreatedAt,
+ Modified: src.UpdatedAt,
+ IsFolder: true,
+ }, nil
+ })
+ } else {
+ // get release info by release id
+ releaseID := dir.GetID()
+ if releaseID == "" {
+ return nil, errs.ObjectNotFound
+ }
+ var resp Release
+ err := d.Request(http.MethodGet, "/{repo}/-/releases/{release_id}", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ req.SetPathParam("release_id", releaseID)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return utils.SliceConvert(resp.Assets, func(src ReleaseAsset) (model.Obj, error) {
+ return &Object{
+ Object: model.Object{
+ ID: src.ID,
+ Path: src.Path,
+ Name: src.Name,
+ Size: src.Size,
+ Ctime: src.CreatedAt,
+ Modified: src.UpdatedAt,
+ IsFolder: false,
+ },
+ ParentID: dir.GetID(),
+ }, nil
+ })
+ }
+}
+
+func (d *CnbReleases) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+ return &model.Link{
+ URL: "https://cnb.cool" + file.GetPath(),
+ }, nil
+}
+
+func (d *CnbReleases) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
+ if parentDir.GetPath() == "/" {
+ // create a new release
+ branch := d.DefaultBranch
+ if branch == "" {
+ branch = "main" // fallback to "main" if not set
+ }
+ return d.Request(http.MethodPost, "/{repo}/-/releases", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ req.SetBody(base.Json{
+ "name": dirName,
+ "tag_name": dirName,
+ "target_commitish": branch,
+ })
+ }, nil)
+ }
+ return errs.NotImplement
+}
+
+func (d *CnbReleases) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+ return nil, errs.NotImplement
+}
+
+func (d *CnbReleases) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
+ if srcObj.IsDir() && !d.UseTagName {
+ return d.Request(http.MethodPatch, "/{repo}/-/releases/{release_id}", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ req.SetPathParam("release_id", srcObj.GetID())
+ req.SetFormData(map[string]string{
+ "name": newName,
+ })
+ }, nil)
+ }
+ return errs.NotImplement
+}
+
+func (d *CnbReleases) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+ return nil, errs.NotImplement
+}
+
+func (d *CnbReleases) Remove(ctx context.Context, obj model.Obj) error {
+ if obj.IsDir() {
+ return d.Request(http.MethodDelete, "/{repo}/-/releases/{release_id}", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ req.SetPathParam("release_id", obj.GetID())
+ }, nil)
+ }
+ if o, ok := obj.(*Object); ok {
+ return d.Request(http.MethodDelete, "/{repo}/-/releases/{release_id}/assets/{asset_id}", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ req.SetPathParam("release_id", o.ParentID)
+ req.SetPathParam("asset_id", obj.GetID())
+ }, nil)
+ } else {
+ return fmt.Errorf("unable to get release ID")
+ }
+}
+
+func (d *CnbReleases) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
+ // 1. get upload info
+ var resp ReleaseAssetUploadURL
+ err := d.Request(http.MethodPost, "/{repo}/-/releases/{release_id}/asset-upload-url", func(req *resty.Request) {
+ req.SetPathParam("repo", d.Repo)
+ req.SetPathParam("release_id", dstDir.GetID())
+ req.SetBody(base.Json{
+ "asset_name": file.GetName(),
+ "overwrite": true,
+ "size": file.GetSize(),
+ })
+ }, &resp)
+ if err != nil {
+ return err
+ }
+
+ // 2. upload file
+ // use multipart to create form file
+ var b bytes.Buffer
+ w := multipart.NewWriter(&b)
+ _, err = w.CreateFormFile("file", file.GetName())
+ if err != nil {
+ return err
+ }
+ headSize := b.Len()
+ err = w.Close()
+ if err != nil {
+ return err
+ }
+
+ head := bytes.NewReader(b.Bytes()[:headSize])
+ tail := bytes.NewReader(b.Bytes()[headSize:])
+ rateLimitedRd := driver.NewLimitedUploadStream(ctx, io.MultiReader(head, file, tail))
+
+ // use net/http to upload file
+ ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Duration(resp.ExpiresInSec+1)*time.Second)
+ defer cancel()
+ req, err := http.NewRequestWithContext(ctxWithTimeout, http.MethodPost, resp.UploadURL, rateLimitedRd)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", w.FormDataContentType())
+ req.Header.Set("User-Agent", base.UserAgent)
+ httpResp, err := base.HttpClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer httpResp.Body.Close()
+ if httpResp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("upload file failed: %s", httpResp.Status)
+ }
+
+ // 3. verify upload
+ return d.Request(http.MethodPost, resp.VerifyURL, nil, nil)
+}
+
+var _ driver.Driver = (*CnbReleases)(nil)
diff --git a/drivers/cnb_releases/meta.go b/drivers/cnb_releases/meta.go
new file mode 100644
index 00000000..2894d8a2
--- /dev/null
+++ b/drivers/cnb_releases/meta.go
@@ -0,0 +1,26 @@
+package cnb_releases
+
+import (
+ "github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/op"
+)
+
+type Addition struct {
+ driver.RootPath
+ Repo string `json:"repo" type:"string" required:"true"`
+ Token string `json:"token" type:"string" required:"true"`
+ UseTagName bool `json:"use_tag_name" type:"bool" default:"false" help:"Use tag name instead of release name"`
+ DefaultBranch string `json:"default_branch" type:"string" default:"main" help:"Default branch for new releases"`
+}
+
+var config = driver.Config{
+ Name: "CNB Releases",
+ LocalSort: true,
+ DefaultRoot: "/",
+}
+
+func init() {
+ op.RegisterDriver(func() driver.Driver {
+ return &CnbReleases{}
+ })
+}
diff --git a/drivers/cnb_releases/types.go b/drivers/cnb_releases/types.go
new file mode 100644
index 00000000..a89ddbf6
--- /dev/null
+++ b/drivers/cnb_releases/types.go
@@ -0,0 +1,100 @@
+package cnb_releases
+
+import (
+ "time"
+
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
+)
+
+type Object struct {
+ model.Object
+ ParentID string
+}
+
+type TagList []Tag
+
+type Tag struct {
+ Commit struct {
+ Author UserInfo `json:"author"`
+ Commit CommitObject `json:"commit"`
+ Committer UserInfo `json:"committer"`
+ Parents []CommitParent `json:"parents"`
+ Sha string `json:"sha"`
+ } `json:"commit"`
+ Name string `json:"name"`
+ Target string `json:"target"`
+ TargetType string `json:"target_type"`
+ Verification TagObjectVerification `json:"verification"`
+}
+
+type UserInfo struct {
+ Freeze bool `json:"freeze"`
+ Nickname string `json:"nickname"`
+ Username string `json:"username"`
+}
+
+type CommitObject struct {
+ Author Signature `json:"author"`
+ CommentCount int `json:"comment_count"`
+ Committer Signature `json:"committer"`
+ Message string `json:"message"`
+ Tree CommitObjectTree `json:"tree"`
+ Verification CommitObjectVerification `json:"verification"`
+}
+
+type Signature struct {
+ Date time.Time `json:"date"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+}
+
+type CommitObjectTree struct {
+ Sha string `json:"sha"`
+}
+
+type CommitObjectVerification struct {
+ Payload string `json:"payload"`
+ Reason string `json:"reason"`
+ Signature string `json:"signature"`
+ Verified bool `json:"verified"`
+ VerifiedAt string `json:"verified_at"`
+}
+
+type CommitParent = CommitObjectTree
+
+type TagObjectVerification = CommitObjectVerification
+
+type ReleaseList []Release
+
+type Release struct {
+ Assets []ReleaseAsset `json:"assets"`
+ Author UserInfo `json:"author"`
+ Body string `json:"body"`
+ CreatedAt time.Time `json:"created_at"`
+ Draft bool `json:"draft"`
+ ID string `json:"id"`
+ IsLatest bool `json:"is_latest"`
+ Name string `json:"name"`
+ Prerelease bool `json:"prerelease"`
+ PublishedAt time.Time `json:"published_at"`
+ TagCommitish string `json:"tag_commitish"`
+ TagName string `json:"tag_name"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+type ReleaseAsset struct {
+ ContentType string `json:"content_type"`
+ CreatedAt time.Time `json:"created_at"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Size int64 `json:"size"`
+ UpdatedAt time.Time `json:"updated_at"`
+ Uploader UserInfo `json:"uploader"`
+}
+
+type ReleaseAssetUploadURL struct {
+ UploadURL string `json:"upload_url"`
+ ExpiresInSec int `json:"expires_in_sec"`
+ VerifyURL string `json:"verify_url"`
+}
diff --git a/drivers/cnb_releases/util.go b/drivers/cnb_releases/util.go
new file mode 100644
index 00000000..83f857a4
--- /dev/null
+++ b/drivers/cnb_releases/util.go
@@ -0,0 +1,58 @@
+package cnb_releases
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/OpenListTeam/OpenList/v4/drivers/base"
+ log "github.com/sirupsen/logrus"
+)
+
+// do others that not defined in Driver interface
+
+func (d *CnbReleases) Request(method string, path string, callback base.ReqCallback, resp any) error {
+ if d.ref != nil {
+ return d.ref.Request(method, path, callback, resp)
+ }
+ var url string
+ if strings.HasPrefix(path, "http") {
+ url = path
+ } else {
+ url = "https://api.cnb.cool" + path
+ }
+ req := base.RestyClient.R()
+ req.SetHeader("Accept", "application/json")
+ req.SetAuthScheme("Bearer")
+ req.SetAuthToken(d.Token)
+
+ if callback != nil {
+ callback(req)
+ }
+ res, err := req.Execute(method, url)
+ log.Debugln(res.String())
+ if err != nil {
+ return err
+ }
+ if res.StatusCode() != http.StatusOK && res.StatusCode() != http.StatusCreated && res.StatusCode() != http.StatusNoContent {
+ return fmt.Errorf("failed to request %s, status code: %d, message: %s", url, res.StatusCode(), res.String())
+ }
+
+ if resp != nil {
+ err = json.Unmarshal(res.Body(), resp)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (d *CnbReleases) sumAssetsSize(assets []ReleaseAsset) int64 {
+ var size int64
+ for _, asset := range assets {
+ size += asset.Size
+ }
+ return size
+}
diff --git a/drivers/crypt/driver.go b/drivers/crypt/driver.go
index 704c70cb..1398ff1c 100644
--- a/drivers/crypt/driver.go
+++ b/drivers/crypt/driver.go
@@ -44,7 +44,7 @@ func (d *Crypt) GetAddition() driver.Additional {
}
func (d *Crypt) Init(ctx context.Context) error {
- //obfuscate credentials if it's updated or just created
+ // obfuscate credentials if it's updated or just created
err := d.updateObfusParm(&d.Password)
if err != nil {
return fmt.Errorf("failed to obfuscate password: %w", err)
@@ -63,7 +63,7 @@ func (d *Crypt) Init(ctx context.Context) error {
op.MustSaveDriverStorage(d)
- //need remote storage exist
+ // need remote storage exist
storage, err := fs.GetStorage(d.RemotePath, &fs.GetStoragesArgs{})
if err != nil {
return fmt.Errorf("can't find remote storage: %w", err)
@@ -109,8 +109,8 @@ func (d *Crypt) Drop(ctx context.Context) error {
func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
path := dir.GetPath()
- //return d.list(ctx, d.RemotePath, path)
- //remoteFull
+ // return d.list(ctx, d.RemotePath, path)
+ // remoteFull
objs, err := fs.List(ctx, d.getPathForRemote(path, true), &fs.ListArgs{NoLog: true, Refresh: args.Refresh})
// the obj must implement the model.SetPath interface
@@ -124,7 +124,7 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
if obj.IsDir() {
name, err := d.cipher.DecryptDirName(obj.GetName())
if err != nil {
- //filter illegal files
+ // filter illegal files
continue
}
if !d.ShowHidden && strings.HasPrefix(name, ".") {
@@ -143,12 +143,12 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
thumb, ok := model.GetThumb(obj)
size, err := d.cipher.DecryptedSize(obj.GetSize())
if err != nil {
- //filter illegal files
+ // filter illegal files
continue
}
name, err := d.cipher.DecryptFileName(obj.GetName())
if err != nil {
- //filter illegal files
+ // filter illegal files
continue
}
if !d.ShowHidden && strings.HasPrefix(name, ".") {
@@ -202,7 +202,7 @@ func (d *Crypt) Get(ctx context.Context, path string) (model.Obj, error) {
remoteObj, err = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})
if err != nil {
if errs.IsObjectNotFound(err) && secondTry {
- //try the opposite
+ // try the opposite
remoteFullPath = d.getPathForRemote(path, !firstTryIsFolder)
remoteObj, err2 = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true})
if err2 != nil {
@@ -240,7 +240,7 @@ func (d *Crypt) Get(ctx context.Context, path string) (model.Obj, error) {
IsFolder: remoteObj.IsDir(),
}
return obj, nil
- //return nil, errs.ObjectNotFound
+ // return nil, errs.ObjectNotFound
}
// https://github.com/rclone/rclone/blob/v1.67.0/backend/crypt/cipher.go#L37
@@ -317,7 +317,8 @@ func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
}
return readSeeker, nil
}),
- SyncClosers: utils.NewSyncClosers(remoteLink),
+ SyncClosers: utils.NewSyncClosers(remoteLink),
+ RequireReference: remoteLink.RequireReference,
}, nil
}
@@ -366,7 +367,6 @@ func (d *Crypt) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return fmt.Errorf("failed to convert path to remote path: %w", err)
}
return op.Copy(ctx, d.remoteStorage, srcRemoteActualPath, dstRemoteActualPath)
-
}
func (d *Crypt) Remove(ctx context.Context, obj model.Obj) error {
@@ -411,6 +411,16 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
return nil
}
+func (d *Crypt) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ remoteDetails, err := op.GetStorageDetails(ctx, d.remoteStorage)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: remoteDetails.DiskUsage,
+ }, nil
+}
+
//func (d *Safe) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
diff --git a/drivers/doubao/util.go b/drivers/doubao/util.go
index 325c16c5..68660897 100644
--- a/drivers/doubao/util.go
+++ b/drivers/doubao/util.go
@@ -486,7 +486,7 @@ func (d *Doubao) Upload(ctx context.Context, config *UploadConfig, dstDir model.
"Authorization": {storeInfo.Auth},
"Content-Type": {"application/octet-stream"},
"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))},
}
res, err := base.HttpClient.Do(req)
@@ -577,7 +577,7 @@ func (d *Doubao) UploadByMultipart(ctx context.Context, config *UploadConfig, fi
if partIndex == totalParts-1 {
size = fileSize - offset
}
- var reader *stream.SectionReader
+ var reader io.ReadSeeker
var rateLimitedRd io.Reader
crc32Value := ""
threadG.GoWithLifecycle(errgroup.Lifecycle{
@@ -612,7 +612,7 @@ func (d *Doubao) UploadByMultipart(ctx context.Context, config *UploadConfig, fi
"Authorization": {storeInfo.Auth},
"Content-Type": {"application/octet-stream"},
"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))},
}
res, err := base.HttpClient.Do(req)
diff --git a/drivers/febbox/meta.go b/drivers/febbox/meta.go
index e449ad00..cb6ff8fb 100644
--- a/drivers/febbox/meta.go
+++ b/drivers/febbox/meta.go
@@ -16,9 +16,10 @@ type Addition struct {
}
var config = driver.Config{
- Name: "FebBox",
- NoUpload: true,
- DefaultRoot: "0",
+ Name: "FebBox",
+ NoUpload: true,
+ DefaultRoot: "0",
+ LinkCacheMode: driver.LinkCacheIP,
}
func init() {
diff --git a/drivers/ftp/meta.go b/drivers/ftp/meta.go
index 8f30776c..0ec0e735 100644
--- a/drivers/ftp/meta.go
+++ b/drivers/ftp/meta.go
@@ -31,11 +31,11 @@ type Addition struct {
}
var config = driver.Config{
- Name: "FTP",
- LocalSort: true,
- OnlyLinkMFile: false,
- DefaultRoot: "/",
- NoLinkURL: true,
+ Name: "FTP",
+ LocalSort: true,
+ OnlyProxy: true,
+ DefaultRoot: "/",
+ NoLinkURL: true,
}
func init() {
diff --git a/drivers/google_drive/driver.go b/drivers/google_drive/driver.go
index c4dd01af..b4336312 100644
--- a/drivers/google_drive/driver.go
+++ b/drivers/google_drive/driver.go
@@ -167,4 +167,30 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return err
}
+func (d *GoogleDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ if d.DisableDiskUsage {
+ return nil, errs.NotImplement
+ }
+ about, err := d.getAbout(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var total, used uint64
+ if about.StorageQuota.Limit == nil {
+ total = 0
+ } else {
+ total, err = strconv.ParseUint(*about.StorageQuota.Limit, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ }
+ used, err = strconv.ParseUint(about.StorageQuota.Usage, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
+ }, nil
+}
+
var _ driver.Driver = (*GoogleDrive)(nil)
diff --git a/drivers/google_drive/meta.go b/drivers/google_drive/meta.go
index 34eeac28..75263a12 100644
--- a/drivers/google_drive/meta.go
+++ b/drivers/google_drive/meta.go
@@ -7,14 +7,15 @@ import (
type Addition struct {
driver.RootID
- RefreshToken string `json:"refresh_token" required:"true"`
- OrderBy string `json:"order_by" type:"string" help:"such as: folder,name,modifiedTime"`
- OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
- UseOnlineAPI bool `json:"use_online_api" default:"true"`
- APIAddress string `json:"api_url_address" default:"https://api.oplist.org/googleui/renewapi"`
- ClientID string `json:"client_id"`
- ClientSecret string `json:"client_secret"`
- ChunkSize int64 `json:"chunk_size" type:"number" default:"5" help:"chunk size while uploading (unit: MB)"`
+ RefreshToken string `json:"refresh_token" required:"true"`
+ OrderBy string `json:"order_by" type:"string" help:"such as: folder,name,modifiedTime"`
+ OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
+ UseOnlineAPI bool `json:"use_online_api" default:"true"`
+ APIAddress string `json:"api_url_address" default:"https://api.oplist.org/googleui/renewapi"`
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+ ChunkSize int64 `json:"chunk_size" type:"number" default:"5" help:"chunk size while uploading (unit: MB)"`
+ DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
}
var config = driver.Config{
diff --git a/drivers/google_drive/types.go b/drivers/google_drive/types.go
index df8bddb9..5e71f305 100644
--- a/drivers/google_drive/types.go
+++ b/drivers/google_drive/types.go
@@ -78,3 +78,12 @@ type Error struct {
Message string `json:"message"`
} `json:"error"`
}
+
+type AboutResp struct {
+ StorageQuota struct {
+ Limit *string `json:"limit"`
+ Usage string `json:"usage"`
+ UsageInDrive string `json:"usageInDrive"`
+ UsageInDriveTrash string `json:"usageInDriveTrash"`
+ }
+}
diff --git a/drivers/google_drive/util.go b/drivers/google_drive/util.go
index ff4bb7b9..372b6254 100644
--- a/drivers/google_drive/util.go
+++ b/drivers/google_drive/util.go
@@ -28,16 +28,16 @@ import (
// do others that not defined in Driver interface
type googleDriveServiceAccount struct {
- //Type string `json:"type"`
- //ProjectID string `json:"project_id"`
- //PrivateKeyID string `json:"private_key_id"`
+ // Type string `json:"type"`
+ // ProjectID string `json:"project_id"`
+ // PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEMail string `json:"client_email"`
- //ClientID string `json:"client_id"`
- //AuthURI string `json:"auth_uri"`
+ // ClientID string `json:"client_id"`
+ // AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
- //AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
- //ClientX509CertURL string `json:"client_x509_cert_url"`
+ // AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
+ // ClientX509CertURL string `json:"client_x509_cert_url"`
}
func (d *GoogleDrive) refreshToken() error {
@@ -255,7 +255,7 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
}
func (d *GoogleDrive) chunkUpload(ctx context.Context, file model.FileStreamer, url string, up driver.UpdateProgress) error {
- var defaultChunkSize = d.ChunkSize * 1024 * 1024
+ defaultChunkSize := d.ChunkSize * 1024 * 1024
ss, err := stream.NewStreamSectionReader(file, int(defaultChunkSize), &up)
if err != nil {
return err
@@ -315,3 +315,18 @@ func (d *GoogleDrive) chunkUpload(ctx context.Context, file model.FileStreamer,
}
return nil
}
+
+func (d *GoogleDrive) getAbout(ctx context.Context) (*AboutResp, error) {
+ query := map[string]string{
+ "fields": "storageQuota",
+ }
+ var resp AboutResp
+ _, err := d.request("https://www.googleapis.com/drive/v3/about", http.MethodGet, func(req *resty.Request) {
+ req.SetQueryParams(query)
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/halalcloud_open/common.go b/drivers/halalcloud_open/common.go
new file mode 100644
index 00000000..575ccff3
--- /dev/null
+++ b/drivers/halalcloud_open/common.go
@@ -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{},
+ }
+}
diff --git a/drivers/halalcloud_open/driver.go b/drivers/halalcloud_open/driver.go
new file mode 100644
index 00000000..6a74538d
--- /dev/null
+++ b/drivers/halalcloud_open/driver.go
@@ -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)
diff --git a/drivers/halalcloud_open/driver_curd_impl.go b/drivers/halalcloud_open/driver_curd_impl.go
new file mode 100644
index 00000000..48dfaf50
--- /dev/null
+++ b/drivers/halalcloud_open/driver_curd_impl.go
@@ -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
+}
diff --git a/drivers/halalcloud_open/driver_get_link.go b/drivers/halalcloud_open/driver_get_link.go
new file mode 100644
index 00000000..cecc46b3
--- /dev/null
+++ b/drivers/halalcloud_open/driver_get_link.go
@@ -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
+}
diff --git a/drivers/halalcloud_open/driver_init.go b/drivers/halalcloud_open/driver_init.go
new file mode 100644
index 00000000..9f702638
--- /dev/null
+++ b/drivers/halalcloud_open/driver_init.go
@@ -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
+}
diff --git a/drivers/halalcloud_open/driver_interface.go b/drivers/halalcloud_open/driver_interface.go
new file mode 100644
index 00000000..0ddf248b
--- /dev/null
+++ b/drivers/halalcloud_open/driver_interface.go
@@ -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)
+}
diff --git a/drivers/halalcloud_open/halalcloud_upload.go b/drivers/halalcloud_open/halalcloud_upload.go
new file mode 100644
index 00000000..f5d173f1
--- /dev/null
+++ b/drivers/halalcloud_open/halalcloud_upload.go
@@ -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
+}
diff --git a/drivers/halalcloud_open/meta.go b/drivers/halalcloud_open/meta.go
new file mode 100644
index 00000000..9d5b16e7
--- /dev/null
+++ b/drivers/halalcloud_open/meta.go
@@ -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{}
+ })
+}
diff --git a/drivers/halalcloud_open/obj_file.go b/drivers/halalcloud_open/obj_file.go
new file mode 100644
index 00000000..839e9f4c
--- /dev/null
+++ b/drivers/halalcloud_open/obj_file.go
@@ -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
+}
diff --git a/drivers/halalcloud_open/utils.go b/drivers/halalcloud_open/utils.go
new file mode 100644
index 00000000..2cb6706a
--- /dev/null
+++ b/drivers/halalcloud_open/utils.go
@@ -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
+
+}
diff --git a/drivers/ilanzou/driver.go b/drivers/ilanzou/driver.go
index 881c59ed..2335cecd 100644
--- a/drivers/ilanzou/driver.go
+++ b/drivers/ilanzou/driver.go
@@ -97,13 +97,13 @@ func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs)
}
obj := model.Object{
ID: strconv.FormatInt(f.FileId, 10),
- //Path: "",
+ // Path: "",
Name: f.FileName,
Size: f.FileSize * 1024,
Modified: updTime,
Ctime: updTime,
IsFolder: false,
- //HashInfo: utils.HashInfo{},
+ // HashInfo: utils.HashInfo{},
}
if f.FileType == 2 {
obj.IsFolder = true
@@ -185,13 +185,13 @@ func (d *ILanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
}
return &model.Object{
ID: utils.Json.Get(res, "list", 0, "id").ToString(),
- //Path: "",
+ // Path: "",
Name: dirName,
Size: 0,
Modified: time.Now(),
Ctime: time.Now(),
IsFolder: true,
- //HashInfo: utils.HashInfo{},
+ // HashInfo: utils.HashInfo{},
}, nil
}
@@ -239,7 +239,7 @@ func (d *ILanZou) Rename(ctx context.Context, srcObj model.Obj, newName string)
}
return &model.Object{
ID: srcObj.GetID(),
- //Path: "",
+ // Path: "",
Name: newName,
Size: srcObj.GetSize(),
Modified: time.Now(),
@@ -392,7 +392,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, s model.FileStreame
}
return &model.Object{
ID: strconv.FormatInt(file.FileId, 10),
- //Path: ,
+ // Path: ,
Name: file.FileName,
Size: s.GetSize(),
Modified: s.ModTime(),
@@ -402,6 +402,22 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, s model.FileStreame
}, nil
}
+func (d *ILanZou) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ res, err := d.proved("/user/account/map", http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ })
+ if err != nil {
+ return nil, err
+ }
+ 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
+ return &model.StorageDetails{
+ DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total),
+ }, nil
+}
+
//func (d *ILanZou) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
diff --git a/drivers/lanzou/help.go b/drivers/lanzou/help.go
index 418868bf..930c7248 100644
--- a/drivers/lanzou/help.go
+++ b/drivers/lanzou/help.go
@@ -1,7 +1,8 @@
package lanzou
import (
- "bytes"
+ "encoding/hex"
+ "errors"
"fmt"
"net/http"
"regexp"
@@ -9,8 +10,6 @@ import (
"strings"
"time"
"unicode"
-
- log "github.com/sirupsen/logrus"
)
const DAY time.Duration = 84600000000000
@@ -122,20 +121,26 @@ var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)
// 在页面被过多访问或其他情况下,有时候会先返回一个加密的页面,其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
// 若该页面进行了js加密,则进行解密,计算acw_sc__v2,并加入cookie
-func CalcAcwScV2(html string) (string, error) {
- log.Debugln("acw_sc__v2", html)
- acwScV2s := findAcwScV2Reg.FindStringSubmatch(html)
- if len(acwScV2s) != 2 {
- return "", fmt.Errorf("无法匹配acw_sc__v2")
+func CalcAcwScV2(htmlContent string) (string, error) {
+ matches := findAcwScV2Reg.FindStringSubmatch(htmlContent)
+ if len(matches) != 2 {
+ return "", errors.New("无法匹配到 arg1 参数")
}
- return HexXor(Unbox(acwScV2s[1]), "3000176000856006061501533003690027800375"), nil
+ arg1 := matches[1]
+
+ mask := "3000176000856006061501533003690027800375"
+ result, err := hexXor(unbox(arg1), mask)
+ if err != nil {
+ return "", fmt.Errorf("hexXor 操作失败: %w", err)
+ }
+
+ return result, nil
}
-func Unbox(hex string) string {
+func unbox(hex string) string {
var box = []int{6, 28, 34, 31, 33, 18, 30, 23, 9, 8, 19, 38, 17, 24, 0, 5, 32, 21, 10, 22, 25, 14, 15, 3, 16, 27, 13, 35, 2, 29, 11, 26, 4, 36, 1, 39, 37, 7, 20, 12}
var newBox = make([]byte, len(hex))
- for i := 0; i < len(box); i++ {
- j := box[i]
+ for i, j := range box {
if len(newBox) > j {
newBox[j] = hex[i]
}
@@ -143,14 +148,21 @@ func Unbox(hex string) string {
return string(newBox)
}
-func HexXor(hex1, hex2 string) string {
- out := bytes.NewBuffer(make([]byte, len(hex1)))
- for i := 0; i < len(hex1) && i < len(hex2); i += 2 {
- v1, _ := strconv.ParseInt(hex1[i:i+2], 16, 64)
- v2, _ := strconv.ParseInt(hex2[i:i+2], 16, 64)
- out.WriteString(strconv.FormatInt(v1^v2, 16))
+func hexXor(hex1, hex2 string) (string, error) {
+ bytes1, err := hex.DecodeString(hex1)
+ if err != nil {
+ return "", fmt.Errorf("解码 hex1 失败: %w", err)
+ }
+ bytes2, err := hex.DecodeString(hex2)
+ if err != nil {
+ return "", fmt.Errorf("解码 hex2 失败: %w", err)
+ }
+ minLength := min(len(bytes2), len(bytes1))
+ resultBytes := make([]byte, minLength)
+ for i := range minLength {
+ resultBytes[i] = bytes1[i] ^ bytes2[i]
}
- return out.String()
+ return hex.EncodeToString(resultBytes), nil
}
var findDataReg = regexp.MustCompile(`data[:\s]+({[^}]+})`) // 查找json
diff --git a/drivers/lanzou/util.go b/drivers/lanzou/util.go
index 018df604..9a15d428 100644
--- a/drivers/lanzou/util.go
+++ b/drivers/lanzou/util.go
@@ -3,6 +3,7 @@ package lanzou
import (
"errors"
"fmt"
+ "io"
"net/http"
"regexp"
"runtime"
@@ -94,36 +95,66 @@ func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{},
}
}
+// 修复点:所有请求都自动处理 acw_sc__v2 验证和 down_ip=1
func (d *LanZou) request(url string, method string, callback base.ReqCallback, up bool) ([]byte, error) {
var req *resty.Request
- if up {
- once.Do(func() {
- upClient = base.NewRestyClient().SetTimeout(120 * time.Second)
- })
- req = upClient.R()
- } else {
- req = base.RestyClient.R()
- }
+ var vs string
+ for retry := 0; retry < 3; retry++ {
+ if up {
+ once.Do(func() {
+ upClient = base.NewRestyClient().SetTimeout(120 * time.Second)
+ })
+ req = upClient.R()
+ } else {
+ req = base.RestyClient.R()
+ }
- req.SetHeaders(map[string]string{
- "Referer": "https://pc.woozooo.com",
- "User-Agent": d.UserAgent,
- })
+ req.SetHeaders(map[string]string{
+ "Referer": "https://pc.woozooo.com",
+ "User-Agent": d.UserAgent,
+ })
- if d.Cookie != "" {
- req.SetHeader("cookie", d.Cookie)
- }
+ // 下载直链时需要加 down_ip=1
+ if strings.Contains(url, "/file/") {
+ cookie := d.Cookie
+ if cookie != "" {
+ cookie += "; "
+ }
+ cookie += "down_ip=1"
+ if vs != "" {
+ cookie += "; acw_sc__v2=" + vs
+ }
+ req.SetHeader("cookie", cookie)
+ } else if d.Cookie != "" {
+ cookie := d.Cookie
+ if vs != "" {
+ cookie += "; acw_sc__v2=" + vs
+ }
+ req.SetHeader("cookie", cookie)
+ } else if vs != "" {
+ req.SetHeader("cookie", "acw_sc__v2="+vs)
+ }
- if callback != nil {
- callback(req)
- }
+ if callback != nil {
+ callback(req)
+ }
- res, err := req.Execute(method, url)
- if err != nil {
- return nil, err
+ res, err := req.Execute(method, url)
+ if err != nil {
+ return nil, err
+ }
+ bodyStr := res.String()
+ log.Debugf("lanzou request: url=>%s ,stats=>%d ,body => %s\n", res.Request.URL, res.StatusCode(), bodyStr)
+ if strings.Contains(bodyStr, "acw_sc__v2") {
+ vs, err = CalcAcwScV2(bodyStr)
+ if err != nil {
+ return nil, err
+ }
+ continue
+ }
+ return res.Body(), err
}
- log.Debugf("lanzou request: url=>%s ,stats=>%d ,body => %s\n", res.Request.URL, res.StatusCode(), res.String())
- return res.Body(), err
+ return nil, errors.New("acw_sc__v2 validation error")
}
func (d *LanZou) Login() ([]*http.Cookie, error) {
@@ -430,27 +461,91 @@ func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (
file.Time = timeFindReg.FindString(sharePageData)
// 重定向获取真实链接
- res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
- "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
- }).Get(downloadUrl)
+ var (
+ res *resty.Response
+ err error
+ )
+ var vs string
+ var bodyStr string
+ for i := 0; i < 3; i++ {
+ res, err = base.NoRedirectClient.R().SetHeaders(map[string]string{
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+ "Referer": baseUrl,
+ }).SetDoNotParseResponse(true).
+ SetCookie(&http.Cookie{
+ Name: "acw_sc__v2",
+ Value: vs,
+ }).SetHeader("cookie", "down_ip=1").Get(downloadUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ if res.StatusCode() == 302 {
+ if res.RawBody() != nil {
+ res.RawBody().Close()
+ }
+ break
+ }
+ bodyBytes, err := io.ReadAll(res.RawBody())
+ if res.RawBody() != nil {
+ res.RawBody().Close()
+ }
+ if err != nil {
+ return nil, fmt.Errorf("读取响应体失败: %w", err)
+ }
+ bodyStr = string(bodyBytes)
+ if strings.Contains(bodyStr, "acw_sc__v2") {
+ if vs, err = CalcAcwScV2(bodyStr); err != nil {
+ log.Errorf("lanzou: err => acw_sc__v2 validation error ,data => %s\n", bodyStr)
+ return nil, err
+ }
+ continue
+ }
+ break
+ }
+
if err != nil {
return nil, err
}
file.Url = res.Header().Get("location")
- // 触发验证
- rPageData := res.String()
+ // 触发二次验证,也需要处理一下触发acw_sc__v2的情况
if res.StatusCode() != 302 {
- param, err = htmlJsonToMap(rPageData)
+ param, err = htmlJsonToMap(bodyStr)
if err != nil {
return nil, err
}
param["el"] = "2"
time.Sleep(time.Second * 2)
- // 通过验证获取直连
- data, err := d.post(fmt.Sprint(baseUrl, "/ajax.php"), func(req *resty.Request) { req.SetFormData(param) }, nil)
+ // 通过验证获取直链
+ var data []byte
+ for i := 0; i < 3; i++ {
+ data, err = d.post(fmt.Sprint(baseUrl, "/ajax.php"), func(req *resty.Request) {
+ req.SetFormData(param)
+ req.SetHeader("cookie", "down_ip=1")
+ if vs != "" {
+ req.SetCookie(&http.Cookie{
+ Name: "acw_sc__v2",
+ Value: vs,
+ })
+ }
+ }, nil)
+ if err != nil {
+ return nil, err
+ }
+ ajaxBodyStr := string(data)
+ if strings.Contains(ajaxBodyStr, "acw_sc__v2") {
+ if vs, err = CalcAcwScV2(ajaxBodyStr); err != nil {
+ log.Errorf("lanzou: err => acw_sc__v2 validation error ,data => %s\n", ajaxBodyStr)
+ return nil, err
+ }
+ time.Sleep(time.Second * 2)
+ continue
+ }
+ break
+ }
if err != nil {
return nil, err
}
diff --git a/drivers/local/driver.go b/drivers/local/driver.go
index df357536..45badb2e 100644
--- a/drivers/local/driver.go
+++ b/drivers/local/driver.go
@@ -51,7 +51,7 @@ func (d *Local) Config() driver.Config {
func (d *Local) Init(ctx context.Context) error {
if d.MkdirPerm == "" {
- d.mkdirPerm = 0777
+ d.mkdirPerm = 0o777
} else {
v, err := strconv.ParseUint(d.MkdirPerm, 8, 32)
if err != nil {
@@ -150,6 +150,7 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
}
return files, nil
}
+
func (d *Local) FileInfoToObj(ctx context.Context, f fs.FileInfo, reqPath string, fullPath string) model.Obj {
thumb := ""
if d.Thumbnail {
@@ -198,7 +199,7 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
path = filepath.Join(d.GetRootPath(), path)
f, err := os.Stat(path)
if err != nil {
- if strings.Contains(err.Error(), "cannot find the file") {
+ if os.IsNotExist(err) {
return nil, errs.ObjectNotFound
}
return nil, err
@@ -234,6 +235,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) {
fullPath := file.GetPath()
link := &model.Link{}
+ var MFile model.File
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
var buf *bytes.Buffer
var thumbPath *string
@@ -260,9 +262,9 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, err
}
link.ContentLength = int64(stat.Size())
- link.MFile = open
+ MFile = open
} else {
- link.MFile = bytes.NewReader(buf.Bytes())
+ MFile = bytes.NewReader(buf.Bytes())
link.ContentLength = int64(buf.Len())
}
} else {
@@ -271,13 +273,11 @@ func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
return nil, err
}
link.ContentLength = file.GetSize()
- link.MFile = open
- }
- link.AddIfCloser(link.MFile)
- if !d.Config().OnlyLinkMFile {
- link.RangeReader = stream.GetRangeReaderFromMFile(link.ContentLength, link.MFile)
- link.MFile = nil
+ MFile = open
}
+ link.SyncClosers.AddIfCloser(MFile)
+ link.RangeReader = stream.GetRangeReaderFromMFile(link.ContentLength, MFile)
+ link.RequireReference = link.SyncClosers.Length() > 0
return link, nil
}
@@ -374,11 +374,26 @@ func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
err = os.Remove(obj.GetPath())
}
} else {
- dstPath := filepath.Join(d.RecycleBinPath, obj.GetName())
+ objPath := obj.GetPath()
+ 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 {
+ return err
+ }
+ }
+
+ dstPath := filepath.Join(recycleBinPath, objName)
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 {
return err
@@ -427,4 +442,14 @@ func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
return nil
}
+func (d *Local) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ du, err := getDiskUsage(d.RootFolderPath)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: du,
+ }, nil
+}
+
var _ driver.Driver = (*Local)(nil)
diff --git a/drivers/local/meta.go b/drivers/local/meta.go
index b16f3ca5..a27e9eec 100644
--- a/drivers/local/meta.go
+++ b/drivers/local/meta.go
@@ -18,12 +18,12 @@ type Addition struct {
}
var config = driver.Config{
- Name: "Local",
- OnlyLinkMFile: false,
- LocalSort: true,
- NoCache: true,
- DefaultRoot: "/",
- NoLinkURL: true,
+ Name: "Local",
+ LocalSort: true,
+ OnlyProxy: true,
+ NoCache: true,
+ DefaultRoot: "/",
+ NoLinkURL: true,
}
func init() {
diff --git a/drivers/local/util_unix.go b/drivers/local/util_unix.go
index ddb4879f..3362df34 100644
--- a/drivers/local/util_unix.go
+++ b/drivers/local/util_unix.go
@@ -5,8 +5,25 @@ package local
import (
"io/fs"
"strings"
+ "syscall"
+
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
)
func isHidden(f fs.FileInfo, _ string) bool {
return strings.HasPrefix(f.Name(), ".")
}
+
+func getDiskUsage(path string) (model.DiskUsage, error) {
+ var stat syscall.Statfs_t
+ err := syscall.Statfs(path, &stat)
+ if err != nil {
+ return model.DiskUsage{}, err
+ }
+ total := stat.Blocks * uint64(stat.Bsize)
+ free := stat.Bfree * uint64(stat.Bsize)
+ return model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: free,
+ }, nil
+}
diff --git a/drivers/local/util_windows.go b/drivers/local/util_windows.go
index 8df191cb..37064009 100644
--- a/drivers/local/util_windows.go
+++ b/drivers/local/util_windows.go
@@ -1,22 +1,51 @@
-//go:build windows
-
-package local
-
-import (
- "io/fs"
- "path/filepath"
- "syscall"
-)
-
-func isHidden(f fs.FileInfo, fullPath string) bool {
- filePath := filepath.Join(fullPath, f.Name())
- namePtr, err := syscall.UTF16PtrFromString(filePath)
- if err != nil {
- return false
- }
- attrs, err := syscall.GetFileAttributes(namePtr)
- if err != nil {
- return false
- }
- return attrs&syscall.FILE_ATTRIBUTE_HIDDEN != 0
-}
+//go:build windows
+
+package local
+
+import (
+ "errors"
+ "io/fs"
+ "path/filepath"
+ "syscall"
+
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
+ "golang.org/x/sys/windows"
+)
+
+func isHidden(f fs.FileInfo, fullPath string) bool {
+ filePath := filepath.Join(fullPath, f.Name())
+ namePtr, err := syscall.UTF16PtrFromString(filePath)
+ if err != nil {
+ return false
+ }
+ attrs, err := syscall.GetFileAttributes(namePtr)
+ if err != nil {
+ return false
+ }
+ return attrs&syscall.FILE_ATTRIBUTE_HIDDEN != 0
+}
+
+func getDiskUsage(path string) (model.DiskUsage, error) {
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return model.DiskUsage{}, err
+ }
+ root := filepath.VolumeName(abs)
+ if len(root) != 2 || root[1] != ':' {
+ return model.DiskUsage{}, errors.New("cannot get disk label")
+ }
+ var freeBytes, totalBytes, totalFreeBytes uint64
+ err = windows.GetDiskFreeSpaceEx(
+ windows.StringToUTF16Ptr(root),
+ &freeBytes,
+ &totalBytes,
+ &totalFreeBytes,
+ )
+ if err != nil {
+ return model.DiskUsage{}, err
+ }
+ return model.DiskUsage{
+ TotalSpace: totalBytes,
+ FreeSpace: freeBytes,
+ }, nil
+}
diff --git a/drivers/mediafire/driver.go b/drivers/mediafire/driver.go
new file mode 100644
index 00000000..bd250259
--- /dev/null
+++ b/drivers/mediafire/driver.go
@@ -0,0 +1,431 @@
+package mediafire
+
+/*
+Package mediafire
+Author: Da3zKi7
+Date: 2025-09-11
+
+D@' 3z K!7 - The King Of Cracking
+
+Modifications by ILoveScratch2
+Date: 2025-09-21
+
+Date: 2025-09-26
+Final opts by @Suyunjing @j2rong4cn @KirCute @Da3zKi7
+*/
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/OpenListTeam/OpenList/v4/drivers/base"
+ "github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
+ "github.com/OpenListTeam/OpenList/v4/internal/stream"
+ "github.com/OpenListTeam/OpenList/v4/pkg/cron"
+ "github.com/OpenListTeam/OpenList/v4/pkg/utils"
+ "golang.org/x/time/rate"
+)
+
+type Mediafire struct {
+ model.Storage
+ Addition
+
+ cron *cron.Cron
+
+ actionToken string
+ limiter *rate.Limiter
+
+ appBase string
+ apiBase string
+ hostBase string
+ maxRetries int
+
+ secChUa string
+ secChUaPlatform string
+ userAgent string
+}
+
+func (d *Mediafire) Config() driver.Config {
+ return config
+}
+
+func (d *Mediafire) GetAddition() driver.Additional {
+ return &d.Addition
+}
+
+// Init initializes the MediaFire driver with session token and cookie validation
+func (d *Mediafire) Init(ctx context.Context) error {
+ if d.SessionToken == "" {
+ return fmt.Errorf("Init :: [MediaFire] {critical} missing sessionToken")
+ }
+
+ if d.Cookie == "" {
+ return fmt.Errorf("Init :: [MediaFire] {critical} missing Cookie")
+ }
+ // Setup rate limiter if rate limit is configured
+ if d.LimitRate > 0 {
+ d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
+ }
+ // Validate and refresh session token if needed
+ if _, err := d.getSessionToken(ctx); err != nil {
+
+ d.renewToken(ctx)
+
+ // Avoids 10 mins token expiry (6- 9)
+ num := rand.Intn(4) + 6
+
+ d.cron = cron.NewCron(time.Minute * time.Duration(num))
+ d.cron.Do(func() {
+ // Crazy, but working way to refresh session token
+ d.renewToken(ctx)
+ })
+
+ }
+
+ return nil
+}
+
+// Drop cleans up driver resources
+func (d *Mediafire) Drop(ctx context.Context) error {
+ // Clear cached resources
+ d.actionToken = ""
+ if d.cron != nil {
+ d.cron.Stop()
+ d.cron = nil
+ }
+ return nil
+}
+
+// List retrieves files and folders from the specified directory
+func (d *Mediafire) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+ files, err := d.getFiles(ctx, dir.GetID())
+ if err != nil {
+ return nil, err
+ }
+ return utils.SliceConvert(files, func(src File) (model.Obj, error) {
+ return d.fileToObj(src), nil
+ })
+}
+
+// Link generates a direct download link for the specified file
+func (d *Mediafire) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+ downloadUrl, err := d.getDirectDownloadLink(ctx, file.GetID())
+ if err != nil {
+ return nil, err
+ }
+
+ res, err := base.NoRedirectClient.R().SetDoNotParseResponse(true).SetContext(ctx).Head(downloadUrl)
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ _ = res.RawBody().Close()
+ }()
+
+ if res.StatusCode() == 302 {
+ downloadUrl = res.Header().Get("location")
+ }
+
+ return &model.Link{
+ URL: downloadUrl,
+ Header: http.Header{
+ "Origin": []string{d.appBase},
+ "Referer": []string{d.appBase + "/"},
+ "sec-ch-ua": []string{d.secChUa},
+ "sec-ch-ua-platform": []string{d.secChUaPlatform},
+ "User-Agent": []string{d.userAgent},
+ },
+ }, nil
+}
+
+// MakeDir creates a new folder in the specified parent directory
+func (d *Mediafire) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
+ data := map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "parent_key": parentDir.GetID(),
+ "foldername": dirName,
+ }
+
+ var resp MediafireFolderCreateResponse
+ _, err := d.postForm(ctx, "/folder/create.php", data, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ created, _ := time.Parse("2006-01-02T15:04:05Z", resp.Response.CreatedUTC)
+
+ return &model.Object{
+ ID: resp.Response.FolderKey,
+ Name: resp.Response.Name,
+ Size: 0,
+ Modified: created,
+ Ctime: created,
+ IsFolder: true,
+ }, nil
+}
+
+// Move relocates a file or folder to a different parent directory
+func (d *Mediafire) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+ var data map[string]string
+ var endpoint string
+
+ if srcObj.IsDir() {
+
+ endpoint = "/folder/move.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "folder_key_src": srcObj.GetID(),
+ "folder_key_dst": dstDir.GetID(),
+ }
+ } else {
+
+ endpoint = "/file/move.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "quick_key": srcObj.GetID(),
+ "folder_key": dstDir.GetID(),
+ }
+ }
+
+ var resp MediafireMoveResponse
+ _, err := d.postForm(ctx, endpoint, data, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ return srcObj, nil
+}
+
+// Rename changes the name of a file or folder
+func (d *Mediafire) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
+ var data map[string]string
+ var endpoint string
+
+ if srcObj.IsDir() {
+
+ endpoint = "/folder/update.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "folder_key": srcObj.GetID(),
+ "foldername": newName,
+ }
+ } else {
+
+ endpoint = "/file/update.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "quick_key": srcObj.GetID(),
+ "filename": newName,
+ }
+ }
+
+ var resp MediafireRenameResponse
+ _, err := d.postForm(ctx, endpoint, data, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ return &model.Object{
+ ID: srcObj.GetID(),
+ Name: newName,
+ Size: srcObj.GetSize(),
+ Modified: srcObj.ModTime(),
+ Ctime: srcObj.CreateTime(),
+ IsFolder: srcObj.IsDir(),
+ }, nil
+}
+
+// Copy creates a duplicate of a file or folder in the specified destination directory
+func (d *Mediafire) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
+ var data map[string]string
+ var endpoint string
+
+ if srcObj.IsDir() {
+
+ endpoint = "/folder/copy.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "folder_key_src": srcObj.GetID(),
+ "folder_key_dst": dstDir.GetID(),
+ }
+ } else {
+
+ endpoint = "/file/copy.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "quick_key": srcObj.GetID(),
+ "folder_key": dstDir.GetID(),
+ }
+ }
+
+ var resp MediafireCopyResponse
+ _, err := d.postForm(ctx, endpoint, data, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ var newID string
+ if srcObj.IsDir() {
+ if len(resp.Response.NewFolderKeys) > 0 {
+ newID = resp.Response.NewFolderKeys[0]
+ }
+ } else {
+ if len(resp.Response.NewQuickKeys) > 0 {
+ newID = resp.Response.NewQuickKeys[0]
+ }
+ }
+
+ return &model.Object{
+ ID: newID,
+ Name: srcObj.GetName(),
+ Size: srcObj.GetSize(),
+ Modified: srcObj.ModTime(),
+ Ctime: srcObj.CreateTime(),
+ IsFolder: srcObj.IsDir(),
+ }, nil
+}
+
+// Remove deletes a file or folder permanently
+func (d *Mediafire) Remove(ctx context.Context, obj model.Obj) error {
+ var data map[string]string
+ var endpoint string
+
+ if obj.IsDir() {
+
+ endpoint = "/folder/delete.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "folder_key": obj.GetID(),
+ }
+ } else {
+
+ endpoint = "/file/delete.php"
+ data = map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "quick_key": obj.GetID(),
+ }
+ }
+
+ var resp MediafireRemoveResponse
+ _, err := d.postForm(ctx, endpoint, data, &resp)
+ if err != nil {
+ return err
+ }
+
+ return checkAPIResult(resp.Response.Result)
+}
+
+// Put uploads a file to the specified directory with support for resumable upload and quick upload
+func (d *Mediafire) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
+ fileHash := file.GetHash().GetHash(utils.SHA256)
+ var err error
+
+ // Try to use existing hash first, cache only if necessary
+ if len(fileHash) != utils.SHA256.Width {
+ _, fileHash, err = stream.CacheFullAndHash(file, &up, utils.SHA256)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ checkResp, err := d.uploadCheck(ctx, file.GetName(), file.GetSize(), fileHash, dstDir.GetID())
+ if err != nil {
+ return nil, err
+ }
+
+ if checkResp.Response.HashExists == "yes" && checkResp.Response.InAccount == "yes" {
+ up(100.0)
+ existingFile, err := d.getExistingFileInfo(ctx, fileHash, file.GetName(), dstDir.GetID())
+ if err == nil && existingFile != nil {
+ // File exists, return existing file info
+ return &model.Object{
+ ID: existingFile.GetID(),
+ Name: file.GetName(),
+ Size: file.GetSize(),
+ }, nil
+ }
+ // If getExistingFileInfo fails, log and continue with normal upload
+ // This ensures upload doesn't fail due to search issues
+ }
+
+ var pollKey string
+
+ if checkResp.Response.ResumableUpload.AllUnitsReady != "yes" {
+ pollKey, err = d.uploadUnits(ctx, file, checkResp, file.GetName(), fileHash, dstDir.GetID(), up)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ pollKey = checkResp.Response.ResumableUpload.UploadKey
+ up(100.0)
+ }
+
+ pollResp, err := d.pollUpload(ctx, pollKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &model.Object{
+ ID: pollResp.Response.Doupload.QuickKey,
+ Name: file.GetName(),
+ Size: file.GetSize(),
+ }, nil
+}
+
+func (d *Mediafire) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ data := map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ }
+ var resp MediafireUserInfoResponse
+ _, err := d.postForm(ctx, "/user/get_info.php", data, &resp)
+ if err != nil {
+ return nil, err
+ }
+ used, err := strconv.ParseUint(resp.Response.UserInfo.UsedStorageSize, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ total, err := strconv.ParseUint(resp.Response.UserInfo.StorageLimit, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: total - used,
+ },
+ }, nil
+}
+
+var _ driver.Driver = (*Mediafire)(nil)
diff --git a/drivers/mediafire/meta.go b/drivers/mediafire/meta.go
new file mode 100644
index 00000000..78a5b9b1
--- /dev/null
+++ b/drivers/mediafire/meta.go
@@ -0,0 +1,61 @@
+package mediafire
+
+/*
+Package mediafire
+Author: Da3zKi7
+Date: 2025-09-11
+
+D@' 3z K!7 - The King Of Cracking
+
+Modifications by ILoveScratch2
+Date: 2025-09-21
+
+Date: 2025-09-26
+Final opts by @Suyunjing @j2rong4cn @KirCute @Da3zKi7
+*/
+
+import (
+ "github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/op"
+)
+
+type Addition struct {
+ driver.RootPath
+ //driver.RootID
+
+ SessionToken string `json:"session_token" required:"true" type:"string" help:"Required for MediaFire API"`
+ Cookie string `json:"cookie" required:"true" type:"string" help:"Required for navigation"`
+
+ OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
+ OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
+ ChunkSize int64 `json:"chunk_size" type:"number" default:"100"`
+ UploadThreads int `json:"upload_threads" type:"number" default:"3" help:"concurrent upload threads"`
+ LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate ([limit]r/1s)"`
+}
+
+var config = driver.Config{
+ Name: "MediaFire",
+ LocalSort: false,
+ OnlyProxy: false,
+ NoCache: false,
+ NoUpload: false,
+ NeedMs: false,
+ DefaultRoot: "/",
+ CheckStatus: false,
+ Alert: "",
+ NoOverwriteUpload: true,
+}
+
+func init() {
+ op.RegisterDriver(func() driver.Driver {
+ return &Mediafire{
+ appBase: "https://app.mediafire.com",
+ apiBase: "https://www.mediafire.com/api/1.5",
+ hostBase: "https://www.mediafire.com",
+ maxRetries: 3,
+ secChUa: "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"139\", \"Google Chrome\";v=\"139\"",
+ secChUaPlatform: "Windows",
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
+ }
+ })
+}
diff --git a/drivers/mediafire/types.go b/drivers/mediafire/types.go
new file mode 100644
index 00000000..4a59ae56
--- /dev/null
+++ b/drivers/mediafire/types.go
@@ -0,0 +1,246 @@
+package mediafire
+
+/*
+Package mediafire
+Author: Da3zKi7
+Date: 2025-09-11
+
+D@' 3z K!7 - The King Of Cracking
+*/
+
+type MediafireRenewTokenResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ SessionToken string `json:"session_token"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+
+type MediafireResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ FolderContent struct {
+ ChunkSize string `json:"chunk_size"`
+ ContentType string `json:"content_type"`
+ ChunkNumber string `json:"chunk_number"`
+ FolderKey string `json:"folderkey"`
+ Folders []MediafireFolder `json:"folders,omitempty"`
+ Files []MediafireFile `json:"files,omitempty"`
+ MoreChunks string `json:"more_chunks"`
+ } `json:"folder_content"`
+ Result string `json:"result"`
+ } `json:"response"`
+}
+
+type MediafireFolder struct {
+ FolderKey string `json:"folderkey"`
+ Name string `json:"name"`
+ Created string `json:"created"`
+ CreatedUTC string `json:"created_utc"`
+}
+
+type MediafireFile struct {
+ QuickKey string `json:"quickkey"`
+ Filename string `json:"filename"`
+ Size string `json:"size"`
+ Created string `json:"created"`
+ CreatedUTC string `json:"created_utc"`
+ MimeType string `json:"mimetype"`
+}
+
+type File struct {
+ ID string
+ Name string
+ Size int64
+ CreatedUTC string
+ IsFolder bool
+}
+
+type FolderContentResponse struct {
+ Folders []MediafireFolder
+ Files []MediafireFile
+ MoreChunks bool
+}
+
+type MediafireLinksResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Links []struct {
+ QuickKey string `json:"quickkey"`
+ View string `json:"view"`
+ NormalDownload string `json:"normal_download"`
+ OneTime struct {
+ Download string `json:"download"`
+ View string `json:"view"`
+ } `json:"one_time"`
+ } `json:"links"`
+ OneTimeKeyRequestCount string `json:"one_time_key_request_count"`
+ OneTimeKeyRequestMaxCount string `json:"one_time_key_request_max_count"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+
+type MediafireDirectDownloadResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Links []struct {
+ QuickKey string `json:"quickkey"`
+ DirectDownload string `json:"direct_download"`
+ } `json:"links"`
+ DirectDownloadFreeBandwidth string `json:"direct_download_free_bandwidth"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+
+type MediafireFolderCreateResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ FolderKey string `json:"folder_key"`
+ UploadKey string `json:"upload_key"`
+ ParentFolderKey string `json:"parent_folderkey"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Created string `json:"created"`
+ CreatedUTC string `json:"created_utc"`
+ Privacy string `json:"privacy"`
+ FileCount string `json:"file_count"`
+ FolderCount string `json:"folder_count"`
+ Revision string `json:"revision"`
+ DropboxEnabled string `json:"dropbox_enabled"`
+ Flag string `json:"flag"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ NewDeviceRevision int `json:"new_device_revision"`
+ } `json:"response"`
+}
+
+type MediafireMoveResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Asynchronous string `json:"asynchronous,omitempty"`
+ NewNames []string `json:"new_names"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ NewDeviceRevision int `json:"new_device_revision"`
+ } `json:"response"`
+}
+
+type MediafireRenameResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Asynchronous string `json:"asynchronous,omitempty"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ NewDeviceRevision int `json:"new_device_revision"`
+ } `json:"response"`
+}
+
+type MediafireCopyResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Asynchronous string `json:"asynchronous,omitempty"`
+ NewQuickKeys []string `json:"new_quickkeys,omitempty"`
+ NewFolderKeys []string `json:"new_folderkeys,omitempty"`
+ SkippedCount string `json:"skipped_count,omitempty"`
+ OtherCount string `json:"other_count,omitempty"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ NewDeviceRevision int `json:"new_device_revision"`
+ } `json:"response"`
+}
+
+type MediafireRemoveResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Asynchronous string `json:"asynchronous,omitempty"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ NewDeviceRevision int `json:"new_device_revision"`
+ } `json:"response"`
+}
+
+type MediafireCheckResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ HashExists string `json:"hash_exists"`
+ InAccount string `json:"in_account"`
+ InFolder string `json:"in_folder"`
+ FileExists string `json:"file_exists"`
+ ResumableUpload struct {
+ AllUnitsReady string `json:"all_units_ready"`
+ NumberOfUnits string `json:"number_of_units"`
+ UnitSize string `json:"unit_size"`
+ Bitmap struct {
+ Count string `json:"count"`
+ Words []string `json:"words"`
+ } `json:"bitmap"`
+ UploadKey string `json:"upload_key"`
+ } `json:"resumable_upload"`
+ AvailableSpace string `json:"available_space"`
+ UsedStorageSize string `json:"used_storage_size"`
+ StorageLimit string `json:"storage_limit"`
+ StorageLimitExceeded string `json:"storage_limit_exceeded"`
+ UploadURL struct {
+ Simple string `json:"simple"`
+ SimpleFallback string `json:"simple_fallback"`
+ Resumable string `json:"resumable"`
+ ResumableFallback string `json:"resumable_fallback"`
+ } `json:"upload_url"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+type MediafireActionTokenResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ ActionToken string `json:"action_token"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+
+type MediafirePollResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ Doupload struct {
+ Result string `json:"result"`
+ Status string `json:"status"`
+ Description string `json:"description"`
+ QuickKey string `json:"quickkey"`
+ Hash string `json:"hash"`
+ Filename string `json:"filename"`
+ Size string `json:"size"`
+ Created string `json:"created"`
+ CreatedUTC string `json:"created_utc"`
+ Revision string `json:"revision"`
+ } `json:"doupload"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+
+type MediafireFileSearchResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ FileInfo []File `json:"file_info"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
+
+type MediafireUserInfoResponse struct {
+ Response struct {
+ Action string `json:"action"`
+ UserInfo struct {
+ Email string `json:"string"`
+ DisplayName string `json:"display_name"`
+ UsedStorageSize string `json:"used_storage_size"`
+ StorageLimit string `json:"storage_limit"`
+ } `json:"user_info"`
+ Result string `json:"result"`
+ CurrentAPIVersion string `json:"current_api_version"`
+ } `json:"response"`
+}
diff --git a/drivers/mediafire/util.go b/drivers/mediafire/util.go
new file mode 100644
index 00000000..6ded6bdf
--- /dev/null
+++ b/drivers/mediafire/util.go
@@ -0,0 +1,729 @@
+package mediafire
+
+/*
+Package mediafire
+Author: Da3zKi7
+Date: 2025-09-11
+
+D@' 3z K!7 - The King Of Cracking
+
+Modifications by ILoveScratch2
+Date: 2025-09-21
+
+Date: 2025-09-26
+Final opts by @Suyunjing @j2rong4cn @KirCute @Da3zKi7
+*/
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/OpenListTeam/OpenList/v4/drivers/base"
+ "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/OpenListTeam/OpenList/v4/pkg/errgroup"
+ "github.com/OpenListTeam/OpenList/v4/pkg/utils"
+ "github.com/avast/retry-go"
+ "github.com/go-resty/resty/v2"
+)
+
+// checkAPIResult validates MediaFire API response result and returns error if not successful
+func checkAPIResult(result string) error {
+ if result != "Success" {
+ return fmt.Errorf("MediaFire API error: %s", result)
+ }
+ return nil
+}
+
+// getSessionToken retrieves and validates session token from MediaFire
+func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) {
+ if d.limiter != nil {
+ if err := d.limiter.Wait(ctx); err != nil {
+ return "", fmt.Errorf("rate limit wait failed: %w", err)
+ }
+ }
+
+ tokenURL := d.hostBase + "/application/get_session_token.php"
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
+ if err != nil {
+ return "", err
+ }
+
+ req.Header.Set("Accept", "*/*")
+ req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
+ req.Header.Set("Accept-Language", "en-US,en;q=0.9")
+ req.Header.Set("Content-Length", "0")
+ req.Header.Set("Cookie", d.Cookie)
+ req.Header.Set("DNT", "1")
+ req.Header.Set("Origin", d.hostBase)
+ req.Header.Set("Priority", "u=1, i")
+ req.Header.Set("Referer", (d.hostBase + "/"))
+ req.Header.Set("Sec-Ch-Ua", d.secChUa)
+ req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
+ req.Header.Set("Sec-Ch-Ua-Platform", d.secChUaPlatform)
+ req.Header.Set("Sec-Fetch-Dest", "empty")
+ req.Header.Set("Sec-Fetch-Mode", "cors")
+ req.Header.Set("Sec-Fetch-Site", "same-site")
+ req.Header.Set("User-Agent", d.userAgent)
+ // req.Header.Set("Connection", "keep-alive")
+
+ resp, err := base.HttpClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ // fmt.Printf("getSessionToken :: Raw response: %s\n", string(body))
+ // fmt.Printf("getSessionToken :: Parsed response: %+v\n", resp)
+
+ var tokenResp struct {
+ Response struct {
+ SessionToken string `json:"session_token"`
+ } `json:"response"`
+ }
+
+ if resp.StatusCode == 200 {
+ if err := json.Unmarshal(body, &tokenResp); err != nil {
+ return "", err
+ }
+
+ if tokenResp.Response.SessionToken == "" {
+ return "", fmt.Errorf("empty session token received")
+ }
+
+ cookieMap := make(map[string]string)
+ for _, cookie := range resp.Cookies() {
+ cookieMap[cookie.Name] = cookie.Value
+ }
+
+ if len(cookieMap) > 0 {
+
+ var cookies []string
+ for name, value := range cookieMap {
+ cookies = append(cookies, fmt.Sprintf("%s=%s", name, value))
+ }
+ d.Cookie = strings.Join(cookies, "; ")
+ op.MustSaveDriverStorage(d)
+
+ // fmt.Printf("getSessionToken :: Captured cookies: %s\n", d.Cookie)
+ }
+
+ } else {
+ return "", fmt.Errorf("getSessionToken :: failed to get session token, status code: %d", resp.StatusCode)
+ }
+
+ d.SessionToken = tokenResp.Response.SessionToken
+
+ // fmt.Printf("Init :: Obtain Session Token %v", d.SessionToken)
+
+ op.MustSaveDriverStorage(d)
+
+ return d.SessionToken, nil
+}
+
+// renewToken refreshes the current session token when expired
+func (d *Mediafire) renewToken(ctx context.Context) error {
+ query := map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ }
+
+ var resp MediafireRenewTokenResponse
+ _, err := d.postForm(ctx, "/user/renew_session_token.php", query, &resp)
+ if err != nil {
+ return fmt.Errorf("failed to renew token: %w", err)
+ }
+
+ // fmt.Printf("getInfo :: Raw response: %s\n", string(body))
+ // fmt.Printf("getInfo :: Parsed response: %+v\n", resp)
+
+ if resp.Response.Result != "Success" {
+ return fmt.Errorf("MediaFire token renewal failed: %s", resp.Response.Result)
+ }
+
+ d.SessionToken = resp.Response.SessionToken
+
+ // fmt.Printf("Init :: Renew Session Token: %s", resp.Response.Result)
+
+ op.MustSaveDriverStorage(d)
+
+ return nil
+}
+
+func (d *Mediafire) getFiles(ctx context.Context, folderKey string) ([]File, error) {
+ // Pre-allocate slice with reasonable capacity to reduce memory allocations
+ files := make([]File, 0, d.ChunkSize*2) // Estimate: ChunkSize for files + folders
+ hasMore := true
+ chunkNumber := 1
+
+ for hasMore {
+ resp, err := d.getFolderContent(ctx, folderKey, chunkNumber)
+ if err != nil {
+ return nil, err
+ }
+
+ // Process folders and files in single loop to improve cache locality
+ totalItems := len(resp.Folders) + len(resp.Files)
+ if cap(files)-len(files) < totalItems {
+ // Grow slice if needed
+ newFiles := make([]File, len(files), len(files)+totalItems+int(d.ChunkSize))
+ copy(newFiles, files)
+ files = newFiles
+ }
+
+ for _, folder := range resp.Folders {
+ files = append(files, File{
+ ID: folder.FolderKey,
+ Name: folder.Name,
+ Size: 0,
+ CreatedUTC: folder.CreatedUTC,
+ IsFolder: true,
+ })
+ }
+
+ for _, file := range resp.Files {
+ size, _ := strconv.ParseInt(file.Size, 10, 64)
+ files = append(files, File{
+ ID: file.QuickKey,
+ Name: file.Filename,
+ Size: size,
+ CreatedUTC: file.CreatedUTC,
+ IsFolder: false,
+ })
+ }
+
+ hasMore = resp.MoreChunks
+ chunkNumber++
+ }
+
+ return files, nil
+}
+
+func (d *Mediafire) getFolderContent(ctx context.Context, folderKey string, chunkNumber int) (*FolderContentResponse, error) {
+ foldersResp, err := d.getFolderContentByType(ctx, folderKey, "folders", chunkNumber)
+ if err != nil {
+ return nil, err
+ }
+
+ filesResp, err := d.getFolderContentByType(ctx, folderKey, "files", chunkNumber)
+ if err != nil {
+ return nil, err
+ }
+
+ return &FolderContentResponse{
+ Folders: foldersResp.Response.FolderContent.Folders,
+ Files: filesResp.Response.FolderContent.Files,
+ MoreChunks: foldersResp.Response.FolderContent.MoreChunks == "yes" || filesResp.Response.FolderContent.MoreChunks == "yes",
+ }, nil
+}
+
+func (d *Mediafire) getFolderContentByType(ctx context.Context, folderKey, contentType string, chunkNumber int) (*MediafireResponse, error) {
+ data := map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "folder_key": folderKey,
+ "content_type": contentType,
+ "chunk": strconv.Itoa(chunkNumber),
+ "chunk_size": strconv.FormatInt(d.ChunkSize, 10),
+ "details": "yes",
+ "order_direction": d.OrderDirection,
+ "order_by": d.OrderBy,
+ "filter": "",
+ }
+
+ var resp MediafireResponse
+ _, err := d.postForm(ctx, "/folder/get_content.php", data, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ return &resp, nil
+}
+
+// fileToObj converts MediaFire file data to model.ObjThumb with thumbnail support
+func (d *Mediafire) fileToObj(f File) *model.ObjThumb {
+ created, _ := time.Parse("2006-01-02T15:04:05Z", f.CreatedUTC)
+
+ var thumbnailURL string
+ if !f.IsFolder && f.ID != "" {
+ thumbnailURL = d.hostBase + "/convkey/acaa/" + f.ID + "3g.jpg"
+ }
+
+ return &model.ObjThumb{
+ Object: model.Object{
+ ID: f.ID,
+ // Path: "",
+ Name: f.Name,
+ Size: f.Size,
+ Modified: created,
+ Ctime: created,
+ IsFolder: f.IsFolder,
+ },
+ Thumbnail: model.Thumbnail{
+ Thumbnail: thumbnailURL,
+ },
+ }
+}
+
+func (d *Mediafire) setCommonHeaders(req *resty.Request) {
+ req.SetHeaders(map[string]string{
+ "Cookie": d.Cookie,
+ "User-Agent": d.userAgent,
+ "Origin": d.appBase,
+ "Referer": d.appBase + "/",
+ })
+}
+
+// apiRequest performs HTTP request to MediaFire API with rate limiting and common headers
+func (d *Mediafire) apiRequest(ctx context.Context, method, endpoint string, queryParams, formData map[string]string, resp interface{}) ([]byte, error) {
+ if d.limiter != nil {
+ if err := d.limiter.Wait(ctx); err != nil {
+ return nil, fmt.Errorf("rate limit wait failed: %w", err)
+ }
+ }
+
+ req := base.RestyClient.R()
+ req.SetContext(ctx)
+ d.setCommonHeaders(req)
+
+ // Set query parameters for GET requests
+ if queryParams != nil {
+ req.SetQueryParams(queryParams)
+ }
+
+ // Set form data for POST requests
+ if formData != nil {
+ req.SetFormData(formData)
+ req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
+ }
+
+ // Set response object if provided
+ if resp != nil {
+ req.SetResult(resp)
+ }
+
+ var res *resty.Response
+ var err error
+
+ // Execute request based on method
+ switch method {
+ case "GET":
+ res, err = req.Get(d.apiBase + endpoint)
+ case "POST":
+ res, err = req.Post(d.apiBase + endpoint)
+ default:
+ return nil, fmt.Errorf("unsupported HTTP method: %s", method)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return res.Body(), nil
+}
+
+func (d *Mediafire) getForm(ctx context.Context, endpoint string, query map[string]string, resp interface{}) ([]byte, error) {
+ return d.apiRequest(ctx, "GET", endpoint, query, nil, resp)
+}
+
+func (d *Mediafire) postForm(ctx context.Context, endpoint string, data map[string]string, resp interface{}) ([]byte, error) {
+ return d.apiRequest(ctx, "POST", endpoint, nil, data, resp)
+}
+
+func (d *Mediafire) getDirectDownloadLink(ctx context.Context, fileID string) (string, error) {
+ data := map[string]string{
+ "session_token": d.SessionToken,
+ "quick_key": fileID,
+ "link_type": "direct_download",
+ "response_format": "json",
+ }
+
+ var resp MediafireDirectDownloadResponse
+ _, err := d.getForm(ctx, "/file/get_links.php", data, &resp)
+ if err != nil {
+ return "", err
+ }
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return "", err
+ }
+
+ if len(resp.Response.Links) == 0 {
+ return "", fmt.Errorf("no download links found")
+ }
+
+ return resp.Response.Links[0].DirectDownload, nil
+}
+
+func (d *Mediafire) uploadCheck(ctx context.Context, filename string, filesize int64, filehash, folderKey string) (*MediafireCheckResponse, error) {
+ actionToken, err := d.getActionToken(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get action token: %w", err)
+ }
+
+ query := map[string]string{
+ "session_token": actionToken, /* d.SessionToken */
+ "filename": filename,
+ "size": strconv.FormatInt(filesize, 10),
+ "hash": filehash,
+ "folder_key": folderKey,
+ "resumable": "yes",
+ "response_format": "json",
+ }
+
+ var resp MediafireCheckResponse
+ _, err = d.postForm(ctx, "/upload/check.php", query, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ // fmt.Printf("uploadCheck :: Raw response: %s\n", string(body))
+ // fmt.Printf("uploadCheck :: Parsed response: %+v\n", resp)
+
+ // fmt.Printf("uploadCheck :: ResumableUpload section: %+v\n", resp.Response.ResumableUpload)
+ // fmt.Printf("uploadCheck :: Upload key specifically: '%s'\n", resp.Response.ResumableUpload.UploadKey)
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ return &resp, nil
+}
+
+func (d *Mediafire) uploadUnits(ctx context.Context, file model.FileStreamer, checkResp *MediafireCheckResponse, filename, fileHash, folderKey string, up driver.UpdateProgress) (string, error) {
+ unitSize, _ := strconv.ParseInt(checkResp.Response.ResumableUpload.UnitSize, 10, 64)
+ numUnits, _ := strconv.Atoi(checkResp.Response.ResumableUpload.NumberOfUnits)
+ uploadKey := checkResp.Response.ResumableUpload.UploadKey
+
+ stringWords := checkResp.Response.ResumableUpload.Bitmap.Words
+ intWords := make([]int, 0, len(stringWords))
+ for _, word := range stringWords {
+ if intWord, err := strconv.Atoi(word); err == nil {
+ intWords = append(intWords, intWord)
+ }
+ }
+
+ // Intelligent buffer sizing for large files
+ bufferSize := int(unitSize)
+ fileSize := file.GetSize()
+
+ // Split in chunks
+ if fileSize > d.ChunkSize*1024*1024 {
+
+ // Large, use ChunkSize (default = 100MB)
+ bufferSize = min(int(fileSize), int(d.ChunkSize)*1024*1024)
+ } else if fileSize > 10*1024*1024 {
+ // Medium, use full file size for concurrent access
+ bufferSize = int(fileSize)
+ }
+
+ // Create stream section reader for efficient chunking
+ ss, err := stream.NewStreamSectionReader(file, bufferSize, &up)
+ if err != nil {
+ return "", err
+ }
+
+ // Cal minimal parallel upload threads, allows MediaFire resumable upload to rule it over custom value
+ // If file is big, likely will respect d.UploadThreads instead of MediaFire's suggestion i.e. 5 threads
+ thread := min(numUnits, d.UploadThreads)
+
+ // Create ordered group for sequential upload processing with retry logic
+ threadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,
+ retry.Attempts(3),
+ retry.Delay(time.Second),
+ retry.DelayType(retry.BackOffDelay))
+
+ var finalUploadKey string
+ var keyMutex sync.Mutex
+
+ fileSize = file.GetSize()
+ for unitID := range numUnits {
+ if utils.IsCanceled(uploadCtx) {
+ break
+ }
+
+ start := int64(unitID) * unitSize
+ size := unitSize
+ if start+size > fileSize {
+ size = fileSize - start
+ }
+
+ var reader io.ReadSeeker
+ var rateLimitedRd io.Reader
+ var unitHash string
+
+ // Use lifecycle pattern for proper resource management
+ threadG.GoWithLifecycle(errgroup.Lifecycle{
+ Before: func(ctx context.Context) error {
+ // Skip already uploaded units
+ if d.isUnitUploaded(intWords, unitID) {
+ return ss.DiscardSection(start, size)
+ }
+
+ var err error
+ reader, err = ss.GetSectionReader(start, size)
+ if err != nil {
+ return err
+ }
+ rateLimitedRd = driver.NewLimitedUploadStream(ctx, reader)
+ return nil
+ },
+ Do: func(ctx context.Context) error {
+ if reader == nil {
+ return nil // Skip if reader is not initialized (already uploaded)
+ }
+
+ if unitHash == "" {
+ reader.Seek(0, io.SeekStart)
+ var err error
+ unitHash, err = utils.HashReader(utils.SHA256, reader)
+ if err != nil {
+ return err
+ }
+ }
+ reader.Seek(0, io.SeekStart)
+
+ // Perform upload
+
+ actionToken, err := d.getActionToken(ctx)
+ if err != nil {
+ return err
+ }
+ if d.limiter != nil {
+ if err := d.limiter.Wait(ctx); err != nil {
+ return fmt.Errorf("rate limit wait failed: %w", err)
+ }
+ }
+
+ url := d.apiBase + "/upload/resumable.php"
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, rateLimitedRd)
+ if err != nil {
+ return err
+ }
+
+ q := req.URL.Query()
+ q.Add("folder_key", folderKey)
+ q.Add("response_format", "json")
+ q.Add("session_token", actionToken)
+ q.Add("key", uploadKey)
+ req.URL.RawQuery = q.Encode()
+
+ req.Header.Set("x-filehash", fileHash)
+ req.Header.Set("x-filesize", strconv.FormatInt(fileSize, 10))
+ req.Header.Set("x-unit-id", strconv.Itoa(unitID))
+ req.Header.Set("x-unit-size", strconv.FormatInt(size, 10))
+ req.Header.Set("x-unit-hash", unitHash)
+ req.Header.Set("x-filename", filename)
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.ContentLength = size
+
+ /* fmt.Printf("Debug resumable upload request:\n")
+ fmt.Printf(" URL: %s\n", req.URL.String())
+ fmt.Printf(" Headers: %+v\n", req.Header)
+ fmt.Printf(" Unit ID: %d\n", unitID)
+ fmt.Printf(" Unit Size: %d\n", len(unitData))
+ fmt.Printf(" Upload Key: %s\n", uploadKey)
+ fmt.Printf(" Action Token: %s\n", actionToken) */
+
+ res, err := base.HttpClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return fmt.Errorf("failed to read response body: %v", err)
+ }
+
+ // fmt.Printf("MediaFire resumable upload response (status %d): %s\n", res.StatusCode, string(body))
+
+ var uploadResp struct {
+ Response struct {
+ Doupload struct {
+ Key string `json:"key"`
+ } `json:"doupload"`
+ Result string `json:"result"`
+ } `json:"response"`
+ }
+
+ if err := json.Unmarshal(body, &uploadResp); err != nil {
+ return fmt.Errorf("failed to parse response: %v", err)
+ }
+
+ if res.StatusCode != 200 {
+ return fmt.Errorf("resumable upload failed with status %d", res.StatusCode)
+ }
+
+ // Thread-safe update of final upload key
+ keyMutex.Lock()
+ finalUploadKey = uploadResp.Response.Doupload.Key
+ keyMutex.Unlock()
+
+ return nil
+ },
+ After: func(err error) {
+ up(float64(threadG.Success()) * 100 / float64(numUnits))
+ if reader != nil {
+ // Cleanup resources
+ ss.FreeSectionReader(reader)
+ }
+ },
+ })
+ }
+
+ if err := threadG.Wait(); err != nil {
+ return "", err
+ }
+
+ return finalUploadKey, nil
+}
+
+/*func (d *Mediafire) uploadSingleUnit(ctx context.Context, file model.FileStreamer, unitID int, unitSize int64, fileHash, filename, uploadKey, folderKey string, fileSize int64) (string, error) {
+ start := int64(unitID) * unitSize
+ size := unitSize
+
+ if start+size > fileSize {
+ size = fileSize - start
+ }
+
+ unitData := make([]byte, size)
+ _, err := file.Read(unitData)
+ if err != nil {
+ return "", err
+ }
+
+ return d.resumableUpload(ctx, folderKey, uploadKey, unitData, unitID, fileHash, filename, fileSize)
+}*/
+
+func (d *Mediafire) getActionToken(ctx context.Context) (string, error) {
+ if d.actionToken != "" {
+ return d.actionToken, nil
+ }
+
+ data := map[string]string{
+ "type": "upload",
+ "lifespan": "1440",
+ "response_format": "json",
+ "session_token": d.SessionToken,
+ }
+
+ var resp MediafireActionTokenResponse
+ _, err := d.postForm(ctx, "/user/get_action_token.php", data, &resp)
+ if err != nil {
+ return "", err
+ }
+
+ if resp.Response.Result != "Success" {
+ return "", fmt.Errorf("MediaFire action token failed: %s", resp.Response.Result)
+ }
+
+ return resp.Response.ActionToken, nil
+}
+
+func (d *Mediafire) pollUpload(ctx context.Context, key string) (*MediafirePollResponse, error) {
+ actionToken, err := d.getActionToken(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get action token: %w", err)
+ }
+
+ // fmt.Printf("Debug Key: %+v\n", key)
+
+ query := map[string]string{
+ "key": key,
+ "response_format": "json",
+ "session_token": actionToken, /* d.SessionToken */
+ }
+
+ var resp MediafirePollResponse
+ _, err = d.postForm(ctx, "/upload/poll_upload.php", query, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ // fmt.Printf("pollUpload :: Raw response: %s\n", string(body))
+ // fmt.Printf("pollUpload :: Parsed response: %+v\n", resp)
+
+ // fmt.Printf("pollUpload :: Debug Result: %+v\n", resp.Response.Result)
+
+ if err := checkAPIResult(resp.Response.Result); err != nil {
+ return nil, err
+ }
+
+ return &resp, nil
+}
+
+func (d *Mediafire) isUnitUploaded(words []int, unitID int) bool {
+ wordIndex := unitID / 16
+ bitIndex := unitID % 16
+ if wordIndex >= len(words) {
+ return false
+ }
+ return (words[wordIndex]>>bitIndex)&1 == 1
+}
+
+func (d *Mediafire) getExistingFileInfo(ctx context.Context, fileHash, filename, folderKey string) (*model.ObjThumb, error) {
+ // First try to find by hash directly (most efficient)
+ if fileInfo, err := d.getFileByHash(ctx, fileHash); err == nil && fileInfo != nil {
+ return fileInfo, nil
+ }
+
+ // If hash search fails, search in the target folder
+ // This is a fallback method in case the file exists but hash search doesn't work
+ files, err := d.getFiles(ctx, folderKey)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, file := range files {
+ if file.Name == filename && !file.IsFolder {
+ return d.fileToObj(file), nil
+ }
+ }
+
+ return nil, fmt.Errorf("existing file not found")
+}
+
+func (d *Mediafire) getFileByHash(ctx context.Context, hash string) (*model.ObjThumb, error) {
+ query := map[string]string{
+ "session_token": d.SessionToken,
+ "response_format": "json",
+ "hash": hash,
+ }
+
+ var resp MediafireFileSearchResponse
+ _, err := d.postForm(ctx, "/file/get_info.php", query, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.Response.Result != "Success" {
+ return nil, fmt.Errorf("MediaFire file search failed: %s", resp.Response.Result)
+ }
+
+ if len(resp.Response.FileInfo) == 0 {
+ return nil, fmt.Errorf("file not found by hash")
+ }
+
+ file := resp.Response.FileInfo[0]
+ return d.fileToObj(file), nil
+}
diff --git a/drivers/onedrive/driver.go b/drivers/onedrive/driver.go
index 40adf27c..c3a3020b 100644
--- a/drivers/onedrive/driver.go
+++ b/drivers/onedrive/driver.go
@@ -22,6 +22,7 @@ type Onedrive struct {
AccessToken string
root *Object
mutex sync.Mutex
+ ref *Onedrive
}
func (d *Onedrive) Config() driver.Config {
@@ -36,10 +37,22 @@ func (d *Onedrive) Init(ctx context.Context) error {
if d.ChunkSize < 1 {
d.ChunkSize = 5
}
+ if d.ref != nil {
+ return nil
+ }
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 {
+ d.ref = nil
return nil
}
@@ -207,4 +220,20 @@ func (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return err
}
+func (d *Onedrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ if d.DisableDiskUsage {
+ return nil, errs.NotImplement
+ }
+ drive, err := d.getDrive(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: drive.Quota.Total,
+ FreeSpace: drive.Quota.Remaining,
+ },
+ }, nil
+}
+
var _ driver.Driver = (*Onedrive)(nil)
diff --git a/drivers/onedrive/meta.go b/drivers/onedrive/meta.go
index 91ce16b9..239911f5 100644
--- a/drivers/onedrive/meta.go
+++ b/drivers/onedrive/meta.go
@@ -7,17 +7,18 @@ import (
type Addition struct {
driver.RootPath
- Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
- IsSharepoint bool `json:"is_sharepoint"`
- UseOnlineAPI bool `json:"use_online_api" default:"true"`
- APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
- ClientID string `json:"client_id"`
- ClientSecret string `json:"client_secret"`
- RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
- RefreshToken string `json:"refresh_token" required:"true"`
- SiteId string `json:"site_id"`
- ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
- CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
+ Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
+ IsSharepoint bool `json:"is_sharepoint"`
+ UseOnlineAPI bool `json:"use_online_api" default:"true"`
+ APIAddress string `json:"api_url_address" default:"https://api.oplist.org/onedrive/renewapi"`
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+ RedirectUri string `json:"redirect_uri" required:"true" default:"https://api.oplist.org/onedrive/callback"`
+ RefreshToken string `json:"refresh_token" required:"true"`
+ SiteId string `json:"site_id"`
+ ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
+ CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
+ DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
}
var config = driver.Config{
diff --git a/drivers/onedrive/types.go b/drivers/onedrive/types.go
index 2a328ec4..a00241eb 100644
--- a/drivers/onedrive/types.go
+++ b/drivers/onedrive/types.go
@@ -89,3 +89,15 @@ type FileSystemInfoFacet struct {
CreatedDateTime time.Time `json:"createdDateTime,omitempty"` // The UTC date and time the file was created on a client.
LastModifiedDateTime time.Time `json:"lastModifiedDateTime,omitempty"` // The UTC date and time the file was last modified on a client.
}
+
+type DriveResp struct {
+ ID string `json:"id"`
+ DriveType string `json:"driveType"`
+ Quota struct {
+ Deleted uint64 `json:"deleted"`
+ Remaining uint64 `json:"remaining"`
+ State string `json:"state"`
+ Total uint64 `json:"total"`
+ Used uint64 `json:"used"`
+ } `json:"quota"`
+}
diff --git a/drivers/onedrive/util.go b/drivers/onedrive/util.go
index 3e853cd1..919e0308 100644
--- a/drivers/onedrive/util.go
+++ b/drivers/onedrive/util.go
@@ -134,6 +134,9 @@ func (d *Onedrive) _refreshToken() error {
}
func (d *Onedrive) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ if d.ref != nil {
+ return d.ref.Request(url, method, callback, resp)
+ }
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if callback != nil {
@@ -295,3 +298,21 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
}
return nil
}
+
+func (d *Onedrive) getDrive(ctx context.Context) (*DriveResp, error) {
+ var api string
+ host, _ := onedriveHostMap[d.Region]
+ if d.IsSharepoint {
+ api = fmt.Sprintf("%s/v1.0/sites/%s/drive", host.Api, d.SiteId)
+ } else {
+ api = fmt.Sprintf("%s/v1.0/me/drive", host.Api)
+ }
+ var resp DriveResp
+ _, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/onedrive_app/driver.go b/drivers/onedrive_app/driver.go
index 19d0f4cd..f28adde0 100644
--- a/drivers/onedrive_app/driver.go
+++ b/drivers/onedrive_app/driver.go
@@ -206,4 +206,20 @@ func (d *OnedriveAPP) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return err
}
+func (d *OnedriveAPP) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ if d.DisableDiskUsage {
+ return nil, errs.NotImplement
+ }
+ drive, err := d.getDrive(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: drive.Quota.Total,
+ FreeSpace: drive.Quota.Remaining,
+ },
+ }, nil
+}
+
var _ driver.Driver = (*OnedriveAPP)(nil)
diff --git a/drivers/onedrive_app/meta.go b/drivers/onedrive_app/meta.go
index 2e47f6d9..32694e6d 100644
--- a/drivers/onedrive_app/meta.go
+++ b/drivers/onedrive_app/meta.go
@@ -7,13 +7,14 @@ import (
type Addition struct {
driver.RootPath
- Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
- ClientID string `json:"client_id" required:"true"`
- ClientSecret string `json:"client_secret" required:"true"`
- TenantID string `json:"tenant_id"`
- Email string `json:"email"`
- ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
- CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
+ Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
+ ClientID string `json:"client_id" required:"true"`
+ ClientSecret string `json:"client_secret" required:"true"`
+ TenantID string `json:"tenant_id"`
+ Email string `json:"email"`
+ ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
+ CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
+ DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
}
var config = driver.Config{
diff --git a/drivers/onedrive_app/types.go b/drivers/onedrive_app/types.go
index 38e07f32..438eddfb 100644
--- a/drivers/onedrive_app/types.go
+++ b/drivers/onedrive_app/types.go
@@ -72,3 +72,15 @@ type Files struct {
Value []File `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
+
+type DriveResp struct {
+ ID string `json:"id"`
+ DriveType string `json:"driveType"`
+ Quota struct {
+ Deleted uint64 `json:"deleted"`
+ Remaining uint64 `json:"remaining"`
+ State string `json:"state"`
+ Total uint64 `json:"total"`
+ Used uint64 `json:"used"`
+ } `json:"quota"`
+}
diff --git a/drivers/onedrive_app/util.go b/drivers/onedrive_app/util.go
index 783760ff..4cc6b9c3 100644
--- a/drivers/onedrive_app/util.go
+++ b/drivers/onedrive_app/util.go
@@ -209,3 +209,16 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
}
return nil
}
+
+func (d *OnedriveAPP) getDrive(ctx context.Context) (*DriveResp, error) {
+ host, _ := onedriveHostMap[d.Region]
+ api := fmt.Sprintf("%s/v1.0/users/%s/drive", host.Api, d.Email)
+ var resp DriveResp
+ _, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
+ req.SetContext(ctx)
+ }, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
diff --git a/drivers/openlist/driver.go b/drivers/openlist/driver.go
index b539bb5a..a5368309 100644
--- a/drivers/openlist/driver.go
+++ b/drivers/openlist/driver.go
@@ -35,6 +35,9 @@ func (d *OpenList) GetAddition() driver.Additional {
func (d *OpenList) Init(ctx context.Context) error {
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
+ if d.Addition.Address == "https://alist.xiaoya.pro" {
+ return nil
+ }
var resp common.Resp[MeResp]
_, _, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
req.SetResult(&resp)
@@ -110,19 +113,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) {
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
- userAgent := base.UserAgent
if d.PassUAToUpsteam {
- userAgent = args.Header.Get("user-agent")
- if userAgent == "" {
- userAgent = base.UserAgent
+ userAgent := args.Header.Get("user-agent")
+ if userAgent != "" {
+ headers["User-Agent"] = base.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) {
req.SetResult(&resp).SetBody(FsGetReq{
Path: file.GetPath(),
Password: d.MetaPassword,
- }).SetHeader("user-agent", userAgent)
+ }).SetHeaders(headers)
})
if err != nil {
return nil, err
@@ -355,8 +368,15 @@ func (d *OpenList) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.O
return err
}
-//func (d *OpenList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
-// return nil, errs.NotSupport
-//}
+func (d *OpenList) ResolveLinkCacheMode(_ string) driver.LinkCacheMode {
+ var mode driver.LinkCacheMode
+ if d.PassIPToUpsteam {
+ mode |= driver.LinkCacheIP
+ }
+ if d.PassUAToUpsteam {
+ mode |= driver.LinkCacheUA
+ }
+ return mode
+}
var _ driver.Driver = (*OpenList)(nil)
diff --git a/drivers/openlist/meta.go b/drivers/openlist/meta.go
index fc76c142..10c950a3 100644
--- a/drivers/openlist/meta.go
+++ b/drivers/openlist/meta.go
@@ -12,6 +12,7 @@ type Addition struct {
Username string `json:"username"`
Password string `json:"password"`
Token string `json:"token"`
+ PassIPToUpsteam bool `json:"pass_ip_to_upsteam" default:"true"`
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
ForwardArchiveReq bool `json:"forward_archive_requests" default:"true"`
}
@@ -22,6 +23,7 @@ var config = driver.Config{
DefaultRoot: "/",
CheckStatus: true,
ProxyRangeOption: true,
+ LinkCacheMode: driver.LinkCacheAuto,
}
func init() {
diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index 015a8a9d..df2cce3c 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -36,7 +36,6 @@ func (d *PikPak) GetAddition() driver.Additional {
}
func (d *PikPak) Init(ctx context.Context) (err error) {
-
if d.Common == nil {
d.Common = &Common{
client: base.NewRestyClient(),
@@ -251,7 +250,7 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
}
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`·
if d.Addition.Platform == "android" {
params.Endpoint = "mypikpak.net"
@@ -264,6 +263,27 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
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) {
requestBody := base.Json{
@@ -282,7 +302,6 @@ func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir
req.SetContext(ctx).
SetBody(requestBody)
}, &resp)
-
if err != nil {
return nil, err
}
@@ -329,7 +348,6 @@ func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []
req.SetContext(ctx).
SetQueryParams(params)
}, &resp)
-
if err != nil {
return nil, fmt.Errorf("failed to get offline list: %w", err)
}
diff --git a/drivers/pikpak/types.go b/drivers/pikpak/types.go
index bea766fe..6ae78a45 100644
--- a/drivers/pikpak/types.go
+++ b/drivers/pikpak/types.go
@@ -78,7 +78,7 @@ type Media struct {
type UploadTaskData struct {
UploadType string `json:"upload_type"`
- //UPLOAD_TYPE_RESUMABLE
+ // UPLOAD_TYPE_RESUMABLE
Resumable *struct {
Kind string `json:"kind"`
Params S3Params `json:"params"`
@@ -195,3 +195,15 @@ type CaptchaTokenResponse struct {
ExpiresIn int64 `json:"expires_in"`
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"`
+}
diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go
new file mode 100644
index 00000000..06aa645d
--- /dev/null
+++ b/drivers/proton_drive/driver.go
@@ -0,0 +1,290 @@
+package protondrive
+
+/*
+Package protondrive
+Author: Da3zKi7
+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: &model.FileRangeReader{
+ RangeReaderIF: 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)
diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go
new file mode 100644
index 00000000..acf65677
--- /dev/null
+++ b/drivers/proton_drive/meta.go
@@ -0,0 +1,56 @@
+package protondrive
+
+/*
+Package protondrive
+Author: Da3zKi7
+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",
+ }
+ })
+}
diff --git a/drivers/proton_drive/types.go b/drivers/proton_drive/types.go
new file mode 100644
index 00000000..ed6dcd19
--- /dev/null
+++ b/drivers/proton_drive/types.go
@@ -0,0 +1,38 @@
+package protondrive
+
+/*
+Package protondrive
+Author: Da3zKi7
+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"`
+}
diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go
new file mode 100644
index 00000000..a59bb6cb
--- /dev/null
+++ b/drivers/proton_drive/util.go
@@ -0,0 +1,670 @@
+package protondrive
+
+/*
+Package protondrive
+Author: Da3zKi7
+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
+}
diff --git a/drivers/quark_uc_tv/driver.go b/drivers/quark_uc_tv/driver.go
index 05735f87..dc062d9f 100644
--- a/drivers/quark_uc_tv/driver.go
+++ b/drivers/quark_uc_tv/driver.go
@@ -95,14 +95,22 @@ func (d *QuarkUCTV) List(ctx context.Context, dir model.Obj, args model.ListArgs
files := make([]model.Obj, 0)
pageIndex := int64(0)
pageSize := int64(100)
+ desc := "1"
+ orderBy := "3"
+ if d.OrderDirection == "asc" {
+ desc = "0"
+ }
+ if d.OrderBy == "file_name" {
+ orderBy = "1"
+ }
for {
var filesData FilesData
_, err := d.request(ctx, "/file", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"method": "list",
"parent_fid": dir.GetID(),
- "order_by": "3",
- "desc": "1",
+ "order_by": orderBy,
+ "desc": desc,
"category": "",
"source": "",
"ex_source": "",
@@ -161,7 +169,7 @@ func (d *QuarkUCTV) Put(ctx context.Context, dstDir model.Obj, stream model.File
}
type QuarkUCTVCommon struct {
- AccessToken string `json:"access_token"`
+ AccessToken string
}
var _ driver.Driver = (*QuarkUCTV)(nil)
diff --git a/drivers/quark_uc_tv/meta.go b/drivers/quark_uc_tv/meta.go
index 81c9c7ef..a8af1a13 100644
--- a/drivers/quark_uc_tv/meta.go
+++ b/drivers/quark_uc_tv/meta.go
@@ -8,6 +8,8 @@ import (
type Addition struct {
// Usually one of two
driver.RootID
+ OrderBy string `json:"order_by" type:"select" options:"file_name,updated_at" default:"updated_at"`
+ OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"desc"`
// define other
RefreshToken string `json:"refresh_token" required:"false" default:""`
// 必要且影响登录,由签名决定
diff --git a/drivers/sftp/driver.go b/drivers/sftp/driver.go
index 7de24248..a3b3a96d 100644
--- a/drivers/sftp/driver.go
+++ b/drivers/sftp/driver.go
@@ -4,6 +4,7 @@ import (
"context"
"os"
"path"
+ "strings"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -68,15 +69,10 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
Limiter: stream.ServerDownloadLimit,
Ctx: ctx,
}
- if !d.Config().OnlyLinkMFile {
- return &model.Link{
- RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
- SyncClosers: utils.NewSyncClosers(remoteFile),
- }, nil
- }
return &model.Link{
- MFile: mFile,
- SyncClosers: utils.NewSyncClosers(remoteFile),
+ RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
+ SyncClosers: utils.NewSyncClosers(remoteFile),
+ RequireReference: true,
}, nil
}
@@ -127,4 +123,22 @@ func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStrea
return err
}
+func (d *SFTP) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ stat, err := d.client.StatVFS(d.RootFolderPath)
+ if err != nil {
+ if strings.Contains(err.Error(), "unimplemented") {
+ return nil, errs.NotImplement
+ }
+ return nil, err
+ }
+ total := stat.Blocks * stat.Bsize
+ free := stat.Bfree * stat.Bsize
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: total,
+ FreeSpace: free,
+ },
+ }, nil
+}
+
var _ driver.Driver = (*SFTP)(nil)
diff --git a/drivers/sftp/meta.go b/drivers/sftp/meta.go
index 9dada9ef..1c9bd3e3 100644
--- a/drivers/sftp/meta.go
+++ b/drivers/sftp/meta.go
@@ -16,12 +16,12 @@ type Addition struct {
}
var config = driver.Config{
- Name: "SFTP",
- LocalSort: true,
- OnlyLinkMFile: false,
- DefaultRoot: "/",
- CheckStatus: true,
- NoLinkURL: true,
+ Name: "SFTP",
+ LocalSort: true,
+ OnlyProxy: true,
+ DefaultRoot: "/",
+ CheckStatus: true,
+ NoLinkURL: true,
}
func init() {
diff --git a/drivers/smb/driver.go b/drivers/smb/driver.go
index 3e12f122..7d4b0cb9 100644
--- a/drivers/smb/driver.go
+++ b/drivers/smb/driver.go
@@ -11,7 +11,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
- "github.com/hirochachacha/go-smb2"
+ "github.com/cloudsoda/go-smb2"
)
type SMB struct {
@@ -33,7 +33,7 @@ func (d *SMB) Init(ctx context.Context) error {
if !strings.Contains(d.Addition.Address, ":") {
d.Addition.Address = d.Addition.Address + ":445"
}
- return d._initFS()
+ return d._initFS(ctx)
}
func (d *SMB) Drop(ctx context.Context) error {
@@ -44,7 +44,7 @@ func (d *SMB) Drop(ctx context.Context) error {
}
func (d *SMB) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return nil, err
}
fullPath := dir.GetPath()
@@ -71,7 +71,7 @@ func (d *SMB) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]m
}
func (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return nil, err
}
fullPath := file.GetPath()
@@ -86,20 +86,15 @@ func (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*m
Limiter: stream.ServerDownloadLimit,
Ctx: ctx,
}
- if !d.Config().OnlyLinkMFile {
- return &model.Link{
- RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
- SyncClosers: utils.NewSyncClosers(remoteFile),
- }, nil
- }
return &model.Link{
- MFile: mFile,
- SyncClosers: utils.NewSyncClosers(remoteFile),
+ RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), mFile),
+ SyncClosers: utils.NewSyncClosers(remoteFile),
+ RequireReference: true,
}, nil
}
func (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return err
}
fullPath := filepath.Join(parentDir.GetPath(), dirName)
@@ -113,7 +108,7 @@ func (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string)
}
func (d *SMB) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return err
}
srcPath := srcObj.GetPath()
@@ -128,7 +123,7 @@ func (d *SMB) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (d *SMB) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return err
}
srcPath := srcObj.GetPath()
@@ -143,7 +138,7 @@ func (d *SMB) Rename(ctx context.Context, srcObj model.Obj, newName string) erro
}
func (d *SMB) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return err
}
srcPath := srcObj.GetPath()
@@ -163,7 +158,7 @@ func (d *SMB) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (d *SMB) Remove(ctx context.Context, obj model.Obj) error {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return err
}
var err error
@@ -182,7 +177,7 @@ func (d *SMB) Remove(ctx context.Context, obj model.Obj) error {
}
func (d *SMB) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
- if err := d.checkConn(); err != nil {
+ if err := d.checkConn(ctx); err != nil {
return err
}
fullPath := filepath.Join(dstDir.GetPath(), stream.GetName())
@@ -205,6 +200,22 @@ func (d *SMB) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
return nil
}
+func (d *SMB) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ if err := d.checkConn(ctx); err != nil {
+ return nil, err
+ }
+ stat, err := d.fs.Statfs(d.RootFolderPath)
+ if err != nil {
+ return nil, err
+ }
+ return &model.StorageDetails{
+ DiskUsage: model.DiskUsage{
+ TotalSpace: stat.BlockSize() * stat.TotalBlockCount(),
+ FreeSpace: stat.BlockSize() * stat.AvailableBlockCount(),
+ },
+ }, nil
+}
+
//func (d *SMB) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
diff --git a/drivers/smb/meta.go b/drivers/smb/meta.go
index 87a98277..ce0e5d3b 100644
--- a/drivers/smb/meta.go
+++ b/drivers/smb/meta.go
@@ -14,12 +14,12 @@ type Addition struct {
}
var config = driver.Config{
- Name: "SMB",
- LocalSort: true,
- OnlyLinkMFile: false,
- DefaultRoot: ".",
- NoCache: true,
- NoLinkURL: true,
+ Name: "SMB",
+ LocalSort: true,
+ OnlyProxy: true,
+ DefaultRoot: ".",
+ NoCache: true,
+ NoLinkURL: true,
}
func init() {
diff --git a/drivers/smb/util.go b/drivers/smb/util.go
index 3e40f813..6ae365f8 100644
--- a/drivers/smb/util.go
+++ b/drivers/smb/util.go
@@ -1,9 +1,9 @@
package smb
import (
+ "context"
"fmt"
"io/fs"
- "net"
"os"
"path/filepath"
"sync/atomic"
@@ -12,7 +12,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
- "github.com/hirochachacha/go-smb2"
+ "github.com/cloudsoda/go-smb2"
)
func (d *SMB) updateLastConnTime() {
@@ -27,24 +27,20 @@ func (d *SMB) getLastConnTime() time.Time {
return time.Unix(atomic.LoadInt64(&d.lastConnTime), 0)
}
-func (d *SMB) initFS() error {
+func (d *SMB) initFS(ctx context.Context) error {
_, err, _ := singleflight.AnyGroup.Do(fmt.Sprintf("SMB.initFS:%p", d), func() (any, error) {
- return nil, d._initFS()
+ return nil, d._initFS(ctx)
})
return err
}
-func (d *SMB) _initFS() error {
- conn, err := net.Dial("tcp", d.Address)
- if err != nil {
- return err
- }
+func (d *SMB) _initFS(ctx context.Context) error {
dialer := &smb2.Dialer{
Initiator: &smb2.NTLMInitiator{
User: d.Username,
Password: d.Password,
},
}
- s, err := dialer.Dial(conn)
+ s, err := dialer.Dial(ctx, d.Address)
if err != nil {
return err
}
@@ -56,14 +52,14 @@ func (d *SMB) _initFS() error {
return err
}
-func (d *SMB) checkConn() error {
+func (d *SMB) checkConn(ctx context.Context) error {
if time.Since(d.getLastConnTime()) < 5*time.Minute {
return nil
}
if d.fs != nil {
_ = d.fs.Umount()
}
- return d.initFS()
+ return d.initFS(ctx)
}
// CopyFile File copies a single file from src to dst
diff --git a/drivers/strm/driver.go b/drivers/strm/driver.go
index 80af52d4..e4482cf9 100644
--- a/drivers/strm/driver.go
+++ b/drivers/strm/driver.go
@@ -12,6 +12,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"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/server/common"
)
@@ -108,7 +109,7 @@ func (d *Strm) Get(ctx context.Context, path string) (model.Obj, error) {
if err != nil {
continue
}
- // fs.Get 没报错,说明不是strm生成的路径,需要直接返回
+ // fs.Get 没报错,说明不是strm驱动映射的路径,需要直接返回
size := int64(0)
if !obj.IsDir() {
size = obj.GetSize()
@@ -123,6 +124,11 @@ func (d *Strm) Get(ctx context.Context, path string) (model.Obj, error) {
HashInfo: obj.GetHash(),
}, nil
}
+ if strings.HasSuffix(path, ".strm") {
+ // 上面fs.Get都没找到且后缀为.strm
+ // 返回errs.NotSupport使得op.Get尝试从op.List中查找
+ return nil, errs.NotSupport
+ }
return nil, errs.ObjectNotFound
}
@@ -151,7 +157,7 @@ func (d *Strm) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
if file.GetID() == "strm" {
link := d.getLink(ctx, file.GetPath())
return &model.Link{
- MFile: strings.NewReader(link),
+ RangeReader: stream.GetRangeReaderFromMFile(int64(len(link)), strings.NewReader(link)),
}, nil
}
// ftp,s3
diff --git a/drivers/strm/meta.go b/drivers/strm/meta.go
index 0f3e0a99..d3c33164 100644
--- a/drivers/strm/meta.go
+++ b/drivers/strm/meta.go
@@ -15,14 +15,13 @@ type Addition struct {
}
var config = driver.Config{
- Name: "Strm",
- LocalSort: true,
- NoCache: true,
- NoUpload: true,
- DefaultRoot: "/",
- OnlyLinkMFile: true,
- OnlyProxy: true,
- NoLinkURL: true,
+ Name: "Strm",
+ LocalSort: true,
+ OnlyProxy: true,
+ NoCache: true,
+ NoUpload: true,
+ DefaultRoot: "/",
+ NoLinkURL: true,
}
func init() {
diff --git a/drivers/teldrive/types.go b/drivers/teldrive/types.go
index f6399e06..084f967e 100644
--- a/drivers/teldrive/types.go
+++ b/drivers/teldrive/types.go
@@ -2,6 +2,7 @@ package teldrive
import (
"context"
+ "io"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/model"
@@ -50,8 +51,8 @@ type chunkTask struct {
chunkIdx int
fileName string
chunkSize int64
- reader *stream.SectionReader
- ss *stream.StreamSectionReader
+ reader io.ReadSeeker
+ ss stream.StreamSectionReaderIF
}
type CopyManager struct {
diff --git a/drivers/teldrive/upload.go b/drivers/teldrive/upload.go
index 168d9bef..4f717dc8 100644
--- a/drivers/teldrive/upload.go
+++ b/drivers/teldrive/upload.go
@@ -164,7 +164,7 @@ func (d *Teldrive) doSingleUpload(ctx context.Context, dstDir model.Obj, file mo
if err := d.singleUploadRequest(fileId, func(req *resty.Request) {
uploadParams := map[string]string{
"partName": func() string {
- digits := len(fmt.Sprintf("%d", totalParts))
+ digits := len(strconv.Itoa(totalParts))
return file.GetName() + fmt.Sprintf(".%0*d", digits, 1)
}(),
"partNo": strconv.Itoa(1),
@@ -333,7 +333,7 @@ func (d *Teldrive) uploadSingleChunk(ctx context.Context, fileId string, task ch
err := d.singleUploadRequest(fileId, func(req *resty.Request) {
uploadParams := map[string]string{
"partName": func() string {
- digits := len(fmt.Sprintf("%d", totalParts))
+ digits := len(strconv.Itoa(totalParts))
return task.fileName + fmt.Sprintf(".%0*d", digits, task.chunkIdx)
}(),
"partNo": strconv.Itoa(task.chunkIdx),
diff --git a/drivers/template/driver.go b/drivers/template/driver.go
index 5587dfea..477ca7f7 100644
--- a/drivers/template/driver.go
+++ b/drivers/template/driver.go
@@ -93,6 +93,11 @@ func (d *Template) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.O
return nil, errs.NotImplement
}
+func (d *Template) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
+ // TODO return storage details (total space, free space, etc.)
+ return nil, errs.NotImplement
+}
+
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
diff --git a/drivers/template/meta.go b/drivers/template/meta.go
index a546e676..f525b5a9 100644
--- a/drivers/template/meta.go
+++ b/drivers/template/meta.go
@@ -16,7 +16,6 @@ type Addition struct {
var config = driver.Config{
Name: "Template",
LocalSort: false,
- OnlyLinkMFile: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
diff --git a/drivers/virtual/driver.go b/drivers/virtual/driver.go
index 1d14427c..6e6bd087 100644
--- a/drivers/virtual/driver.go
+++ b/drivers/virtual/driver.go
@@ -2,11 +2,11 @@ package virtual
import (
"context"
- "io"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
+ "github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
)
@@ -42,16 +42,14 @@ func (d *Virtual) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return res, nil
}
-type DummyMFile struct {
- io.Reader
-}
+type DummyMFile struct{}
func (f DummyMFile) Read(p []byte) (n int, err error) {
- return f.Reader.Read(p)
+ return random.Rand.Read(p)
}
func (f DummyMFile) ReadAt(p []byte, off int64) (n int, err error) {
- return f.Reader.Read(p)
+ return random.Rand.Read(p)
}
func (DummyMFile) Seek(offset int64, whence int) (int64, error) {
@@ -60,7 +58,7 @@ func (DummyMFile) Seek(offset int64, whence int) (int64, error) {
func (d *Virtual) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
return &model.Link{
- MFile: DummyMFile{Reader: random.Rand},
+ RangeReader: stream.GetRangeReaderFromMFile(file.GetSize(), DummyMFile{}),
}, nil
}
diff --git a/drivers/virtual/meta.go b/drivers/virtual/meta.go
index f567830a..c270ec12 100644
--- a/drivers/virtual/meta.go
+++ b/drivers/virtual/meta.go
@@ -14,11 +14,11 @@ type Addition struct {
}
var config = driver.Config{
- Name: "Virtual",
- OnlyLinkMFile: true,
- LocalSort: true,
- NeedMs: true,
- NoLinkURL: true,
+ Name: "Virtual",
+ LocalSort: true,
+ OnlyProxy: true,
+ NeedMs: true,
+ NoLinkURL: true,
}
func init() {
diff --git a/drivers/wopan/util.go b/drivers/wopan/util.go
index 6b8918b0..fec5122b 100644
--- a/drivers/wopan/util.go
+++ b/drivers/wopan/util.go
@@ -36,5 +36,6 @@ func (d *Wopan) getSpaceType() string {
// 20230607214351
func getTime(str string) (time.Time, error) {
- return time.Parse("20060102150405", str)
+ loc := time.FixedZone("UTC+8", 8*60*60)
+ return time.ParseInLocation("20060102150405", str, loc)
}
diff --git a/go.mod b/go.mod
index da8a1974..263eb479 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
github.com/OpenListTeam/times v0.1.0
github.com/OpenListTeam/wopan-sdk-go v0.1.5
github.com/ProtonMail/go-crypto v1.3.0
- github.com/power721/115driver v0.2.5
+ github.com/ProtonMail/gopenpgp/v2 v2.9.0
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/avast/retry-go v3.0.0+incompatible
github.com/aws/aws-sdk-go v1.55.7
@@ -21,6 +21,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.1.0
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e
+ github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc
github.com/coreos/go-oidc v2.3.0+incompatible
github.com/deckarep/golang-set/v2 v2.8.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
@@ -37,8 +38,9 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
+ github.com/halalcloud/golang-sdk-lite v0.0.0-20251006164234-3c629727c499
github.com/hekmon/transmissionrpc/v3 v3.0.0
- github.com/hirochachacha/go-smb2 v1.1.0
+ github.com/henrybear327/go-proton-api v1.0.0
github.com/ipfs/go-ipfs-api v0.7.0
github.com/itsHenry35/gofakes3 v0.0.8
github.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3
@@ -51,7 +53,9 @@ require (
github.com/ncw/swift/v2 v2.0.4
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.9
+ github.com/power721/115driver v0.2.5
github.com/pquerna/otp v1.5.0
+ github.com/quic-go/quic-go v0.54.1
github.com/rclone/rclone v1.70.3
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/shirou/gopsutil/v4 v4.25.5
@@ -60,6 +64,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/t3rm1n4l/go-mega v0.0.0-20241213151442-a19cff0ec7b5
+ github.com/tchap/go-patricia/v2 v2.3.3
github.com/u2takey/ffmpeg-go v0.5.0
github.com/upyun/go-sdk/v3 v3.0.4
github.com/winfsp/cgofuse v1.6.0
@@ -81,18 +86,42 @@ require (
require (
cloud.google.com/go/compute/metadata v0.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
+ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
+ github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e // indirect
+ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
+ github.com/ProtonMail/go-srp v0.0.7 // indirect
+ github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
+ github.com/andybalholm/cascadia v1.3.3 // indirect
+ github.com/bradenaw/juniper v0.15.3 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/cronokirby/saferith v0.33.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
+ github.com/emersion/go-message v0.18.2 // indirect
+ github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff // indirect
+ github.com/geoffgarside/ber v1.2.0 // indirect
+ github.com/hashicorp/go-uuid v1.0.3 // indirect
+ github.com/jcmturner/aescts/v2 v2.0.0 // indirect
+ github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
+ github.com/jcmturner/gofork v1.7.6 // indirect
+ github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
+ github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
+ github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/lanrat/extsort v1.0.2 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.0 // indirect
github.com/minio/xxml v0.0.3 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/otiai10/mint v1.6.3 // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
+ github.com/relvacode/iso8601 v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ go.uber.org/mock v0.5.0 // indirect
+ golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
+ golang.org/x/mod v0.27.0 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
)
@@ -168,7 +197,6 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
- github.com/geoffgarside/ber v1.2.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -185,6 +213,7 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
+ github.com/henrybear327/Proton-API-Bridge v1.0.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/go-cid v0.5.0
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -254,11 +283,11 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
golang.org/x/arch v0.18.0 // indirect
- golang.org/x/sync v0.16.0 // indirect
- golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/sync v0.16.0
+ golang.org/x/sys v0.34.0
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0
- golang.org/x/tools v0.34.0 // indirect
+ golang.org/x/tools v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6 // indirect
@@ -268,4 +297,8 @@ require (
lukechampine.com/blake3 v1.1.7 // indirect
)
+replace github.com/ProtonMail/go-proton-api => github.com/henrybear327/go-proton-api v1.0.0
+
+replace github.com/cronokirby/saferith => github.com/Da3zKi7/saferith v0.33.0-fixed
+
// replace github.com/OpenListTeam/115-sdk-go => ../../OpenListTeam/115-sdk-go
diff --git a/go.sum b/go.sum
index c3a333df..b3377c27 100644
--- a/go.sum
+++ b/go.sum
@@ -37,6 +37,10 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Da3zKi7/saferith v0.33.0-fixed h1:fnIWTk7EP9mZAICf7aQjeoAwpfrlCrkOvqmi6CbWdTk=
+github.com/Da3zKi7/saferith v0.33.0-fixed/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
+github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
github.com/OpenListTeam/115-sdk-go v0.2.2 h1:JCrGHqQjBX3laOA6Hw4CuBovSg7g+FC5s0LEAYsRciU=
@@ -53,14 +57,26 @@ github.com/OpenListTeam/times v0.1.0 h1:qknxw+qj5CYKgXAwydA102UEpPcpU8TYNGRmwRyP
github.com/OpenListTeam/times v0.1.0/go.mod h1:Jx7qen5NCYzKk2w14YuvU48YYMcPa1P9a+EJePC15Pc=
github.com/OpenListTeam/wopan-sdk-go v0.1.5 h1:iKKcVzIqBgtGDbn0QbdWrCazSGxXFmYFyrnFBG+U8dI=
github.com/OpenListTeam/wopan-sdk-go v0.1.5/go.mod h1:otynv0CgSNUClPpUgZ44qCZGcMRe0dc83Pkk65xAunI=
+github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
+github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
+github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
+github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e h1:lCsqUUACrcMC83lg5rTo9Y0PnPItE61JSfvMyIcANwk=
+github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
+github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
+github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
+github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
+github.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFUPP/+/kE=
+github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s=
+github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
+github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
-github.com/SheltonZhu/115driver v1.1.1 h1:9EMhe2ZJflGiAaZbYInw2jqxTcqZNF+DtVDsEy70aFU=
-github.com/SheltonZhu/115driver v1.1.1/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
@@ -72,6 +88,8 @@ github.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAP
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
@@ -164,6 +182,9 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/bradenaw/juniper v0.15.3 h1:RHIAMEDTpvmzV1wg1jMAHGOoI2oJUSPx3lxRldXnFGo=
+github.com/bradenaw/juniper v0.15.3/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27BOs5z9dq8=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -198,8 +219,13 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e h1:GLC8iDDcbt1H8+RkNao2nRGjyNTIo81e1rAJT9/uWYA=
github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e/go.mod h1:ln9Whp+wVY/FTbn2SK0ag+SKD2fC0yQCF/Lqowc1LmU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc h1:t8YjNUCt1DimB4HCIXBztwWMhgxr5yG5/YaRl9Afdfg=
+github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc/go.mod h1:CgWpFCFWzzEA5hVkhAc6DZZzGd3czx+BblvOzjmg6KA=
+github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs=
+github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
@@ -235,6 +261,10 @@ github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJL
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
+github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
+github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
+github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -254,7 +284,6 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
-github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
@@ -355,8 +384,14 @@ github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/halalcloud/golang-sdk-lite v0.0.0-20251006164234-3c629727c499 h1:4ovnBdiGDFi8putQGxhipuuhXItAgh4/YnzufPYkZkQ=
+github.com/halalcloud/golang-sdk-lite v0.0.0-20251006164234-3c629727c499/go.mod h1:8x1h4rm3s8xMcTyJrq848sQ6BJnKzl57mDY4CNshdPM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -364,6 +399,9 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -375,8 +413,10 @@ github.com/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI
github.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M=
github.com/hekmon/transmissionrpc/v3 v3.0.0 h1:0Fb11qE0IBh4V4GlOwHNYpqpjcYDp5GouolwrpmcUDQ=
github.com/hekmon/transmissionrpc/v3 v3.0.0/go.mod h1:38SlNhFzinVUuY87wGj3acOmRxeYZAZfrj6Re7UgCDg=
-github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
-github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
+github.com/henrybear327/Proton-API-Bridge v1.0.0 h1:gjKAaWfKu++77WsZTHg6FUyPC5W0LTKWQciUm8PMZb0=
+github.com/henrybear327/Proton-API-Bridge v1.0.0/go.mod h1:gunH16hf6U74W2b9CGDaWRadiLICsoJ6KRkSt53zLts=
+github.com/henrybear327/go-proton-api v1.0.0 h1:zYi/IbjLwFAW7ltCeqXneUGJey0TN//Xo851a/BgLXw=
+github.com/henrybear327/go-proton-api v1.0.0/go.mod h1:w63MZuzufKcIZ93pwRgiOtxMXYafI8H74D77AxytOBc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -396,6 +436,18 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -567,8 +619,14 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
+github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
+github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rclone/rclone v1.70.3 h1:rg/WNh4DmSVZyKP2tHZ4lAaWEyMi7h/F0r7smOMA3IE=
github.com/rclone/rclone v1.70.3/go.mod h1:nLyN+hpxAsQn9Rgt5kM774lcRDad82x/KqQeBZ83cMo=
+github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU=
+github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4=
github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -626,6 +684,8 @@ github.com/t3rm1n4l/go-mega v0.0.0-20241213151442-a19cff0ec7b5 h1:Sa+sR8aaAMFwxh
github.com/t3rm1n4l/go-mega v0.0.0-20241213151442-a19cff0ec7b5/go.mod h1:UdZiFUFu6e2WjjtjxivwXWcwc1N/8zgbkBR9QNucUOY=
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6KqyMNo//xk8B8o6zW9/RVmy1VamOs=
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543/go.mod h1:jpwqYA8KUVEvSUJHkCXsnBRJCSKP1BMa81QZ6kvRpow=
+github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
+github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
@@ -682,6 +742,10 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
@@ -693,8 +757,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
@@ -736,6 +801,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -748,16 +815,19 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -800,6 +870,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -818,6 +889,7 @@ golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXct
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
@@ -833,6 +905,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@@ -874,8 +947,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
-golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/archive/archives/archives.go b/internal/archive/archives/archives.go
index d9c59aa9..6f6245a8 100644
--- a/internal/archive/archives/archives.go
+++ b/internal/archive/archives/archives.go
@@ -1,10 +1,11 @@
package archives
import (
+ "fmt"
"io"
"io/fs"
"os"
- stdpath "path"
+ "path/filepath"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/archive/tool"
@@ -107,7 +108,7 @@ func (Archives) Decompress(ss []*stream.SeekableStream, outputPath string, args
}
if stat.IsDir() {
isDir = true
- outputPath = stdpath.Join(outputPath, stat.Name())
+ outputPath = filepath.Join(outputPath, stat.Name())
err = os.Mkdir(outputPath, 0700)
if err != nil {
return filterPassword(err)
@@ -120,11 +121,14 @@ func (Archives) Decompress(ss []*stream.SeekableStream, outputPath string, args
return err
}
relPath := strings.TrimPrefix(p, path+"/")
- dstPath := stdpath.Join(outputPath, relPath)
+ dstPath := filepath.Join(outputPath, relPath)
+ if !strings.HasPrefix(dstPath, outputPath+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", relPath)
+ }
if d.IsDir() {
err = os.MkdirAll(dstPath, 0700)
} else {
- dir := stdpath.Dir(dstPath)
+ dir := filepath.Dir(dstPath)
err = decompress(fsys, p, dir, func(_ float64) {})
}
return err
diff --git a/internal/archive/archives/utils.go b/internal/archive/archives/utils.go
index ce84da09..ddead84c 100644
--- a/internal/archive/archives/utils.go
+++ b/internal/archive/archives/utils.go
@@ -1,10 +1,11 @@
package archives
import (
+ "fmt"
"io"
fs2 "io/fs"
"os"
- stdpath "path"
+ "path/filepath"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -69,7 +70,11 @@ func decompress(fsys fs2.FS, filePath, targetPath string, up model.UpdateProgres
if err != nil {
return err
}
- f, err := os.OpenFile(stdpath.Join(targetPath, stat.Name()), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ destPath := filepath.Join(targetPath, stat.Name())
+ if !strings.HasPrefix(destPath, targetPath+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", stat.Name())
+ }
+ f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
diff --git a/internal/archive/iso9660/iso9660.go b/internal/archive/iso9660/iso9660.go
index eb4e975d..7b26dcae 100644
--- a/internal/archive/iso9660/iso9660.go
+++ b/internal/archive/iso9660/iso9660.go
@@ -1,9 +1,11 @@
package iso9660
import (
+ "fmt"
"io"
"os"
- stdpath "path"
+ "path/filepath"
+ "strings"
"github.com/OpenListTeam/OpenList/v4/internal/archive/tool"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -79,7 +81,11 @@ func (ISO9660) Decompress(ss []*stream.SeekableStream, outputPath string, args m
}
if obj.IsDir() {
if args.InnerPath != "/" {
- outputPath = stdpath.Join(outputPath, obj.Name())
+ rootpath := outputPath
+ outputPath = filepath.Join(outputPath, obj.Name())
+ if !strings.HasPrefix(outputPath, rootpath+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", obj.Name())
+ }
if err = os.MkdirAll(outputPath, 0700); err != nil {
return err
}
diff --git a/internal/archive/iso9660/utils.go b/internal/archive/iso9660/utils.go
index e3326b9b..0e915133 100644
--- a/internal/archive/iso9660/utils.go
+++ b/internal/archive/iso9660/utils.go
@@ -1,8 +1,9 @@
package iso9660
import (
+ "fmt"
"os"
- stdpath "path"
+ "path/filepath"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -62,7 +63,11 @@ func toModelObj(file *iso9660.File) model.Obj {
}
func decompress(f *iso9660.File, path string, up model.UpdateProgress) error {
- file, err := os.OpenFile(stdpath.Join(path, f.Name()), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ destPath := filepath.Join(path, f.Name())
+ if !strings.HasPrefix(destPath, path+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", f.Name())
+ }
+ file, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
@@ -84,7 +89,10 @@ func decompressAll(children []*iso9660.File, path string) error {
if err != nil {
return err
}
- nextPath := stdpath.Join(path, child.Name())
+ nextPath := filepath.Join(path, child.Name())
+ if !strings.HasPrefix(nextPath, path+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", child.Name())
+ }
if err = os.MkdirAll(nextPath, 0700); err != nil {
return err
}
diff --git a/internal/archive/rardecode/rardecode.go b/internal/archive/rardecode/rardecode.go
index d2c6a448..13a22e3e 100644
--- a/internal/archive/rardecode/rardecode.go
+++ b/internal/archive/rardecode/rardecode.go
@@ -3,7 +3,7 @@ package rardecode
import (
"io"
"os"
- stdpath "path"
+ "path/filepath"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/archive/tool"
@@ -93,7 +93,7 @@ func (RarDecoder) Decompress(ss []*stream.SeekableStream, outputPath string, arg
}
} else {
innerPath := strings.TrimPrefix(args.InnerPath, "/")
- innerBase := stdpath.Base(innerPath)
+ innerBase := filepath.Base(innerPath)
createdBaseDir := false
for {
var header *rardecode.FileHeader
@@ -115,7 +115,7 @@ func (RarDecoder) Decompress(ss []*stream.SeekableStream, outputPath string, arg
}
break
} else if strings.HasPrefix(name, innerPath+"/") {
- targetPath := stdpath.Join(outputPath, innerBase)
+ targetPath := filepath.Join(outputPath, innerBase)
if !createdBaseDir {
err = os.Mkdir(targetPath, 0700)
if err != nil {
diff --git a/internal/archive/rardecode/utils.go b/internal/archive/rardecode/utils.go
index 93a71da9..e933005a 100644
--- a/internal/archive/rardecode/utils.go
+++ b/internal/archive/rardecode/utils.go
@@ -5,7 +5,7 @@ import (
"io"
"io/fs"
"os"
- stdpath "path"
+ "path/filepath"
"sort"
"strings"
"time"
@@ -124,7 +124,7 @@ type WrapFileInfo struct {
}
func (f *WrapFileInfo) Name() string {
- return stdpath.Base(f.File.Name)
+ return filepath.Base(f.File.Name)
}
func (f *WrapFileInfo) Size() int64 {
@@ -183,12 +183,16 @@ func getReader(ss []*stream.SeekableStream, password string) (*rardecode.Reader,
func decompress(reader *rardecode.Reader, header *rardecode.FileHeader, filePath, outputPath string) error {
targetPath := outputPath
- dir, base := stdpath.Split(filePath)
+ dir, base := filepath.Split(filePath)
if dir != "" {
- targetPath = stdpath.Join(targetPath, dir)
- err := os.MkdirAll(targetPath, 0700)
- if err != nil {
- return err
+ targetPath = filepath.Join(targetPath, dir)
+ if strings.HasPrefix(targetPath, outputPath+string(os.PathSeparator)) {
+ err := os.MkdirAll(targetPath, 0700)
+ if err != nil {
+ return err
+ }
+ } else {
+ targetPath = outputPath
}
}
if base != "" {
@@ -201,7 +205,11 @@ func decompress(reader *rardecode.Reader, header *rardecode.FileHeader, filePath
}
func _decompress(reader *rardecode.Reader, header *rardecode.FileHeader, targetPath string, up model.UpdateProgress) error {
- f, err := os.OpenFile(stdpath.Join(targetPath, stdpath.Base(header.Name)), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ destPath := filepath.Join(targetPath, filepath.Base(header.Name))
+ if !strings.HasPrefix(destPath, targetPath+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", filepath.Base(header.Name))
+ }
+ f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
diff --git a/internal/archive/tool/helper.go b/internal/archive/tool/helper.go
index 6b5658a9..adbe56ed 100644
--- a/internal/archive/tool/helper.go
+++ b/internal/archive/tool/helper.go
@@ -1,10 +1,11 @@
package tool
import (
+ "fmt"
"io"
"io/fs"
"os"
- stdpath "path"
+ "path/filepath"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/model"
@@ -40,13 +41,13 @@ func GenerateMetaTreeFromFolderTraversal(r ArchiveReader) (bool, []model.ObjTree
isNewFolder := false
if !file.FileInfo().IsDir() {
// 先将 文件 添加到 所在的文件夹
- dir = stdpath.Dir(name)
+ dir = filepath.Dir(name)
dirObj = dirMap[dir]
if dirObj == nil {
isNewFolder = dir != "."
dirObj = &model.ObjectTree{}
dirObj.IsFolder = true
- dirObj.Name = stdpath.Base(dir)
+ dirObj.Name = filepath.Base(dir)
dirObj.Modified = file.FileInfo().ModTime()
dirMap[dir] = dirObj
}
@@ -64,28 +65,28 @@ func GenerateMetaTreeFromFolderTraversal(r ArchiveReader) (bool, []model.ObjTree
dirMap[dir] = dirObj
}
dirObj.IsFolder = true
- dirObj.Name = stdpath.Base(dir)
+ dirObj.Name = filepath.Base(dir)
dirObj.Modified = file.FileInfo().ModTime()
}
if isNewFolder {
// 将 文件夹 添加到 父文件夹
// 考虑压缩包仅记录文件的路径,不记录文件夹
// 循环创建所有父文件夹
- parentDir := stdpath.Dir(dir)
+ parentDir := filepath.Dir(dir)
for {
parentDirObj := dirMap[parentDir]
if parentDirObj == nil {
parentDirObj = &model.ObjectTree{}
if parentDir != "." {
parentDirObj.IsFolder = true
- parentDirObj.Name = stdpath.Base(parentDir)
+ parentDirObj.Name = filepath.Base(parentDir)
parentDirObj.Modified = file.FileInfo().ModTime()
}
dirMap[parentDir] = parentDirObj
}
parentDirObj.Children = append(parentDirObj.Children, dirObj)
- parentDir = stdpath.Dir(parentDir)
+ parentDir = filepath.Dir(parentDir)
if dirMap[parentDir] != nil {
break
}
@@ -127,7 +128,7 @@ func DecompressFromFolderTraversal(r ArchiveReader, outputPath string, args mode
}
} else {
innerPath := strings.TrimPrefix(args.InnerPath, "/")
- innerBase := stdpath.Base(innerPath)
+ innerBase := filepath.Base(innerPath)
createdBaseDir := false
for _, file := range files {
name := file.Name()
@@ -138,7 +139,7 @@ func DecompressFromFolderTraversal(r ArchiveReader, outputPath string, args mode
}
break
} else if strings.HasPrefix(name, innerPath+"/") {
- targetPath := stdpath.Join(outputPath, innerBase)
+ targetPath := filepath.Join(outputPath, innerBase)
if !createdBaseDir {
err = os.Mkdir(targetPath, 0700)
if err != nil {
@@ -159,12 +160,16 @@ func DecompressFromFolderTraversal(r ArchiveReader, outputPath string, args mode
func decompress(file SubFile, filePath, outputPath, password string) error {
targetPath := outputPath
- dir, base := stdpath.Split(filePath)
+ dir, base := filepath.Split(filePath)
if dir != "" {
- targetPath = stdpath.Join(targetPath, dir)
- err := os.MkdirAll(targetPath, 0700)
- if err != nil {
- return err
+ targetPath = filepath.Join(targetPath, dir)
+ if strings.HasPrefix(targetPath, outputPath+string(os.PathSeparator)) {
+ err := os.MkdirAll(targetPath, 0700)
+ if err != nil {
+ return err
+ }
+ } else {
+ targetPath = outputPath
}
}
if base != "" {
@@ -185,7 +190,11 @@ func _decompress(file SubFile, targetPath, password string, up model.UpdateProgr
return err
}
defer func() { _ = rc.Close() }()
- f, err := os.OpenFile(stdpath.Join(targetPath, file.FileInfo().Name()), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ destPath := filepath.Join(targetPath, file.FileInfo().Name())
+ if !strings.HasPrefix(destPath, targetPath+string(os.PathSeparator)) {
+ return fmt.Errorf("illegal file path: %s", file.FileInfo().Name())
+ }
+ f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
diff --git a/internal/bootstrap/config.go b/internal/bootstrap/config.go
index 2209c64f..bb381c95 100644
--- a/internal/bootstrap/config.go
+++ b/internal/bootstrap/config.go
@@ -39,7 +39,21 @@ func InitConfig() {
if !filepath.IsAbs(dataDir) {
flags.DataDir = filepath.Join(pwd, flags.DataDir)
}
- configPath := filepath.Join(flags.DataDir, "config.json")
+ // Determine config file path: use flags.ConfigPath if provided, otherwise default to /config.json
+ configPath := flags.ConfigPath
+ if configPath == "" {
+ configPath = filepath.Join(flags.DataDir, "config.json")
+ } else {
+ // if relative, resolve relative to working directory
+ if !filepath.IsAbs(configPath) {
+ if absPath, err := filepath.Abs(configPath); err == nil {
+ configPath = absPath
+ } else {
+ configPath = filepath.Join(pwd, configPath)
+ }
+ }
+ }
+ configPath = filepath.Clean(configPath)
log.Infof("reading config file: %s", configPath)
if !utils.Exists(configPath) {
log.Infof("config file not exists, creating default config file")
diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go
index 3f4f8288..f05fb18a 100644
--- a/internal/bootstrap/data/setting.go
+++ b/internal/bootstrap/data/setting.go
@@ -100,7 +100,7 @@ func InitialSettings() []model.SettingItem {
//{Key: conf.ApiUrl, Value: "", Type: conf.TypeString, Group: model.SITE},
//{Key: conf.BasePath, Value: "", Type: conf.TypeString, Group: model.SITE},
{Key: conf.SiteTitle, Value: "OpenList", Type: conf.TypeString, Group: model.SITE},
- {Key: conf.Announcement, Value: "### repo\nhttps://github.com/OpenListTeam/OpenList", Type: conf.TypeText, Group: model.SITE},
+ {Key: conf.Announcement, Value: "Welcome to the OpenList project!\nFor the latest updates, to contribute code, or to submit suggestions and issues, please visit our [project repository](https://github.com/OpenListTeam/OpenList).", Type: conf.TypeText, Group: model.SITE},
{Key: "pagination_type", Value: "all", Type: conf.TypeSelect, Options: "all,pagination,load_more,auto_load_more", Group: model.SITE},
{Key: "default_page_size", Value: "30", Type: conf.TypeNumber, Group: model.SITE},
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
@@ -114,6 +114,9 @@ func InitialSettings() []model.SettingItem {
{Key: "share_icon", Value: "🎁", Type: conf.TypeString, Group: model.STYLE},
{Key: "home_container", Value: "max_980px", Type: conf.TypeSelect, Options: "max_980px,hope_container", Group: model.STYLE},
{Key: "settings_layout", Value: "list", Type: conf.TypeSelect, Options: "list,responsive", Group: model.STYLE},
+ {Key: conf.HideStorageDetails, Value: "true", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PRIVATE},
+ {Key: conf.HideStorageDetailsInManagePage, Value: "true", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PRIVATE},
+ {Key: "show_disk_usage_in_plain_text", Value: "false", Type: conf.TypeBool, Group: model.STYLE, Flag: model.PUBLIC},
// preview settings
{Key: conf.TextTypes, Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc,strm", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.AudioTypes, Value: "mp3,flac,ogg,m4a,wav,opus,wma", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
@@ -145,7 +148,10 @@ func InitialSettings() []model.SettingItem {
{Key: "audio_cover", Value: "https://res.oplist.org/logo/logo.svg", MigrationValue: "https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg", Type: conf.TypeString, Group: model.PREVIEW},
{Key: conf.AudioAutoplay, Value: "true", Type: conf.TypeBool, Group: model.PREVIEW},
{Key: conf.VideoAutoplay, Value: "true", Type: conf.TypeBool, Group: model.PREVIEW},
+ {Key: conf.PreviewDownloadByDefault, Value: "false", Type: conf.TypeBool, Group: model.PREVIEW},
{Key: conf.PreviewArchivesByDefault, Value: "true", Type: conf.TypeBool, Group: model.PREVIEW},
+ {Key: conf.SharePreviewDownloadByDefault, Value: "true", Type: conf.TypeBool, Group: model.PREVIEW},
+ {Key: conf.SharePreviewArchivesByDefault, Value: "false", Type: conf.TypeBool, Group: model.PREVIEW},
{Key: conf.ReadMeAutoRender, Value: "true", Type: conf.TypeBool, Group: model.PREVIEW},
{Key: conf.FilterReadMeScripts, Value: "true", Type: conf.TypeBool, Group: model.PREVIEW},
// global settings
@@ -155,14 +161,16 @@ func InitialSettings() []model.SettingItem {
{Key: conf.CustomizeBody, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.LinkExpiration, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.SignAll, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
- {Key: conf.PrivacyRegs, Value: `(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])
+ {
+ Key: conf.PrivacyRegs, Value: `(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])
([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:)
(?U)access_token=(.*)&`,
- Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
+ Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE,
+ },
{Key: conf.OcrApi, Value: "https://openlistteam-ocr-api-server.hf.space/ocr/file/json", MigrationValue: "https://api.example.com/ocr/file/json", Type: conf.TypeString, Group: model.GLOBAL}, // TODO: This can be replace by a community-hosted endpoint, see https://github.com/OpenListTeam/ocr_api_server
{Key: conf.FilenameCharMapping, Value: `{"/": "|"}`, Type: conf.TypeText, Group: model.GLOBAL},
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
- {Key: conf.IgnoreDirectLinkParams, Value: "sign,openlist_ts", Type: conf.TypeString, Group: model.GLOBAL},
+ {Key: conf.IgnoreDirectLinkParams, Value: "sign,openlist_ts,raw", Type: conf.TypeString, Group: model.GLOBAL},
{Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.SharePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.ShareArchivePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
@@ -212,12 +220,11 @@ func InitialSettings() []model.SettingItem {
// ftp settings
{Key: conf.FTPPublicHost, Value: "127.0.0.1", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
{Key: conf.FTPPasvPortMap, Value: "", Type: conf.TypeText, Group: model.FTP, Flag: model.PRIVATE},
- {Key: conf.FTPProxyUserAgent, Value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
- "Chrome/87.0.4280.88 Safari/537.36", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
{Key: conf.FTPMandatoryTLS, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
{Key: conf.FTPImplicitTLS, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
{Key: conf.FTPTLSPrivateKeyPath, Value: "", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
{Key: conf.FTPTLSPublicCertPath, Value: "", Type: conf.TypeString, Group: model.FTP, Flag: model.PRIVATE},
+ {Key: conf.SFTPDisablePasswordLogin, Value: "false", Type: conf.TypeBool, Group: model.FTP, Flag: model.PRIVATE},
// traffic settings
{Key: conf.TaskOfflineDownloadThreadsNum, Value: strconv.Itoa(conf.Conf.Tasks.Download.Workers), Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
diff --git a/internal/bootstrap/storage.go b/internal/bootstrap/storage.go
index 47cd5421..3528644f 100644
--- a/internal/bootstrap/storage.go
+++ b/internal/bootstrap/storage.go
@@ -45,7 +45,7 @@ func LoadStorages() {
i+1, storage.MountPath, storage.Driver)
}
}
- conf.StoragesLoaded = true
+ conf.SendStoragesLoadedSignal()
log.Infof("=== load storages completed ===")
if conf.LazyLoad {
syncStatus(2)
diff --git a/internal/cache/keyed_cache.go b/internal/cache/keyed_cache.go
new file mode 100644
index 00000000..87e71f18
--- /dev/null
+++ b/internal/cache/keyed_cache.go
@@ -0,0 +1,101 @@
+package cache
+
+import (
+ "sync"
+ "time"
+)
+
+type KeyedCache[T any] struct {
+ entries map[string]*CacheEntry[T]
+ mu sync.RWMutex
+ ttl time.Duration
+}
+
+func NewKeyedCache[T any](ttl time.Duration) *KeyedCache[T] {
+ c := &KeyedCache[T]{
+ entries: make(map[string]*CacheEntry[T]),
+ ttl: ttl,
+ }
+ gcFuncs = append(gcFuncs, c.GC)
+ return c
+}
+
+func (c *KeyedCache[T]) Set(key string, value T) {
+ c.SetWithExpirable(key, value, ExpirationTime(time.Now().Add(c.ttl)))
+}
+
+func (c *KeyedCache[T]) SetWithTTL(key string, value T, ttl time.Duration) {
+ c.SetWithExpirable(key, value, ExpirationTime(time.Now().Add(ttl)))
+}
+
+func (c *KeyedCache[T]) SetWithExpirable(key string, value T, exp Expirable) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.entries[key] = &CacheEntry[T]{
+ data: value,
+ Expirable: exp,
+ }
+}
+
+func (c *KeyedCache[T]) Get(key string) (T, bool) {
+ c.mu.RLock()
+ entry, exists := c.entries[key]
+ if !exists {
+ c.mu.RUnlock()
+ return *new(T), false
+ }
+
+ expired := entry.Expired()
+ c.mu.RUnlock()
+
+ if !expired {
+ return entry.data, true
+ }
+
+ c.mu.Lock()
+ if c.entries[key] == entry {
+ delete(c.entries, key)
+ c.mu.Unlock()
+ return *new(T), false
+ }
+ c.mu.Unlock()
+ return *new(T), false
+}
+
+func (c *KeyedCache[T]) Delete(key string) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ delete(c.entries, key)
+}
+
+func (c *KeyedCache[T]) Take(key string) (T, bool) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if entry, exists := c.entries[key]; exists {
+ delete(c.entries, key)
+ return entry.data, true
+ }
+ return *new(T), false
+}
+
+func (c *KeyedCache[T]) Clear() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.entries = make(map[string]*CacheEntry[T])
+}
+
+func (c *KeyedCache[T]) GC() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ expiredKeys := make([]string, 0, len(c.entries))
+ for key, entry := range c.entries {
+ if entry.Expired() {
+ expiredKeys = append(expiredKeys, key)
+ }
+ }
+ for _, key := range expiredKeys {
+ delete(c.entries, key)
+ }
+}
diff --git a/internal/cache/type.go b/internal/cache/type.go
new file mode 100644
index 00000000..4eccba52
--- /dev/null
+++ b/internal/cache/type.go
@@ -0,0 +1,18 @@
+package cache
+
+import "time"
+
+type Expirable interface {
+ Expired() bool
+}
+
+type ExpirationTime time.Time
+
+func (e ExpirationTime) Expired() bool {
+ return time.Now().After(time.Time(e))
+}
+
+type CacheEntry[T any] struct {
+ Expirable
+ data T
+}
diff --git a/internal/cache/typed_cache.go b/internal/cache/typed_cache.go
new file mode 100644
index 00000000..7ba126be
--- /dev/null
+++ b/internal/cache/typed_cache.go
@@ -0,0 +1,113 @@
+package cache
+
+import (
+ "sync"
+ "time"
+)
+
+type TypedCache[T any] struct {
+ entries map[string]map[string]*CacheEntry[T]
+ mu sync.RWMutex
+ ttl time.Duration
+}
+
+func NewTypedCache[T any](ttl time.Duration) *TypedCache[T] {
+ c := &TypedCache[T]{
+ entries: make(map[string]map[string]*CacheEntry[T]),
+ ttl: ttl,
+ }
+ gcFuncs = append(gcFuncs, c.GC)
+ return c
+}
+
+func (c *TypedCache[T]) SetType(key, typeKey string, value T) {
+ c.SetTypeWithExpirable(key, typeKey, value, ExpirationTime(time.Now().Add(c.ttl)))
+}
+
+func (c *TypedCache[T]) SetTypeWithTTL(key, typeKey string, value T, ttl time.Duration) {
+ c.SetTypeWithExpirable(key, typeKey, value, ExpirationTime(time.Now().Add(ttl)))
+}
+
+func (c *TypedCache[T]) SetTypeWithExpirable(key, typeKey string, value T, exp Expirable) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ cache, exists := c.entries[key]
+ if !exists {
+ cache = make(map[string]*CacheEntry[T])
+ c.entries[key] = cache
+ }
+
+ cache[typeKey] = &CacheEntry[T]{
+ data: value,
+ Expirable: exp,
+ }
+}
+
+func (c *TypedCache[T]) GetType(key, typeKey string) (T, bool) {
+ c.mu.RLock()
+ cache, exists := c.entries[key]
+ if !exists {
+ c.mu.RUnlock()
+ return *new(T), false
+ }
+ entry, exists := cache[typeKey]
+ if !exists {
+ c.mu.RUnlock()
+ return *new(T), false
+ }
+ expired := entry.Expired()
+ c.mu.RUnlock()
+
+ if !expired {
+ return entry.data, true
+ }
+
+ c.mu.Lock()
+ if cache[typeKey] == entry {
+ delete(cache, typeKey)
+ if len(cache) == 0 {
+ delete(c.entries, key)
+ }
+ c.mu.Unlock()
+ return *new(T), false
+ }
+ c.mu.Unlock()
+ return *new(T), false
+}
+
+func (c *TypedCache[T]) DeleteKey(key string) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ delete(c.entries, key)
+}
+
+func (c *TypedCache[T]) Clear() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.entries = make(map[string]map[string]*CacheEntry[T])
+}
+
+func (c *TypedCache[T]) GC() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ expiredKeys := make(map[string][]string)
+ for tk, entries := range c.entries {
+ for key, entry := range entries {
+ if !entry.Expired() {
+ continue
+ }
+ if _, ok := expiredKeys[tk]; !ok {
+ expiredKeys[tk] = make([]string, 0, len(entries))
+ }
+ expiredKeys[tk] = append(expiredKeys[tk], key)
+ }
+ }
+ for tk, keys := range expiredKeys {
+ for _, key := range keys {
+ delete(c.entries[tk], key)
+ }
+ if len(c.entries[tk]) == 0 {
+ delete(c.entries, tk)
+ }
+ }
+}
diff --git a/internal/cache/utils.go b/internal/cache/utils.go
new file mode 100644
index 00000000..82b7cf85
--- /dev/null
+++ b/internal/cache/utils.go
@@ -0,0 +1,24 @@
+package cache
+
+import (
+ "time"
+
+ "github.com/OpenListTeam/OpenList/v4/pkg/cron"
+ log "github.com/sirupsen/logrus"
+)
+
+var (
+ cacheGcCron *cron.Cron
+ gcFuncs []func()
+)
+
+func init() {
+ // TODO Move to bootstrap
+ cacheGcCron = cron.NewCron(time.Hour)
+ cacheGcCron.Do(func() {
+ log.Infof("Start cache GC")
+ for _, f := range gcFuncs {
+ f()
+ }
+ })
+}
diff --git a/internal/conf/config.go b/internal/conf/config.go
index af198e91..4f239a11 100644
--- a/internal/conf/config.go
+++ b/internal/conf/config.go
@@ -35,6 +35,7 @@ type Scheme struct {
UnixFile string `json:"unix_file" env:"UNIX_FILE"`
UnixFilePerm string `json:"unix_file_perm" env:"UNIX_FILE_PERM"`
EnableH2c bool `json:"enable_h2c" env:"ENABLE_H2C"`
+ EnableH3 bool `json:"enable_h3" env:"ENABLE_H3"`
}
type LogConfig struct {
diff --git a/internal/conf/const.go b/internal/conf/const.go
index e37914bf..1ef56208 100644
--- a/internal/conf/const.go
+++ b/internal/conf/const.go
@@ -19,22 +19,27 @@ const (
AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt"
- Logo = "logo" // multi-lines text, L1: light, EOL: dark
- Favicon = "favicon"
- MainColor = "main_color"
+ Logo = "logo" // multi-lines text, L1: light, EOL: dark
+ Favicon = "favicon"
+ MainColor = "main_color"
+ HideStorageDetails = "hide_storage_details"
+ HideStorageDetailsInManagePage = "hide_storage_details_in_manage_page"
// preview
- TextTypes = "text_types"
- AudioTypes = "audio_types"
- VideoTypes = "video_types"
- ImageTypes = "image_types"
- ProxyTypes = "proxy_types"
- ProxyIgnoreHeaders = "proxy_ignore_headers"
- AudioAutoplay = "audio_autoplay"
- VideoAutoplay = "video_autoplay"
- PreviewArchivesByDefault = "preview_archives_by_default"
- ReadMeAutoRender = "readme_autorender"
- FilterReadMeScripts = "filter_readme_scripts"
+ TextTypes = "text_types"
+ AudioTypes = "audio_types"
+ VideoTypes = "video_types"
+ ImageTypes = "image_types"
+ ProxyTypes = "proxy_types"
+ ProxyIgnoreHeaders = "proxy_ignore_headers"
+ AudioAutoplay = "audio_autoplay"
+ VideoAutoplay = "video_autoplay"
+ PreviewDownloadByDefault = "preview_download_by_default"
+ PreviewArchivesByDefault = "preview_archives_by_default"
+ SharePreviewDownloadByDefault = "share_preview_download_by_default"
+ SharePreviewArchivesByDefault = "share_preview_archives_by_default"
+ ReadMeAutoRender = "readme_autorender"
+ FilterReadMeScripts = "filter_readme_scripts"
// global
HideFiles = "hide_files"
@@ -138,14 +143,18 @@ const (
QbittorrentUrl = "qbittorrent_url"
QbittorrentSeedtime = "qbittorrent_seedtime"
+ // 123 open offline download
+ Pan123OpenOfflineDownloadCallbackUrl = "123_open_callback_url"
+ Pan123OpenTempDir = "123_open_temp_dir"
+
// ftp
- FTPPublicHost = "ftp_public_host"
- FTPPasvPortMap = "ftp_pasv_port_map"
- FTPProxyUserAgent = "ftp_proxy_user_agent"
- FTPMandatoryTLS = "ftp_mandatory_tls"
- FTPImplicitTLS = "ftp_implicit_tls"
- FTPTLSPrivateKeyPath = "ftp_tls_private_key_path"
- FTPTLSPublicCertPath = "ftp_tls_public_cert_path"
+ FTPPublicHost = "ftp_public_host"
+ FTPPasvPortMap = "ftp_pasv_port_map"
+ FTPMandatoryTLS = "ftp_mandatory_tls"
+ FTPImplicitTLS = "ftp_implicit_tls"
+ FTPTLSPrivateKeyPath = "ftp_tls_private_key_path"
+ FTPTLSPublicCertPath = "ftp_tls_public_cert_path"
+ SFTPDisablePasswordLogin = "sftp_disable_password_login"
// traffic
TaskOfflineDownloadThreadsNum = "offline_download_task_threads_num"
diff --git a/internal/conf/var.go b/internal/conf/var.go
index 1ba1f688..0a43c74d 100644
--- a/internal/conf/var.go
+++ b/internal/conf/var.go
@@ -3,6 +3,7 @@ package conf
import (
"net/url"
"regexp"
+ "sync"
)
var (
@@ -24,8 +25,7 @@ var PrivacyReg []*regexp.Regexp
var (
// StoragesLoaded loaded success if empty
- StoragesLoaded = false
- LazyLoad = false
+ LazyLoad = false
// 单个Buffer最大限制
MaxBufferLimit = 16 * 1024 * 1024
// 超过该阈值的Buffer将使用 mmap 分配,可主动释放内存
@@ -36,3 +36,39 @@ var (
ManageHtml string
IndexHtml string
)
+
+var (
+ // StoragesLoaded loaded success if empty
+ StoragesLoaded = false
+ storagesLoadMu sync.RWMutex
+ storagesLoadSignal chan struct{} = make(chan struct{})
+)
+
+func StoragesLoadSignal() <-chan struct{} {
+ storagesLoadMu.RLock()
+ ch := storagesLoadSignal
+ storagesLoadMu.RUnlock()
+ return ch
+}
+func SendStoragesLoadedSignal() {
+ storagesLoadMu.Lock()
+ select {
+ case <-storagesLoadSignal:
+ // already closed
+ default:
+ StoragesLoaded = true
+ close(storagesLoadSignal)
+ }
+ storagesLoadMu.Unlock()
+}
+func ResetStoragesLoadSignal() {
+ storagesLoadMu.Lock()
+ select {
+ case <-storagesLoadSignal:
+ StoragesLoaded = false
+ storagesLoadSignal = make(chan struct{})
+ default:
+ // not closed -> nothing to do
+ }
+ storagesLoadMu.Unlock()
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index c223fe10..96529c15 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -12,7 +12,7 @@ var db *gorm.DB
func Init(d *gorm.DB) {
db = d
- err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.Token), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB))
+ err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB))
if err != nil {
log.Fatalf("failed migrate database: %s", err.Error())
}
diff --git a/internal/db/sharing.go b/internal/db/sharing.go
index 3748796b..c96572a4 100644
--- a/internal/db/sharing.go
+++ b/internal/db/sharing.go
@@ -60,3 +60,7 @@ func DeleteSharingById(id string) error {
s := model.SharingDB{ID: id}
return errors.WithStack(db.Where(s).Delete(&s).Error)
}
+
+func DeleteSharingsByCreatorId(creatorId uint) error {
+ return errors.WithStack(db.Where("creator_id = ?", creatorId).Delete(&model.SharingDB{}).Error)
+}
diff --git a/internal/db/tasks.go b/internal/db/tasks.go
index dcb9dfea..36054898 100644
--- a/internal/db/tasks.go
+++ b/internal/db/tasks.go
@@ -1,6 +1,7 @@
package db
import (
+ "github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/pkg/errors"
)
@@ -30,6 +31,7 @@ func GetTaskDataFunc(type_s string, enabled bool) func() ([]byte, error) {
return nil
}
return func() ([]byte, error) {
+ <-conf.StoragesLoadSignal()
return []byte(task.PersistData), nil
}
}
diff --git a/internal/driver/config.go b/internal/driver/config.go
index bec6d47b..12fb1577 100644
--- a/internal/driver/config.go
+++ b/internal/driver/config.go
@@ -3,11 +3,9 @@ package driver
type Config struct {
Name string `json:"name"`
LocalSort bool `json:"local_sort"`
- // if the driver returns Link with MFile, this should be set to true
- OnlyLinkMFile bool `json:"only_local"`
- OnlyProxy bool `json:"only_proxy"`
- NoCache bool `json:"no_cache"`
- NoUpload bool `json:"no_upload"`
+ OnlyProxy bool `json:"only_proxy"`
+ NoCache bool `json:"no_cache"`
+ NoUpload bool `json:"no_upload"`
// if need get message from user, such as validate code
NeedMs bool `json:"need_ms"`
DefaultRoot string `json:"default_root"`
@@ -19,8 +17,24 @@ type Config struct {
ProxyRangeOption bool `json:"-"`
// if the driver returns Link without URL, this should be set to true
NoLinkURL bool `json:"-"`
+ // Link cache behaviour:
+ // - LinkCacheAuto: let driver decide per-path (implement driver.LinkCacheModeResolver)
+ // - LinkCacheNone: no extra info added to cache key (default)
+ // - flags (OR-able) can add more attributes to cache key (IP, UA, ...)
+ LinkCacheMode `json:"-"`
}
+type LinkCacheMode int8
+
+const (
+ LinkCacheAuto LinkCacheMode = -1 // Let the driver decide per-path (use driver.LinkCacheModeResolver)
+ LinkCacheNone LinkCacheMode = 0 // No extra info added to cache key (default)
+)
+
+const (
+ LinkCacheIP LinkCacheMode = 1 << iota // include client IP in cache key
+ LinkCacheUA // include User-Agent in cache key
+)
func (c Config) MustProxy() bool {
- return c.OnlyProxy || c.OnlyLinkMFile || c.NoLinkURL
+ return c.OnlyProxy || c.NoLinkURL
}
diff --git a/internal/driver/driver.go b/internal/driver/driver.go
index 2884b543..1ce1e451 100644
--- a/internal/driver/driver.go
+++ b/internal/driver/driver.go
@@ -9,8 +9,8 @@ import (
type Driver interface {
Meta
Reader
- //Writer
- //Other
+ // Writer
+ // Other
}
type Meta interface {
@@ -47,11 +47,6 @@ type Getter interface {
Get(ctx context.Context, path string) (model.Obj, error)
}
-type GetObjInfo interface {
- // GetObjInfo get file info by path
- GetObjInfo(ctx context.Context, path string) (model.Obj, error)
-}
-
//type Writer interface {
// Mkdir
// Move
@@ -210,6 +205,16 @@ type ArchiveDecompressResult interface {
ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error)
}
+type WithDetails interface {
+ // GetDetails get storage details (total space, free space, etc.)
+ GetDetails(ctx context.Context) (*model.StorageDetails, error)
+}
+
type Reference interface {
InitReference(storage Driver) error
}
+
+type LinkCacheModeResolver interface {
+ // ResolveLinkCacheMode returns the LinkCacheMode for the given path.
+ ResolveLinkCacheMode(path string) LinkCacheMode
+}
diff --git a/internal/driver/utils.go b/internal/driver/utils.go
index 9e4b6b01..22e1e85b 100644
--- a/internal/driver/utils.go
+++ b/internal/driver/utils.go
@@ -20,7 +20,7 @@ func (p *Progress) Write(b []byte) (n int, err error) {
n = len(b)
p.Done += int64(n)
p.up(float64(p.Done) / float64(p.Total) * 100)
- return
+ return n, err
}
func NewProgress(total int64, up UpdateProgress) *Progress {
@@ -61,3 +61,10 @@ type ReaderWithCtx = stream.ReaderWithCtx
type ReaderUpdatingProgress = stream.ReaderUpdatingProgress
type SimpleReaderWithSize = stream.SimpleReaderWithSize
+
+func DiskUsageFromUsedAndTotal(used, total uint64) model.DiskUsage {
+ return model.DiskUsage{
+ TotalSpace: max(used, total),
+ FreeSpace: total - min(used, total),
+ }
+}
diff --git a/internal/errs/errors.go b/internal/errs/errors.go
index 4910094e..fdf7f218 100644
--- a/internal/errs/errors.go
+++ b/internal/errs/errors.go
@@ -12,13 +12,12 @@ var (
NotSupport = errors.New("not support")
RelativePath = errors.New("using relative path is not allowed")
- MoveBetweenTwoStorages = errors.New("can't move files between two storages, try to copy")
- UploadNotSupported = errors.New("upload not supported")
-
- MetaNotFound = errors.New("meta not found")
- StorageNotFound = errors.New("storage not found")
- StreamIncomplete = errors.New("upload/download stream incomplete, possible network issue")
- StreamPeekFail = errors.New("StreamPeekFail")
+ UploadNotSupported = errors.New("upload not supported")
+ MetaNotFound = errors.New("meta not found")
+ StorageNotFound = errors.New("storage not found")
+ StorageNotInit = errors.New("storage not init")
+ StreamIncomplete = errors.New("upload/download stream incomplete, possible network issue")
+ StreamPeekFail = errors.New("StreamPeekFail")
UnknownArchiveFormat = errors.New("unknown archive format")
WrongArchivePassword = errors.New("wrong archive password")
@@ -42,6 +41,6 @@ func IsNotFoundError(err error) bool {
func IsNotSupportError(err error) bool {
return errors.Is(pkgerr.Cause(err), NotSupport)
}
-func IsNotImplement(err error) bool {
+func IsNotImplementError(err error) bool {
return errors.Is(pkgerr.Cause(err), NotImplement)
}
diff --git a/internal/errs/unwrap.go b/internal/errs/unwrap.go
new file mode 100644
index 00000000..f34953c7
--- /dev/null
+++ b/internal/errs/unwrap.go
@@ -0,0 +1,11 @@
+package errs
+
+func UnwrapOrSelf(err error) error {
+ u, ok := err.(interface {
+ Unwrap() error
+ })
+ if !ok {
+ return err
+ }
+ return u.Unwrap()
+}
diff --git a/internal/fs/archive.go b/internal/fs/archive.go
index e1e4c448..b2885d2b 100644
--- a/internal/fs/archive.go
+++ b/internal/fs/archive.go
@@ -41,6 +41,18 @@ func (t *ArchiveDownloadTask) Run() error {
if err := t.ReinitCtx(); err != nil {
return err
}
+ if t.SrcStorage == nil {
+ if srcStorage, _, err := op.GetStorageAndActualPath(t.SrcStorageMp); err == nil {
+ t.SrcStorage = srcStorage
+ } else {
+ return err
+ }
+ if dstStorage, _, err := op.GetStorageAndActualPath(t.DstStorageMp); err == nil {
+ t.DstStorage = dstStorage
+ } else {
+ return err
+ }
+ }
t.ClearEndTime()
t.SetStartTime(time.Now())
defer func() { t.SetEndTime(time.Now()) }()
diff --git a/internal/fs/copy_move.go b/internal/fs/copy_move.go
index dbdd2835..e9b70856 100644
--- a/internal/fs/copy_move.go
+++ b/internal/fs/copy_move.go
@@ -48,6 +48,19 @@ func (t *FileTransferTask) Run() error {
if err := t.ReinitCtx(); err != nil {
return err
}
+ if t.SrcStorage == nil {
+ if srcStorage, _, err := op.GetStorageAndActualPath(t.SrcStorageMp); err == nil {
+ t.SrcStorage = srcStorage
+ } else {
+ return err
+ }
+ if dstStorage, _, err := op.GetStorageAndActualPath(t.DstStorageMp); err == nil {
+ t.DstStorage = dstStorage
+ } else {
+ return err
+ }
+ }
+
t.ClearEndTime()
t.SetStartTime(time.Now())
defer func() { t.SetEndTime(time.Now()) }()
@@ -139,7 +152,7 @@ func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath str
if taskType == move {
task_group.RefreshAndRemove(dstDirPath, task_group.SrcPathToRemove(srcObjPath))
} else {
- op.DeleteCache(t.DstStorage, dstDirActualPath)
+ op.Cache.DeleteDirectory(t.DstStorage, dstDirActualPath)
}
}
return nil, err
@@ -173,7 +186,7 @@ func (t *FileTransferTask) RunWithNextTaskCallback(f func(nextTask *FileTransfer
dstActualPath := stdpath.Join(t.DstActualPath, srcObj.GetName())
if t.TaskType == copy {
if t.Ctx().Value(conf.NoTaskKey) != nil {
- defer op.DeleteCache(t.DstStorage, dstActualPath)
+ defer op.Cache.DeleteDirectory(t.DstStorage, dstActualPath)
} else {
task_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToRefresh(dstActualPath))
}
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
index 8c1f646b..ca199ed4 100644
--- a/internal/fs/fs.go
+++ b/internal/fs/fs.go
@@ -19,8 +19,9 @@ import (
// then pass the actual path to the op package
type ListArgs struct {
- Refresh bool
- NoLog bool
+ Refresh bool
+ NoLog bool
+ WithStorageDetails bool
}
func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {
@@ -35,11 +36,12 @@ func List(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
}
type GetArgs struct {
- NoLog bool
+ NoLog bool
+ WithStorageDetails bool
}
func Get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {
- res, err := get(ctx, path)
+ res, err := get(ctx, path, args)
if err != nil {
if !args.NoLog {
log.Warnf("failed get %s: %s", path, err)
diff --git a/internal/fs/get.go b/internal/fs/get.go
index 2761322d..459282ce 100644
--- a/internal/fs/get.go
+++ b/internal/fs/get.go
@@ -11,11 +11,11 @@ import (
"github.com/pkg/errors"
)
-func get(ctx context.Context, path string) (model.Obj, error) {
+func get(ctx context.Context, path string, args *GetArgs) (model.Obj, error) {
path = utils.FixAndCleanPath(path)
// maybe a virtual file
if path != "/" {
- virtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(path))
+ virtualFiles := op.GetStorageVirtualFilesWithDetailsByPath(ctx, stdpath.Dir(path), !args.WithStorageDetails, false)
for _, f := range virtualFiles {
if f.GetName() == stdpath.Base(path) {
return f, nil
diff --git a/internal/fs/list.go b/internal/fs/list.go
index cfc13229..fc3b25ab 100644
--- a/internal/fs/list.go
+++ b/internal/fs/list.go
@@ -15,7 +15,7 @@ import (
func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error) {
meta, _ := ctx.Value(conf.MetaKey).(*model.Meta)
user, _ := ctx.Value(conf.UserKey).(*model.User)
- virtualFiles := op.GetStorageVirtualFilesByPath(path)
+ virtualFiles := op.GetStorageVirtualFilesWithDetailsByPath(ctx, path, !args.WithStorageDetails, args.Refresh)
storage, actualPath, err := op.GetStorageAndActualPath(path)
if err != nil && len(virtualFiles) == 0 {
return nil, errors.WithMessage(err, "failed get storage")
@@ -24,8 +24,9 @@ func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
var _objs []model.Obj
if storage != nil {
_objs, err = op.List(ctx, storage, actualPath, model.ListArgs{
- ReqPath: path,
- Refresh: args.Refresh,
+ ReqPath: path,
+ Refresh: args.Refresh,
+ WithStorageDetails: args.WithStorageDetails,
})
if err != nil {
if !args.NoLog {
diff --git a/internal/model/args.go b/internal/model/args.go
index ead19cbb..3fd5b9e3 100644
--- a/internal/model/args.go
+++ b/internal/model/args.go
@@ -11,9 +11,10 @@ import (
)
type ListArgs struct {
- ReqPath string
- S3ShowPlaceholder bool
- Refresh bool
+ ReqPath string
+ S3ShowPlaceholder bool
+ Refresh bool
+ WithStorageDetails bool
}
type LinkArgs struct {
@@ -27,7 +28,6 @@ type Link struct {
URL string `json:"url"` // most common way
Header http.Header `json:"header"` // needed header (for url)
RangeReader RangeReaderIF `json:"-"` // recommended way if can't use URL
- MFile File `json:"-"` // best for local,smb... file system, which exposes MFile
Expiration *time.Duration // local cache expire Duration
@@ -37,6 +37,8 @@ type Link struct {
ContentLength int64 `json:"-"` // 转码视频、缩略图
utils.SyncClosers `json:"-"`
+ // 如果SyncClosers中的资源被关闭后Link将不可用,则此值应为 true
+ RequireReference bool `json:"-"`
}
type OtherArgs struct {
diff --git a/internal/model/obj.go b/internal/model/obj.go
index 836904fc..b3bf5ebe 100644
--- a/internal/model/obj.go
+++ b/internal/model/obj.go
@@ -80,6 +80,10 @@ type SetPath interface {
SetPath(path string)
}
+type ObjWithProvider interface {
+ GetProvider() string
+}
+
func SortFiles(objs []Obj, orderBy, orderDirection string) {
if orderBy == "" {
return
@@ -166,6 +170,16 @@ func GetUrl(obj Obj) (url string, ok bool) {
return url, false
}
+func GetProvider(obj Obj) (string, bool) {
+ if obj, ok := obj.(ObjWithProvider); ok {
+ return obj.GetProvider(), true
+ }
+ if unwrap, ok := obj.(ObjUnwrap); ok {
+ return GetProvider(unwrap.Unwrap())
+ }
+ return "unknown", false
+}
+
func GetRawObject(obj Obj) *Object {
switch v := obj.(type) {
case *ObjThumbURL:
@@ -174,6 +188,8 @@ func GetRawObject(obj Obj) *Object {
return &v.Object
case *ObjectURL:
return &v.Object
+ case *ObjectProvider:
+ return &v.Object
case *Object:
return v
}
diff --git a/internal/model/object.go b/internal/model/object.go
index b6940746..8e5cdf04 100644
--- a/internal/model/object.go
+++ b/internal/model/object.go
@@ -99,3 +99,16 @@ type ObjThumbURL struct {
Thumbnail
Url
}
+
+type Provider struct {
+ Provider string
+}
+
+func (p Provider) GetProvider() string {
+ return p.Provider
+}
+
+type ObjectProvider struct {
+ Object
+ Provider
+}
diff --git a/internal/model/sharing.go b/internal/model/sharing.go
index dfbc81b0..c5dd95e9 100644
--- a/internal/model/sharing.go
+++ b/internal/model/sharing.go
@@ -33,7 +33,7 @@ func (s *Sharing) Valid() bool {
if len(s.Files) == 0 {
return false
}
- if !s.Creator.CanShare() {
+ if s.Creator == nil || !s.Creator.CanShare() {
return false
}
if s.Expires != nil && !s.Expires.IsZero() && s.Expires.Before(time.Now()) {
diff --git a/internal/model/storage.go b/internal/model/storage.go
index 1f60667e..863464cb 100644
--- a/internal/model/storage.go
+++ b/internal/model/storage.go
@@ -32,7 +32,7 @@ type Proxy struct {
WebdavPolicy string `json:"webdav_policy"`
ProxyRange bool `json:"proxy_range"`
DownProxyURL string `json:"down_proxy_url"`
- //Disable sign for DownProxyURL
+ // Disable sign for DownProxyURL
DisableProxySign bool `json:"disable_proxy_sign"`
}
@@ -55,3 +55,40 @@ func (p Proxy) Webdav302() bool {
func (p Proxy) WebdavProxyURL() bool {
return p.WebdavPolicy == "use_proxy_url"
}
+
+type DiskUsage struct {
+ TotalSpace uint64 `json:"total_space"`
+ FreeSpace uint64 `json:"free_space"`
+}
+
+type StorageDetails struct {
+ DiskUsage
+}
+
+type StorageDetailsWithName struct {
+ *StorageDetails
+ DriverName string `json:"driver_name"`
+}
+
+type ObjWithStorageDetails interface {
+ GetStorageDetails() *StorageDetailsWithName
+}
+
+type ObjStorageDetails struct {
+ Obj
+ StorageDetailsWithName
+}
+
+func (o ObjStorageDetails) GetStorageDetails() *StorageDetailsWithName {
+ return &o.StorageDetailsWithName
+}
+
+func GetStorageDetails(obj Obj) (*StorageDetailsWithName, bool) {
+ if obj, ok := obj.(ObjWithStorageDetails); ok {
+ return obj.GetStorageDetails(), true
+ }
+ if unwrap, ok := obj.(ObjUnwrap); ok {
+ return GetStorageDetails(unwrap.Unwrap())
+ }
+ return nil, false
+}
diff --git a/internal/net/request.go b/internal/net/request.go
index 399e01f3..8d380ea4 100644
--- a/internal/net/request.go
+++ b/internal/net/request.go
@@ -12,6 +12,7 @@ import (
"time"
"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/pkg/utils"
"github.com/rclone/rclone/lib/mmap"
@@ -125,7 +126,7 @@ type ConcurrencyLimit struct {
Limit int // 需要大于0
}
-var ErrExceedMaxConcurrency = ErrorHttpStatusCode(http.StatusTooManyRequests)
+var ErrExceedMaxConcurrency = HttpStatusCodeError(http.StatusTooManyRequests)
func (l *ConcurrencyLimit) sub() error {
l._m.Lock()
@@ -403,7 +404,7 @@ var errInfiniteRetry = errors.New("infinite retry")
func (d *downloader) tryDownloadChunk(params *HttpRequestParams, ch *chunk) (int64, error) {
resp, err := d.cfg.HttpClient(d.ctx, params)
if err != nil {
- statusCode, ok := errors.Unwrap(err).(ErrorHttpStatusCode)
+ statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError)
if !ok {
return 0, err
}
diff --git a/internal/net/serve.go b/internal/net/serve.go
index 1fd40b1c..f8d2e3ca 100644
--- a/internal/net/serve.go
+++ b/internal/net/serve.go
@@ -14,6 +14,7 @@ import (
"time"
"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/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
@@ -114,7 +115,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
reader, err := RangeReadCloser.RangeRead(ctx, http_range.Range{Length: -1})
if err != nil {
code = http.StatusRequestedRangeNotSatisfiable
- if statusCode, ok := errors.Unwrap(err).(ErrorHttpStatusCode); ok {
+ if statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError); ok {
code = int(statusCode)
}
http.Error(w, err.Error(), code)
@@ -137,7 +138,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
sendContent, err = RangeReadCloser.RangeRead(ctx, ra)
if err != nil {
code = http.StatusRequestedRangeNotSatisfiable
- if statusCode, ok := errors.Unwrap(err).(ErrorHttpStatusCode); ok {
+ if statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError); ok {
code = int(statusCode)
}
http.Error(w, err.Error(), code)
@@ -199,7 +200,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
log.Warnf("Maybe size incorrect or reader not giving correct/full data, or connection closed before finish. written bytes: %d ,sendSize:%d, ", written, sendSize)
}
code = http.StatusInternalServerError
- if statusCode, ok := errors.Unwrap(err).(ErrorHttpStatusCode); ok {
+ if statusCode, ok := errs.UnwrapOrSelf(err).(HttpStatusCodeError); ok {
code = int(statusCode)
}
w.WriteHeader(code)
@@ -253,14 +254,14 @@ func RequestHttp(ctx context.Context, httpMethod string, headerOverride http.Hea
_ = res.Body.Close()
msg := string(all)
log.Debugln(msg)
- return nil, fmt.Errorf("http request [%s] failure,status: %w response:%s", URL, ErrorHttpStatusCode(res.StatusCode), msg)
+ return nil, fmt.Errorf("http request [%s] failure,status: %w response:%s", URL, HttpStatusCodeError(res.StatusCode), msg)
}
return res, nil
}
-type ErrorHttpStatusCode int
+type HttpStatusCodeError int
-func (e ErrorHttpStatusCode) Error() string {
+func (e HttpStatusCodeError) Error() string {
return fmt.Sprintf("%d|%s", e, http.StatusText(int(e)))
}
diff --git a/internal/offline_download/123_open/client.go b/internal/offline_download/123_open/client.go
new file mode 100644
index 00000000..ce1453c3
--- /dev/null
+++ b/internal/offline_download/123_open/client.go
@@ -0,0 +1,119 @@
+package _123_open
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+
+ _123_open "github.com/OpenListTeam/OpenList/v4/drivers/123_open"
+ "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/offline_download/tool"
+ "github.com/OpenListTeam/OpenList/v4/internal/op"
+ "github.com/OpenListTeam/OpenList/v4/internal/setting"
+)
+
+type Open123 struct{}
+
+func (*Open123) Name() string {
+ return "123 Open"
+}
+
+func (*Open123) Items() []model.SettingItem {
+ return nil
+}
+
+func (*Open123) Run(_ *tool.DownloadTask) error {
+ return errs.NotSupport
+}
+
+func (*Open123) Init() (string, error) {
+ return "ok", nil
+}
+
+func (*Open123) IsReady() bool {
+ tempDir := setting.GetStr(conf.Pan123OpenTempDir)
+ if tempDir == "" {
+ return false
+ }
+ storage, _, err := op.GetStorageAndActualPath(tempDir)
+ if err != nil {
+ return false
+ }
+ if _, ok := storage.(*_123_open.Open123); !ok {
+ return false
+ }
+ return true
+}
+
+func (*Open123) AddURL(args *tool.AddUrlArgs) (string, error) {
+ storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)
+ if err != nil {
+ return "", err
+ }
+ driver123Open, ok := storage.(*_123_open.Open123)
+ if !ok {
+ return "", fmt.Errorf("unsupported storage driver for offline download, only 123 Open is supported")
+ }
+ ctx := context.Background()
+ if err := op.MakeDir(ctx, storage, actualPath); err != nil {
+ return "", err
+ }
+ parentDir, err := op.GetUnwrap(ctx, storage, actualPath)
+ if err != nil {
+ return "", err
+ }
+ cb := setting.GetStr(conf.Pan123OpenOfflineDownloadCallbackUrl)
+ taskID, err := driver123Open.OfflineDownload(ctx, args.Url, parentDir, cb)
+ if err != nil {
+ return "", fmt.Errorf("failed to add offline download task: %w", err)
+ }
+ return strconv.Itoa(taskID), nil
+}
+
+func (*Open123) Remove(_ *tool.DownloadTask) error {
+ return errs.NotSupport
+}
+
+func (*Open123) Status(task *tool.DownloadTask) (*tool.Status, error) {
+ taskID, err := strconv.Atoi(task.GID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse task ID: %s", task.GID)
+ }
+ storage, _, err := op.GetStorageAndActualPath(task.TempDir)
+ if err != nil {
+ return nil, err
+ }
+ driver123Open, ok := storage.(*_123_open.Open123)
+ if !ok {
+ return nil, fmt.Errorf("unsupported storage driver for offline download, only 123 Open is supported")
+ }
+ process, status, err := driver123Open.OfflineDownloadProcess(context.Background(), taskID)
+ if err != nil {
+ return nil, err
+ }
+ var statusStr string
+ switch status {
+ case 0:
+ statusStr = "downloading"
+ case 1:
+ err = fmt.Errorf("offline download failed")
+ case 2:
+ statusStr = "succeed"
+ case 3:
+ statusStr = "retrying"
+ }
+ return &tool.Status{
+ Progress: process,
+ Completed: status == 2,
+ Status: statusStr,
+ Err: err,
+ }, nil
+}
+
+var _ tool.Tool = (*Open123)(nil)
+
+func init() {
+ tool.Tools.Add(&Open123{})
+}
diff --git a/internal/offline_download/all.go b/internal/offline_download/all.go
index f79a8312..a133e98b 100644
--- a/internal/offline_download/all.go
+++ b/internal/offline_download/all.go
@@ -3,6 +3,7 @@ package offline_download
import (
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download/115"
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download/115_open"
+ _ "github.com/OpenListTeam/OpenList/v4/internal/offline_download/123_open"
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download/aria2"
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download/http"
_ "github.com/OpenListTeam/OpenList/v4/internal/offline_download/pikpak"
diff --git a/internal/offline_download/http/client.go b/internal/offline_download/http/client.go
index f0bb5b4f..5dcb6d59 100644
--- a/internal/offline_download/http/client.go
+++ b/internal/offline_download/http/client.go
@@ -10,6 +10,7 @@ import (
"strings"
"time"
+ "github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
@@ -58,6 +59,7 @@ func (s SimpleHttp) Run(task *tool.DownloadTask) error {
if err != nil {
return err
}
+ req.Header.Set("User-Agent", base.UserAgent)
if streamPut {
req.Header.Set("Range", "bytes=0-")
}
diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go
index aea88e2a..6edee740 100644
--- a/internal/offline_download/tool/add.go
+++ b/internal/offline_download/tool/add.go
@@ -2,18 +2,16 @@ package tool
import (
"context"
- "github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser"
-
- _115_open "github.com/OpenListTeam/OpenList/v4/drivers/115_open"
- "github.com/OpenListTeam/OpenList/v4/server/common"
-
"net/url"
stdpath "path"
"path/filepath"
_115 "github.com/OpenListTeam/OpenList/v4/drivers/115"
+ _115_open "github.com/OpenListTeam/OpenList/v4/drivers/115_open"
+ _123_open "github.com/OpenListTeam/OpenList/v4/drivers/123_open"
"github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
"github.com/OpenListTeam/OpenList/v4/drivers/thunder"
+ "github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser"
"github.com/OpenListTeam/OpenList/v4/drivers/thunderx"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
@@ -22,6 +20,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/internal/task"
+ "github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/google/uuid"
"github.com/pkg/errors"
)
@@ -104,6 +103,13 @@ func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, erro
} else {
tempDir = filepath.Join(setting.GetStr(conf.Pan115OpenTempDir), uid)
}
+ case "123 Open":
+ if _, ok := storage.(*_123_open.Open123); ok && dstDirActualPath != "/" {
+ // directly offline downloading to the root path is not allowed via 123 open platform
+ tempDir = args.DstDirPath
+ } else {
+ tempDir = filepath.Join(setting.GetStr(conf.Pan123OpenTempDir), uid)
+ }
case "PikPak":
if _, ok := storage.(*pikpak.PikPak); ok {
tempDir = args.DstDirPath
diff --git a/internal/offline_download/tool/download.go b/internal/offline_download/tool/download.go
index 2d402dcf..f6c53081 100644
--- a/internal/offline_download/tool/download.go
+++ b/internal/offline_download/tool/download.go
@@ -111,6 +111,9 @@ outer:
if t.tool.Name() == "115 Open" {
return nil
}
+ if t.tool.Name() == "123 Open" {
+ return nil
+ }
t.Status = "offline download completed, maybe transferring"
// hack for qBittorrent
if t.tool.Name() == "qBittorrent" {
@@ -174,7 +177,7 @@ func (t *DownloadTask) Update() (bool, error) {
func (t *DownloadTask) Transfer() error {
toolName := t.tool.Name()
- if toolName == "115 Cloud" || toolName == "115 Open" || toolName == "PikPak" || toolName == "Thunder" || toolName == "ThunderX" || toolName == "ThunderBrowser" {
+ if toolName == "115 Cloud" || toolName == "115 Open" || toolName == "123 Open" || toolName == "PikPak" || toolName == "Thunder" || toolName == "ThunderX" || toolName == "ThunderBrowser" {
// 如果不是直接下载到目标路径,则进行转存
if t.TempDir != t.DstDirPath {
return transferObj(t.Ctx(), t.TempDir, t.DstDirPath, t.DeletePolicy)
diff --git a/internal/offline_download/tool/transfer.go b/internal/offline_download/tool/transfer.go
index 1c1284a0..7daf0b17 100644
--- a/internal/offline_download/tool/transfer.go
+++ b/internal/offline_download/tool/transfer.go
@@ -34,6 +34,20 @@ func (t *TransferTask) Run() error {
if err := t.ReinitCtx(); err != nil {
return err
}
+ if t.SrcStorage == nil && t.SrcStorageMp != "" {
+ if srcStorage, _, err := op.GetStorageAndActualPath(t.SrcStorageMp); err == nil {
+ t.SrcStorage = srcStorage
+ } else {
+ return err
+ }
+ if t.DstStorage == nil {
+ if dstStorage, _, err := op.GetStorageAndActualPath(t.DstStorageMp); err == nil {
+ t.DstStorage = dstStorage
+ } else {
+ return err
+ }
+ }
+ }
t.ClearEndTime()
t.SetStartTime(time.Now())
defer func() { t.SetEndTime(time.Now()) }()
@@ -64,9 +78,8 @@ func (t *TransferTask) Run() error {
return op.Put(t.Ctx(), t.DstStorage, t.DstActualPath, s, t.SetProgress)
}
return transferStdPath(t)
- } else {
- return transferObjPath(t)
}
+ return transferObjPath(t)
}
func (t *TransferTask) GetName() string {
diff --git a/internal/op/archive.go b/internal/op/archive.go
index 964e9397..50e05141 100644
--- a/internal/op/archive.go
+++ b/internal/op/archive.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/OpenListTeam/OpenList/v4/internal/archive/tool"
+ "github.com/OpenListTeam/OpenList/v4/internal/cache"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
@@ -17,17 +18,17 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
- "github.com/OpenListTeam/go-cache"
+ gocache "github.com/OpenListTeam/go-cache"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
-var archiveMetaCache = cache.NewMemCache(cache.WithShards[*model.ArchiveMetaProvider](64))
+var archiveMetaCache = gocache.NewMemCache(gocache.WithShards[*model.ArchiveMetaProvider](64))
var archiveMetaG singleflight.Group[*model.ArchiveMetaProvider]
func GetArchiveMeta(ctx context.Context, storage driver.Driver, path string, args model.ArchiveMetaArgs) (*model.ArchiveMetaProvider, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
path = utils.FixAndCleanPath(path)
key := Key(storage, path)
@@ -37,14 +38,14 @@ func GetArchiveMeta(ctx context.Context, storage driver.Driver, path string, arg
return nil, errors.Wrapf(err, "failed to get %s archive met: %+v", path, err)
}
if m.Expiration != nil {
- archiveMetaCache.Set(key, m, cache.WithEx[*model.ArchiveMetaProvider](*m.Expiration))
+ archiveMetaCache.Set(key, m, gocache.WithEx[*model.ArchiveMetaProvider](*m.Expiration))
}
return m, nil
}
- if storage.Config().OnlyLinkMFile {
- meta, err := fn()
- return meta, err
- }
+ // if storage.Config().NoLinkSingleflight {
+ // meta, err := fn()
+ // return meta, err
+ // }
if !args.Refresh {
if meta, ok := archiveMetaCache.Get(key); ok {
log.Debugf("use cache when get %s archive meta", path)
@@ -158,12 +159,12 @@ func getArchiveMeta(ctx context.Context, storage driver.Driver, path string, arg
return obj, archiveMetaProvider, err
}
-var archiveListCache = cache.NewMemCache(cache.WithShards[[]model.Obj](64))
+var archiveListCache = gocache.NewMemCache(gocache.WithShards[[]model.Obj](64))
var archiveListG singleflight.Group[[]model.Obj]
func ListArchive(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) ([]model.Obj, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
path = utils.FixAndCleanPath(path)
metaKey := Key(storage, path)
@@ -199,7 +200,7 @@ func ListArchive(ctx context.Context, storage driver.Driver, path string, args m
if !storage.Config().NoCache {
if len(files) > 0 {
log.Debugf("set cache: %s => %+v", key, files)
- archiveListCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
+ archiveListCache.Set(key, files, gocache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
} else {
log.Debugf("del cache: %s", key)
archiveListCache.Del(key)
@@ -309,7 +310,7 @@ func splitPath(path string) []string {
func ArchiveGet(ctx context.Context, storage driver.Driver, path string, args model.ArchiveListArgs) (model.Obj, model.Obj, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return nil, nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return nil, nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
path = utils.FixAndCleanPath(path)
af, err := GetUnwrap(ctx, storage, path)
@@ -354,78 +355,50 @@ func ArchiveGet(ctx context.Context, storage driver.Driver, path string, args mo
return nil, nil, errors.WithStack(errs.ObjectNotFound)
}
-type extractLink struct {
- *model.Link
- Obj model.Obj
+type objWithLink struct {
+ link *model.Link
+ obj model.Obj
}
-var extractCache = cache.NewMemCache(cache.WithShards[*extractLink](16))
-var extractG = singleflight.Group[*extractLink]{Remember: true}
+var extractCache = cache.NewKeyedCache[*objWithLink](5 * time.Minute)
+var extractG = singleflight.Group[*objWithLink]{}
func DriverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*model.Link, model.Obj, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return nil, nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return nil, nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
key := stdpath.Join(Key(storage, path), args.InnerPath)
- if link, ok := extractCache.Get(key); ok {
- return link.Link, link.Obj, nil
+ if ol, ok := extractCache.Get(key); ok {
+ if ol.link.Expiration != nil || ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {
+ return ol.link, ol.obj, nil
+ }
}
- var forget any
- var linkM *extractLink
- fn := func() (*extractLink, error) {
- link, err := driverExtract(ctx, storage, path, args)
+ fn := func() (*objWithLink, error) {
+ ol, err := driverExtract(ctx, storage, path, args)
if err != nil {
return nil, errors.Wrapf(err, "failed extract archive")
}
- if link.MFile != nil && forget != nil {
- linkM = link
- return nil, errLinkMFileCache
+ if ol.link.Expiration != nil {
+ extractCache.SetWithTTL(key, ol, *ol.link.Expiration)
+ } else {
+ extractCache.SetWithExpirable(key, ol, &ol.link.SyncClosers)
}
- if link.Link.Expiration != nil {
- extractCache.Set(key, link, cache.WithEx[*extractLink](*link.Link.Expiration))
- }
- link.AddIfCloser(forget)
- return link, nil
+ return ol, nil
}
- if storage.Config().OnlyLinkMFile {
- link, err := fn()
+ for {
+ ol, err, _ := extractG.Do(key, fn)
if err != nil {
return nil, nil, err
}
- return link.Link, link.Obj, nil
- }
-
- forget = utils.CloseFunc(func() error {
- if forget != nil {
- forget = nil
- linkG.Forget(key)
+ if ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {
+ return ol.link, ol.obj, nil
}
- return nil
- })
- link, err, _ := extractG.Do(key, fn)
- if err == nil && !link.AcquireReference() {
- link, err, _ = extractG.Do(key, fn)
- if err == nil {
- link.AcquireReference()
- }
- }
- if err == errLinkMFileCache {
- if linkM != nil {
- return linkM.Link, linkM.Obj, nil
- }
- forget = nil
- link, err = fn()
- }
-
- if err != nil {
- return nil, nil, err
}
- return link.Link, link.Obj, nil
}
-func driverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*extractLink, error) {
+func driverExtract(ctx context.Context, storage driver.Driver, path string, args model.ArchiveInnerArgs) (*objWithLink, error) {
storageAr, ok := storage.(driver.ArchiveReader)
if !ok {
return nil, errs.DriverExtractNotSupported
@@ -441,7 +414,7 @@ func driverExtract(ctx context.Context, storage driver.Driver, path string, args
return nil, errors.WithStack(errs.NotFile)
}
link, err := storageAr.Extract(ctx, archiveFile, args)
- return &extractLink{Link: link, Obj: extracted}, err
+ return &objWithLink{link: link, obj: extracted}, err
}
type streamWithParent struct {
@@ -483,7 +456,7 @@ func InternalExtract(ctx context.Context, storage driver.Driver, path string, ar
func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
srcPath = utils.FixAndCleanPath(srcPath)
dstDirPath = utils.FixAndCleanPath(dstDirPath)
@@ -503,16 +476,16 @@ func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstD
if err == nil {
if len(newObjs) > 0 {
for _, newObj := range newObjs {
- addCacheObj(storage, dstDirPath, model.WrapObjName(newObj))
+ Cache.addDirectoryObject(storage, dstDirPath, model.WrapObjName(newObj))
}
} else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
}
case driver.ArchiveDecompress:
err = s.ArchiveDecompress(ctx, srcObj, dstDir, args)
if err == nil && !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
default:
return errs.NotImplement
diff --git a/internal/op/cache.go b/internal/op/cache.go
new file mode 100644
index 00000000..52e430b1
--- /dev/null
+++ b/internal/op/cache.go
@@ -0,0 +1,257 @@
+package op
+
+import (
+ stdpath "path"
+ "sync"
+ "time"
+
+ "github.com/OpenListTeam/OpenList/v4/internal/cache"
+ "github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
+)
+
+type CacheManager struct {
+ dirCache *cache.KeyedCache[*directoryCache] // Cache for directory listings
+ linkCache *cache.TypedCache[*objWithLink] // Cache for file links
+ userCache *cache.KeyedCache[*model.User] // Cache for user data
+ settingCache *cache.KeyedCache[any] // Cache for settings
+ detailCache *cache.KeyedCache[*model.StorageDetails] // Cache for storage details
+}
+
+func NewCacheManager() *CacheManager {
+ return &CacheManager{
+ dirCache: cache.NewKeyedCache[*directoryCache](time.Minute * 5),
+ linkCache: cache.NewTypedCache[*objWithLink](time.Minute * 30),
+ userCache: cache.NewKeyedCache[*model.User](time.Hour),
+ settingCache: cache.NewKeyedCache[any](time.Hour),
+ detailCache: cache.NewKeyedCache[*model.StorageDetails](time.Minute * 30),
+ }
+}
+
+// global instance
+var Cache = NewCacheManager()
+
+func Key(storage driver.Driver, path string) string {
+ return stdpath.Join(storage.GetStorage().MountPath, path)
+}
+
+// update object in dirCache.
+// if it's a directory, remove all its children from dirCache too.
+// if it's a file, remove its link from linkCache.
+func (cm *CacheManager) updateDirectoryObject(storage driver.Driver, dirPath string, oldObj model.Obj, newObj model.Obj) {
+ key := Key(storage, dirPath)
+ if !oldObj.IsDir() {
+ cm.linkCache.DeleteKey(stdpath.Join(key, oldObj.GetName()))
+ cm.linkCache.DeleteKey(stdpath.Join(key, newObj.GetName()))
+ }
+ if storage.Config().NoCache {
+ return
+ }
+
+ if cache, exist := cm.dirCache.Get(key); exist {
+ if oldObj.IsDir() {
+ cm.deleteDirectoryTree(stdpath.Join(key, oldObj.GetName()))
+ }
+ cache.UpdateObject(oldObj.GetName(), newObj)
+ }
+}
+
+// add new object to dirCache
+func (cm *CacheManager) addDirectoryObject(storage driver.Driver, dirPath string, newObj model.Obj) {
+ if storage.Config().NoCache {
+ return
+ }
+ cache, exist := cm.dirCache.Get(Key(storage, dirPath))
+ if exist {
+ cache.UpdateObject(newObj.GetName(), newObj)
+ }
+}
+
+// recursively delete directory and its children from dirCache
+func (cm *CacheManager) DeleteDirectoryTree(storage driver.Driver, dirPath string) {
+ if storage.Config().NoCache {
+ return
+ }
+ cm.deleteDirectoryTree(Key(storage, dirPath))
+}
+func (cm *CacheManager) deleteDirectoryTree(key string) {
+ if dirCache, exists := cm.dirCache.Take(key); exists {
+ for _, obj := range dirCache.objs {
+ if obj.IsDir() {
+ cm.deleteDirectoryTree(stdpath.Join(key, obj.GetName()))
+ }
+ }
+ }
+}
+
+// remove directory from dirCache
+func (cm *CacheManager) DeleteDirectory(storage driver.Driver, dirPath string) {
+ if storage.Config().NoCache {
+ return
+ }
+ cm.dirCache.Delete(Key(storage, dirPath))
+}
+
+// remove object from dirCache.
+// if it's a directory, remove all its children from dirCache too.
+// if it's a file, remove its link from linkCache.
+func (cm *CacheManager) removeDirectoryObject(storage driver.Driver, dirPath string, obj model.Obj) {
+ key := Key(storage, dirPath)
+ if !obj.IsDir() {
+ cm.linkCache.DeleteKey(stdpath.Join(key, obj.GetName()))
+ }
+
+ if storage.Config().NoCache {
+ return
+ }
+ if cache, exist := cm.dirCache.Get(key); exist {
+ if obj.IsDir() {
+ cm.deleteDirectoryTree(stdpath.Join(key, obj.GetName()))
+ }
+ cache.RemoveObject(obj.GetName())
+ }
+}
+
+// cache user data
+func (cm *CacheManager) SetUser(username string, user *model.User) {
+ cm.userCache.Set(username, user)
+}
+
+// cached user data
+func (cm *CacheManager) GetUser(username string) (*model.User, bool) {
+ return cm.userCache.Get(username)
+}
+
+// remove user data from cache
+func (cm *CacheManager) DeleteUser(username string) {
+ cm.userCache.Delete(username)
+}
+
+// caches setting
+func (cm *CacheManager) SetSetting(key string, setting *model.SettingItem) {
+ cm.settingCache.Set(key, setting)
+}
+
+// cached setting
+func (cm *CacheManager) GetSetting(key string) (*model.SettingItem, bool) {
+ if data, exists := cm.settingCache.Get(key); exists {
+ if setting, ok := data.(*model.SettingItem); ok {
+ return setting, true
+ }
+ }
+ return nil, false
+}
+
+// cache setting groups
+func (cm *CacheManager) SetSettingGroup(key string, settings []model.SettingItem) {
+ cm.settingCache.Set(key, settings)
+}
+
+// cached setting group
+func (cm *CacheManager) GetSettingGroup(key string) ([]model.SettingItem, bool) {
+ if data, exists := cm.settingCache.Get(key); exists {
+ if settings, ok := data.([]model.SettingItem); ok {
+ return settings, true
+ }
+ }
+ return nil, false
+}
+
+func (cm *CacheManager) SetStorageDetails(storage driver.Driver, details *model.StorageDetails) {
+ if storage.Config().NoCache {
+ return
+ }
+ expiration := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)
+ cm.detailCache.SetWithTTL(storage.GetStorage().MountPath, details, expiration)
+}
+
+func (cm *CacheManager) GetStorageDetails(storage driver.Driver) (*model.StorageDetails, bool) {
+ return cm.detailCache.Get(storage.GetStorage().MountPath)
+}
+
+func (cm *CacheManager) InvalidateStorageDetails(storage driver.Driver) {
+ cm.detailCache.Delete(storage.GetStorage().MountPath)
+}
+
+// clears all caches
+func (cm *CacheManager) ClearAll() {
+ cm.dirCache.Clear()
+ cm.linkCache.Clear()
+ cm.userCache.Clear()
+ cm.settingCache.Clear()
+ cm.detailCache.Clear()
+}
+
+type directoryCache struct {
+ objs []model.Obj
+ sorted []model.Obj
+ mu sync.RWMutex
+
+ dirtyFlags uint8
+}
+
+const (
+ dirtyRemove uint8 = 1 << iota // 对象删除:刷新 sorted 副本,但不需要 full sort/extract
+ dirtyUpdate // 对象更新:需要执行 full sort + extract
+)
+
+func newDirectoryCache(objs []model.Obj) *directoryCache {
+ sorted := make([]model.Obj, len(objs))
+ copy(sorted, objs)
+ return &directoryCache{
+ objs: objs,
+ sorted: sorted,
+ }
+}
+
+func (dc *directoryCache) RemoveObject(name string) {
+ dc.mu.Lock()
+ defer dc.mu.Unlock()
+ for i, obj := range dc.objs {
+ if obj.GetName() == name {
+ dc.objs = append(dc.objs[:i], dc.objs[i+1:]...)
+ dc.dirtyFlags |= dirtyRemove
+ break
+ }
+ }
+}
+
+func (dc *directoryCache) UpdateObject(oldName string, newObj model.Obj) {
+ dc.mu.Lock()
+ defer dc.mu.Unlock()
+ if oldName != "" {
+ for i, obj := range dc.objs {
+ if obj.GetName() == oldName {
+ dc.objs[i] = newObj
+ dc.dirtyFlags |= dirtyUpdate
+ return
+ }
+ }
+ }
+ dc.objs = append(dc.objs, newObj)
+ dc.dirtyFlags |= dirtyUpdate
+}
+
+func (dc *directoryCache) GetSortedObjects(meta driver.Meta) []model.Obj {
+ dc.mu.RLock()
+ if dc.dirtyFlags == 0 {
+ dc.mu.RUnlock()
+ return dc.sorted
+ }
+ dc.mu.RUnlock()
+ dc.mu.Lock()
+ defer dc.mu.Unlock()
+
+ sorted := make([]model.Obj, len(dc.objs))
+ copy(sorted, dc.objs)
+ dc.sorted = sorted
+ if dc.dirtyFlags&dirtyUpdate != 0 {
+ storage := meta.GetStorage()
+ if meta.Config().LocalSort {
+ model.SortFiles(sorted, storage.OrderBy, storage.OrderDirection)
+ }
+ model.ExtractFolder(sorted, storage.ExtractFolder)
+ }
+ dc.dirtyFlags = 0
+ return sorted
+}
diff --git a/internal/op/fs.go b/internal/op/fs.go
index bdf0567b..c761828e 100644
--- a/internal/op/fs.go
+++ b/internal/op/fs.go
@@ -4,129 +4,35 @@ import (
"context"
stderrors "errors"
stdpath "path"
- "slices"
- "strings"
"time"
"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/stream"
- "github.com/OpenListTeam/OpenList/v4/pkg/generic_sync"
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
- "github.com/OpenListTeam/go-cache"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
-// In order to facilitate adding some other things before and after file op
-
-var listCache = cache.NewMemCache(cache.WithShards[[]model.Obj](64))
var listG singleflight.Group[[]model.Obj]
-func updateCacheObj(storage driver.Driver, path string, oldObj model.Obj, newObj model.Obj) {
- key := Key(storage, path)
- objs, ok := listCache.Get(key)
- if ok {
- for i, obj := range objs {
- if obj.GetName() == newObj.GetName() {
- objs = slices.Delete(objs, i, i+1)
- break
- }
- }
- for i, obj := range objs {
- if obj.GetName() == oldObj.GetName() {
- objs[i] = newObj
- break
- }
- }
- listCache.Set(key, objs, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
- }
-}
-
-func delCacheObj(storage driver.Driver, path string, obj model.Obj) {
- key := Key(storage, path)
- objs, ok := listCache.Get(key)
- if ok {
- for i, oldObj := range objs {
- if oldObj.GetName() == obj.GetName() {
- objs = append(objs[:i], objs[i+1:]...)
- break
- }
- }
- listCache.Set(key, objs, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
- }
-}
-
-var addSortDebounceMap generic_sync.MapOf[string, func(func())]
-
-func addCacheObj(storage driver.Driver, path string, newObj model.Obj) {
- key := Key(storage, path)
- objs, ok := listCache.Get(key)
- if ok {
- for i, obj := range objs {
- if obj.GetName() == newObj.GetName() {
- objs[i] = newObj
- return
- }
- }
-
- // Simple separation of files and folders
- if len(objs) > 0 && objs[len(objs)-1].IsDir() == newObj.IsDir() {
- objs = append(objs, newObj)
- } else {
- objs = append([]model.Obj{newObj}, objs...)
- }
-
- if storage.Config().LocalSort {
- debounce, _ := addSortDebounceMap.LoadOrStore(key, utils.NewDebounce(time.Minute))
- log.Debug("addCacheObj: wait start sort")
- debounce(func() {
- log.Debug("addCacheObj: start sort")
- model.SortFiles(objs, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)
- addSortDebounceMap.Delete(key)
- })
- }
-
- listCache.Set(key, objs, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
- }
-}
-
-func ClearCache(storage driver.Driver, path string) {
- objs, ok := listCache.Get(Key(storage, path))
- if ok {
- for _, obj := range objs {
- if obj.IsDir() {
- ClearCache(storage, stdpath.Join(path, obj.GetName()))
- }
- }
- }
- listCache.Del(Key(storage, path))
-}
-
-func DeleteCache(storage driver.Driver, path string) {
- listCache.Del(Key(storage, path))
-}
-
-func Key(storage driver.Driver, path string) string {
- return stdpath.Join(storage.GetStorage().MountPath, utils.FixAndCleanPath(path))
-}
-
// List files in storage, not contains virtual file
func List(ctx context.Context, storage driver.Driver, path string, args model.ListArgs) ([]model.Obj, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
path = utils.FixAndCleanPath(path)
log.Debugf("op.List %s", path)
key := Key(storage, path)
if !args.Refresh {
- if files, ok := listCache.Get(key); ok {
+ if dirCache, exists := Cache.dirCache.Get(key); exists {
log.Debugf("use cache when list %s", path)
- return files, nil
+ return dirCache.GetSortedObjects(storage), nil
}
}
+
dir, err := GetUnwrap(ctx, storage, path)
if err != nil {
return nil, errors.WithMessage(err, "failed get dir")
@@ -135,6 +41,7 @@ func List(ctx context.Context, storage driver.Driver, path string, args model.Li
if !dir.IsDir() {
return nil, errors.WithStack(errs.NotFolder)
}
+
objs, err, _ := listG.Do(key, func() ([]model.Obj, error) {
files, err := storage.List(ctx, dir, args)
if err != nil {
@@ -162,10 +69,11 @@ func List(ctx context.Context, storage driver.Driver, path string, args model.Li
if !storage.Config().NoCache {
if len(files) > 0 {
log.Debugf("set cache: %s => %+v", key, files)
- listCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
+ ttl := time.Minute * time.Duration(storage.GetStorage().CacheExpiration)
+ Cache.dirCache.SetWithTTL(key, newDirectoryCache(files), ttl)
} else {
log.Debugf("del cache: %s", key)
- listCache.Del(key)
+ Cache.deleteDirectoryTree(key)
}
}
return files, nil
@@ -184,6 +92,9 @@ func Get(ctx context.Context, storage driver.Driver, path string) (model.Obj, er
if err == nil {
return model.WrapObjName(obj), nil
}
+ if !errs.IsNotImplementError(err) && !errs.IsNotSupportError(err) {
+ return nil, errors.WithMessage(err, "failed to get obj")
+ }
}
// is root folder
@@ -249,103 +160,68 @@ func GetUnwrap(ctx context.Context, storage driver.Driver, path string) (model.O
return model.UnwrapObj(obj), err
}
-var linkCache = cache.NewMemCache(cache.WithShards[*model.Link](16))
-var linkG = singleflight.Group[*model.Link]{Remember: true}
-var errLinkMFileCache = stderrors.New("ErrLinkMFileCache")
+var linkG = singleflight.Group[*objWithLink]{}
// Link get link, if is an url. should have an expiry time
func Link(ctx context.Context, storage driver.Driver, path string, args model.LinkArgs) (*model.Link, model.Obj, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return nil, nil, errors.Errorf("storage not init: %s", storage.GetStorage().Status)
- }
- var (
- file model.Obj
- err error
- )
- // use cache directly
- dir, name := stdpath.Split(stdpath.Join(storage.GetStorage().MountPath, path))
- if cacheFiles, ok := listCache.Get(strings.TrimSuffix(dir, "/")); ok {
- for _, f := range cacheFiles {
- if f.GetName() == name {
- file = model.UnwrapObj(f)
- break
- }
- }
- } else {
- if g, ok := storage.(driver.GetObjInfo); ok {
- file, err = g.GetObjInfo(ctx, path)
- } else {
- file, err = GetUnwrap(ctx, storage, path)
- }
+ return nil, nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
- if file == nil {
- if err != nil {
- return nil, nil, errors.WithMessage(err, "failed to get file")
- }
- return nil, nil, errors.WithStack(errs.ObjectNotFound)
+
+ mode := storage.Config().LinkCacheMode
+ if mode == -1 {
+ mode = storage.(driver.LinkCacheModeResolver).ResolveLinkCacheMode(path)
}
- if file.IsDir() {
- return nil, nil, errors.WithStack(errs.NotFile)
+ typeKey := args.Type
+ if mode&driver.LinkCacheIP == 1 {
+ typeKey += "/" + args.IP
}
-
- key := stdpath.Join(Key(storage, path), args.Type)
- if link, ok := linkCache.Get(key); ok {
- return link, file, nil
+ if mode&driver.LinkCacheUA == 1 {
+ typeKey += "/" + args.Header.Get("User-Agent")
}
+ key := Key(storage, path)
+ if ol, exists := Cache.linkCache.GetType(key, typeKey); exists {
+ if ol.link.Expiration != nil ||
+ ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {
+ return ol.link, ol.obj, nil
+ }
+ }
+
+ fn := func() (*objWithLink, error) {
+ file, err := GetUnwrap(ctx, storage, path)
+ if err != nil {
+ return nil, errors.WithMessage(err, "failed to get file")
+ }
+ if file.IsDir() {
+ return nil, errors.WithStack(errs.NotFile)
+ }
- var forget any
- var linkM *model.Link
- fn := func() (*model.Link, error) {
link, err := storage.Link(ctx, file, args)
if err != nil {
return nil, errors.Wrapf(err, "failed get link")
}
- if link.MFile != nil && forget != nil {
- linkM = link
- return nil, errLinkMFileCache
- }
+ ol := &objWithLink{link: link, obj: file}
if link.Expiration != nil {
- linkCache.Set(key, link, cache.WithEx[*model.Link](*link.Expiration))
+ Cache.linkCache.SetTypeWithTTL(key, typeKey, ol, *link.Expiration)
+ } else {
+ Cache.linkCache.SetTypeWithExpirable(key, typeKey, ol, &link.SyncClosers)
}
- link.AddIfCloser(forget)
- return link, nil
+ return ol, nil
}
-
- if storage.Config().OnlyLinkMFile {
- link, err := fn()
+ retry := 0
+ for {
+ ol, err, _ := linkG.Do(key+"/"+typeKey, fn)
if err != nil {
return nil, nil, err
}
- return link, file, err
- }
-
- forget = utils.CloseFunc(func() error {
- if forget != nil {
- forget = nil
- linkG.Forget(key)
- }
- return nil
- })
- link, err, _ := linkG.Do(key, fn)
- if err == nil && !link.AcquireReference() {
- link, err, _ = linkG.Do(key, fn)
- if err == nil {
- link.AcquireReference()
- }
- }
-
- if err == errLinkMFileCache {
- if linkM != nil {
- return linkM, file, nil
+ if ol.link.SyncClosers.AcquireReference() || !ol.link.RequireReference {
+ if retry > 1 {
+ log.Warnf("Link retry successed after %d times: %s %s", retry, key, typeKey)
+ }
+ return ol.link, ol.obj, nil
}
- forget = nil
- link, err = fn()
+ retry++
}
-
- if err != nil {
- return nil, nil, err
- }
- return link, file, nil
}
// Other api
@@ -365,15 +241,15 @@ func Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (
}
}
-var mkdirG singleflight.Group[interface{}]
+var mkdirG singleflight.Group[any]
func MakeDir(ctx context.Context, storage driver.Driver, path string, lazyCache ...bool) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
path = utils.FixAndCleanPath(path)
key := Key(storage, path)
- _, err, _ := mkdirG.Do(key, func() (interface{}, error) {
+ _, err, _ := mkdirG.Do(key, func() (any, error) {
// check if dir exists
f, err := GetUnwrap(ctx, storage, path)
if err != nil {
@@ -395,15 +271,19 @@ func MakeDir(ctx context.Context, storage driver.Driver, path string, lazyCache
newObj, err = s.MakeDir(ctx, parentDir, dirName)
if err == nil {
if newObj != nil {
- addCacheObj(storage, parentPath, model.WrapObjName(newObj))
+ if !storage.Config().NoCache {
+ if dirCache, exist := Cache.dirCache.Get(Key(storage, parentPath)); exist {
+ dirCache.UpdateObject("", newObj)
+ }
+ }
} else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, parentPath)
+ Cache.DeleteDirectory(storage, parentPath)
}
}
case driver.Mkdir:
err = s.MakeDir(ctx, parentDir, dirName)
if err == nil && !utils.IsBool(lazyCache...) {
- DeleteCache(storage, parentPath)
+ Cache.DeleteDirectory(storage, parentPath)
}
default:
return nil, errs.NotImplement
@@ -424,10 +304,14 @@ func MakeDir(ctx context.Context, storage driver.Driver, path string, lazyCache
func Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, lazyCache ...bool) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
srcPath = utils.FixAndCleanPath(srcPath)
+ srcDirPath := stdpath.Dir(srcPath)
dstDirPath = utils.FixAndCleanPath(dstDirPath)
+ if dstDirPath == srcDirPath {
+ return stderrors.New("move in place")
+ }
srcRawObj, err := Get(ctx, storage, srcPath)
if err != nil {
return errors.WithMessage(err, "failed to get src object")
@@ -437,26 +321,25 @@ func Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string
if err != nil {
return errors.WithMessage(err, "failed to get dst dir")
}
- srcDirPath := stdpath.Dir(srcPath)
switch s := storage.(type) {
case driver.MoveResult:
var newObj model.Obj
newObj, err = s.Move(ctx, srcObj, dstDir)
if err == nil {
- delCacheObj(storage, srcDirPath, srcRawObj)
+ Cache.removeDirectoryObject(storage, srcDirPath, srcRawObj)
if newObj != nil {
- addCacheObj(storage, dstDirPath, model.WrapObjName(newObj))
+ Cache.addDirectoryObject(storage, dstDirPath, model.WrapObjName(newObj))
} else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
}
case driver.Move:
err = s.Move(ctx, srcObj, dstDir)
if err == nil {
- delCacheObj(storage, srcDirPath, srcRawObj)
+ Cache.removeDirectoryObject(storage, srcDirPath, srcRawObj)
if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
}
default:
@@ -467,7 +350,7 @@ func Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string
func Rename(ctx context.Context, storage driver.Driver, srcPath, dstName string, lazyCache ...bool) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
srcPath = utils.FixAndCleanPath(srcPath)
srcRawObj, err := Get(ctx, storage, srcPath)
@@ -475,23 +358,30 @@ func Rename(ctx context.Context, storage driver.Driver, srcPath, dstName string,
return errors.WithMessage(err, "failed to get src object")
}
srcObj := model.UnwrapObj(srcRawObj)
- srcDirPath := stdpath.Dir(srcPath)
switch s := storage.(type) {
case driver.RenameResult:
var newObj model.Obj
newObj, err = s.Rename(ctx, srcObj, dstName)
if err == nil {
+ srcDirPath := stdpath.Dir(srcPath)
if newObj != nil {
- updateCacheObj(storage, srcDirPath, srcRawObj, model.WrapObjName(newObj))
- } else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, srcDirPath)
+ Cache.updateDirectoryObject(storage, srcDirPath, srcRawObj, model.WrapObjName(newObj))
+ } else {
+ Cache.removeDirectoryObject(storage, srcDirPath, srcRawObj)
+ if !utils.IsBool(lazyCache...) {
+ Cache.DeleteDirectory(storage, srcDirPath)
+ }
}
}
case driver.Rename:
err = s.Rename(ctx, srcObj, dstName)
- if err == nil && !utils.IsBool(lazyCache...) {
- DeleteCache(storage, srcDirPath)
+ if err == nil {
+ srcDirPath := stdpath.Dir(srcPath)
+ Cache.removeDirectoryObject(storage, srcDirPath, srcRawObj)
+ if !utils.IsBool(lazyCache...) {
+ Cache.DeleteDirectory(storage, srcDirPath)
+ }
}
default:
return errs.NotImplement
@@ -502,14 +392,18 @@ func Rename(ctx context.Context, storage driver.Driver, srcPath, dstName string,
// Copy Just copy file[s] in a storage
func Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, lazyCache ...bool) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
srcPath = utils.FixAndCleanPath(srcPath)
dstDirPath = utils.FixAndCleanPath(dstDirPath)
- srcObj, err := GetUnwrap(ctx, storage, srcPath)
+ if dstDirPath == stdpath.Dir(srcPath) {
+ return stderrors.New("copy in place")
+ }
+ srcRawObj, err := Get(ctx, storage, srcPath)
if err != nil {
return errors.WithMessage(err, "failed to get src object")
}
+ srcObj := model.UnwrapObj(srcRawObj)
dstDir, err := GetUnwrap(ctx, storage, dstDirPath)
if err != nil {
return errors.WithMessage(err, "failed to get dst dir")
@@ -521,15 +415,17 @@ func Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string
newObj, err = s.Copy(ctx, srcObj, dstDir)
if err == nil {
if newObj != nil {
- addCacheObj(storage, dstDirPath, model.WrapObjName(newObj))
+ Cache.addDirectoryObject(storage, dstDirPath, model.WrapObjName(newObj))
} else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
}
case driver.Copy:
err = s.Copy(ctx, srcObj, dstDir)
- if err == nil && !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ if err == nil {
+ if !utils.IsBool(lazyCache...) {
+ Cache.DeleteDirectory(storage, dstDirPath)
+ }
}
default:
return errs.NotImplement
@@ -539,7 +435,7 @@ func Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string
func Remove(ctx context.Context, storage driver.Driver, path string) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
if utils.PathEqual(path, "/") {
return errors.New("delete root folder is not allowed, please goto the manage page to delete the storage instead")
@@ -560,11 +456,7 @@ func Remove(ctx context.Context, storage driver.Driver, path string) error {
case driver.Remove:
err = s.Remove(ctx, model.UnwrapObj(rawObj))
if err == nil {
- delCacheObj(storage, dirPath, rawObj)
- // clear folder cache recursively
- if rawObj.IsDir() {
- ClearCache(storage, path)
- }
+ Cache.removeDirectoryObject(storage, dirPath, rawObj)
}
default:
return errs.NotImplement
@@ -580,7 +472,7 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
}
}()
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
// UrlTree PUT
if storage.GetStorage().Driver == "UrlTree" {
@@ -624,21 +516,30 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
up = func(p float64) {}
}
+ // 如果小于0,则通过缓存获取完整大小,可能发生于流式上传
+ if file.GetSize() < 0 {
+ log.Warnf("file size < 0, try to get full size from cache")
+ file.CacheFullAndWriter(nil, nil)
+ }
switch s := storage.(type) {
case driver.PutResult:
var newObj model.Obj
newObj, err = s.Put(ctx, parentDir, file, up)
if err == nil {
+ Cache.linkCache.DeleteKey(Key(storage, dstPath))
if newObj != nil {
- addCacheObj(storage, dstDirPath, model.WrapObjName(newObj))
+ Cache.addDirectoryObject(storage, dstDirPath, model.WrapObjName(newObj))
} else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
}
case driver.Put:
err = s.Put(ctx, parentDir, file, up)
- if err == nil && !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ if err == nil {
+ Cache.linkCache.DeleteKey(Key(storage, dstPath))
+ if !utils.IsBool(lazyCache...) {
+ Cache.DeleteDirectory(storage, dstDirPath)
+ }
}
default:
return errs.NotImplement
@@ -653,13 +554,7 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
}
} else {
// upload success, remove old obj
- err := Remove(ctx, storage, tempPath)
- if err != nil {
- return err
- } else {
- key := Key(storage, stdpath.Join(dstDirPath, file.GetName()))
- linkCache.Del(key)
- }
+ err = Remove(ctx, storage, tempPath)
}
}
return errors.WithStack(err)
@@ -667,10 +562,11 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
func PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url string, lazyCache ...bool) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
- return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
+ return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
}
dstDirPath = utils.FixAndCleanPath(dstDirPath)
- _, err := GetUnwrap(ctx, storage, stdpath.Join(dstDirPath, dstName))
+ dstPath := stdpath.Join(dstDirPath, dstName)
+ _, err := GetUnwrap(ctx, storage, dstPath)
if err == nil {
return errors.New("obj already exists")
}
@@ -687,16 +583,20 @@ func PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url
var newObj model.Obj
newObj, err = s.PutURL(ctx, dstDir, dstName, url)
if err == nil {
+ Cache.linkCache.DeleteKey(Key(storage, dstPath))
if newObj != nil {
- addCacheObj(storage, dstDirPath, model.WrapObjName(newObj))
+ Cache.addDirectoryObject(storage, dstDirPath, model.WrapObjName(newObj))
} else if !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ Cache.DeleteDirectory(storage, dstDirPath)
}
}
case driver.PutURL:
err = s.PutURL(ctx, dstDir, dstName, url)
- if err == nil && !utils.IsBool(lazyCache...) {
- DeleteCache(storage, dstDirPath)
+ if err == nil {
+ Cache.linkCache.DeleteKey(Key(storage, dstPath))
+ if !utils.IsBool(lazyCache...) {
+ Cache.DeleteDirectory(storage, dstDirPath)
+ }
}
default:
return errs.NotImplement
diff --git a/internal/op/setting.go b/internal/op/setting.go
index cbfa083f..bda4daaf 100644
--- a/internal/op/setting.go
+++ b/internal/op/setting.go
@@ -5,26 +5,21 @@ import (
"sort"
"strconv"
"strings"
- "time"
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
- "github.com/OpenListTeam/OpenList/v4/pkg/utils"
- "github.com/OpenListTeam/go-cache"
"github.com/pkg/errors"
)
-var settingCache = cache.NewMemCache(cache.WithShards[*model.SettingItem](4))
var settingG singleflight.Group[*model.SettingItem]
var settingCacheF = func(item *model.SettingItem) {
- settingCache.Set(item.Key, item, cache.WithEx[*model.SettingItem](time.Hour))
+ Cache.SetSetting(item.Key, item)
}
-var settingGroupCache = cache.NewMemCache(cache.WithShards[[]model.SettingItem](4))
var settingGroupG singleflight.Group[[]model.SettingItem]
-var settingGroupCacheF = func(key string, item []model.SettingItem) {
- settingGroupCache.Set(key, item, cache.WithEx[[]model.SettingItem](time.Hour))
+var settingGroupCacheF = func(key string, items []model.SettingItem) {
+ Cache.SetSettingGroup(key, items)
}
var settingChangingCallbacks = make([]func(), 0)
@@ -34,8 +29,7 @@ func RegisterSettingChangingCallback(f func()) {
}
func SettingCacheUpdate() {
- settingCache.Clear()
- settingGroupCache.Clear()
+ Cache.ClearAll()
for _, cb := range settingChangingCallbacks {
cb()
}
@@ -60,7 +54,7 @@ func GetSettingsMap() map[string]string {
}
func GetSettingItems() ([]model.SettingItem, error) {
- if items, ok := settingGroupCache.Get("ALL_SETTING_ITEMS"); ok {
+ if items, exists := Cache.GetSettingGroup("ALL_SETTING_ITEMS"); exists {
return items, nil
}
items, err, _ := settingGroupG.Do("ALL_SETTING_ITEMS", func() ([]model.SettingItem, error) {
@@ -75,7 +69,7 @@ func GetSettingItems() ([]model.SettingItem, error) {
}
func GetPublicSettingItems() ([]model.SettingItem, error) {
- if items, ok := settingGroupCache.Get("ALL_PUBLIC_SETTING_ITEMS"); ok {
+ if items, exists := Cache.GetSettingGroup("ALL_PUBLIC_SETTING_ITEMS"); exists {
return items, nil
}
items, err, _ := settingGroupG.Do("ALL_PUBLIC_SETTING_ITEMS", func() ([]model.SettingItem, error) {
@@ -90,7 +84,7 @@ func GetPublicSettingItems() ([]model.SettingItem, error) {
}
func GetSettingItemByKey(key string) (*model.SettingItem, error) {
- if item, ok := settingCache.Get(key); ok {
+ if item, exists := Cache.GetSetting(key); exists {
return item, nil
}
@@ -118,8 +112,8 @@ func GetSettingItemInKeys(keys []string) ([]model.SettingItem, error) {
}
func GetSettingItemsByGroup(group int) ([]model.SettingItem, error) {
- key := strconv.Itoa(group)
- if items, ok := settingGroupCache.Get(key); ok {
+ key := fmt.Sprintf("GROUP_%d", group)
+ if items, exists := Cache.GetSettingGroup(key); exists {
return items, nil
}
items, err, _ := settingGroupG.Do(key, func() ([]model.SettingItem, error) {
@@ -135,11 +129,14 @@ func GetSettingItemsByGroup(group int) ([]model.SettingItem, error) {
func GetSettingItemsInGroups(groups []int) ([]model.SettingItem, error) {
sort.Ints(groups)
- key := strings.Join(utils.MustSliceConvert(groups, func(i int) string {
- return strconv.Itoa(i)
- }), ",")
- if items, ok := settingGroupCache.Get(key); ok {
+ keyParts := make([]string, 0, len(groups))
+ for _, g := range groups {
+ keyParts = append(keyParts, strconv.Itoa(g))
+ }
+ key := "GROUPS_" + strings.Join(keyParts, "_")
+
+ if items, exists := Cache.GetSettingGroup(key); exists {
return items, nil
}
items, err, _ := settingGroupG.Do(key, func() ([]model.SettingItem, error) {
@@ -165,10 +162,10 @@ func SaveSettingItems(items []model.SettingItem) error {
}
}
err := db.SaveSettingItems(items)
- if err != nil {
+ if err != nil {
return fmt.Errorf("failed save setting: %+v", err)
}
- SettingCacheUpdate()
+ SettingCacheUpdate()
return nil
}
diff --git a/internal/op/sharing.go b/internal/op/sharing.go
index dbff1ba8..9db51c59 100644
--- a/internal/op/sharing.go
+++ b/internal/op/sharing.go
@@ -137,3 +137,7 @@ func DeleteSharing(sid string) error {
sharingCache.Del(sid)
return db.DeleteSharingById(sid)
}
+
+func DeleteSharingsByCreatorId(creatorId uint) error {
+ return db.DeleteSharingsByCreatorId(creatorId)
+}
diff --git a/internal/op/storage.go b/internal/op/storage.go
index 68c1c4e1..c47334c0 100644
--- a/internal/op/storage.go
+++ b/internal/op/storage.go
@@ -9,6 +9,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/db"
@@ -16,8 +17,8 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/generic_sync"
+ "github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
- mapset "github.com/deckarep/golang-set/v2"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@@ -369,6 +370,8 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
if oldStorage.MountPath != storage.MountPath {
// mount path renamed, need to drop the storage
storagesMap.Delete(oldStorage.MountPath)
+ Cache.DeleteDirectoryTree(storageDriver, "/")
+ Cache.InvalidateStorageDetails(storageDriver)
}
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
@@ -389,6 +392,7 @@ func DeleteStorageById(ctx context.Context, id uint) error {
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
+ var dropErr error = nil
if !storage.Disabled {
storageDriver, err := GetStorageByMountPath(storage.MountPath)
if err != nil {
@@ -396,17 +400,19 @@ func DeleteStorageById(ctx context.Context, id uint) error {
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
- return errors.Wrapf(err, "failed drop storage")
+ dropErr = errors.Wrapf(err, "failed drop storage")
}
// delete the storage in the memory
storagesMap.Delete(storage.MountPath)
+ Cache.DeleteDirectoryTree(storageDriver, "/")
+ Cache.InvalidateStorageDetails(storageDriver)
go callStorageHooks("del", storageDriver)
}
// delete the storage in the database
if err := db.DeleteStorageById(id); err != nil {
return errors.WithMessage(err, "failed delete storage in database")
}
- return nil
+ return dropErr
}
// MustSaveDriverStorage call from specific driver
@@ -465,6 +471,38 @@ func getStoragesByPath(path string) []driver.Driver {
// for example, there are: /a/b,/a/c,/a/d/e,/a/b.balance1,/av
// GetStorageVirtualFilesByPath(/a) => b,c,d
func GetStorageVirtualFilesByPath(prefix string) []model.Obj {
+ return getStorageVirtualFilesByPath(prefix, func(_ driver.Driver, obj model.Obj) model.Obj {
+ return obj
+ })
+}
+
+func GetStorageVirtualFilesWithDetailsByPath(ctx context.Context, prefix string, hideDetails, refresh bool) []model.Obj {
+ if hideDetails {
+ return GetStorageVirtualFilesByPath(prefix)
+ }
+ return getStorageVirtualFilesByPath(prefix, func(d driver.Driver, obj model.Obj) model.Obj {
+ ret := &model.ObjStorageDetails{
+ Obj: obj,
+ StorageDetailsWithName: model.StorageDetailsWithName{
+ StorageDetails: nil,
+ DriverName: d.Config().Name,
+ },
+ }
+ timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
+ defer cancel()
+ details, err := GetStorageDetails(timeoutCtx, d, refresh)
+ if err != nil {
+ if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
+ log.Errorf("failed get %s storage details: %+v", d.GetStorage().MountPath, err)
+ }
+ return ret
+ }
+ ret.StorageDetails = details
+ return ret
+ })
+}
+
+func getStorageVirtualFilesByPath(prefix string, rootCallback func(driver.Driver, model.Obj) model.Obj) []model.Obj {
files := make([]model.Obj, 0)
storages := storagesMap.Values()
sort.Slice(storages, func(i, j int) bool {
@@ -475,23 +513,44 @@ func GetStorageVirtualFilesByPath(prefix string) []model.Obj {
})
prefix = utils.FixAndCleanPath(prefix)
- set := mapset.NewSet[string]()
+ set := make(map[string]int)
+ var wg sync.WaitGroup
for _, v := range storages {
mountPath := utils.GetActualMountPath(v.GetStorage().MountPath)
// Exclude prefix itself and non prefix
if len(prefix) >= len(mountPath) || !utils.IsSubPath(prefix, mountPath) {
continue
}
- name := strings.SplitN(strings.TrimPrefix(mountPath[len(prefix):], "/"), "/", 2)[0]
- if set.Add(name) {
- files = append(files, &model.Object{
- Name: name,
+ names := strings.SplitN(strings.TrimPrefix(mountPath[len(prefix):], "/"), "/", 2)
+ idx, ok := set[names[0]]
+ if !ok {
+ set[names[0]] = len(files)
+ obj := &model.Object{
+ Name: names[0],
Size: 0,
Modified: v.GetStorage().Modified,
IsFolder: true,
- })
+ }
+ if len(names) == 1 {
+ idx = len(files)
+ files = append(files, obj)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ files[idx] = rootCallback(v, files[idx])
+ }()
+ } else {
+ files = append(files, obj)
+ }
+ } else if len(names) == 1 {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ files[idx] = rootCallback(v, files[idx])
+ }()
}
}
+ wg.Wait()
return files
}
@@ -515,3 +574,29 @@ func GetBalancedStorage(path string) driver.Driver {
return storages[i]
}
}
+
+var detailsG singleflight.Group[*model.StorageDetails]
+
+func GetStorageDetails(ctx context.Context, storage driver.Driver, refresh ...bool) (*model.StorageDetails, error) {
+ if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
+ return nil, errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status)
+ }
+ wd, ok := storage.(driver.WithDetails)
+ if !ok {
+ return nil, errs.NotImplement
+ }
+ if !utils.IsBool(refresh...) {
+ if ret, ok := Cache.GetStorageDetails(storage); ok {
+ return ret, nil
+ }
+ }
+ details, err, _ := detailsG.Do(storage.GetStorage().MountPath, func() (*model.StorageDetails, error) {
+ ret, err := wd.GetDetails(ctx)
+ if err != nil {
+ return nil, err
+ }
+ Cache.SetStorageDetails(storage, ret)
+ return ret, nil
+ })
+ return details, err
+}
diff --git a/internal/op/user.go b/internal/op/user.go
index 3f526625..dc2a0ce3 100644
--- a/internal/op/user.go
+++ b/internal/op/user.go
@@ -1,17 +1,14 @@
package op
import (
- "time"
-
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
- "github.com/OpenListTeam/go-cache"
+ "github.com/pkg/errors"
)
-var userCache = cache.NewMemCache(cache.WithShards[*model.User](2))
var userG singleflight.Group[*model.User]
var guestUser *model.User
var adminUser *model.User
@@ -46,7 +43,7 @@ func GetUserByName(username string) (*model.User, error) {
if username == "" {
return nil, errs.EmptyUsername
}
- if user, ok := userCache.Get(username); ok {
+ if user, exists := Cache.GetUser(username); exists {
return user, nil
}
user, err, _ := userG.Do(username, func() (*model.User, error) {
@@ -54,7 +51,7 @@ func GetUserByName(username string) (*model.User, error) {
if err != nil {
return nil, err
}
- userCache.Set(username, _user, cache.WithEx[*model.User](time.Hour))
+ Cache.SetUser(username, _user)
return _user, nil
})
return user, err
@@ -81,7 +78,10 @@ func DeleteUserById(id uint) error {
if old.IsAdmin() || old.IsGuest() {
return errs.DeleteAdminOrGuest
}
- userCache.Del(old.Username)
+ Cache.DeleteUser(old.Username)
+ if err := DeleteSharingsByCreatorId(id); err != nil {
+ return errors.WithMessage(err, "failed to delete user's sharings")
+ }
return db.DeleteUserById(id)
}
@@ -96,7 +96,7 @@ func UpdateUser(u *model.User) error {
if u.IsGuest() {
guestUser = nil
}
- userCache.Del(old.Username)
+ Cache.DeleteUser(old.Username)
u.BasePath = utils.FixAndCleanPath(u.BasePath)
return db.UpdateUser(u)
}
@@ -125,6 +125,6 @@ func DelUserCache(username string) error {
if user.IsGuest() {
guestUser = nil
}
- userCache.Del(username)
+ Cache.DeleteUser(username)
return nil
}
diff --git a/internal/stream/stream.go b/internal/stream/stream.go
index 94772761..8d2f504f 100644
--- a/internal/stream/stream.go
+++ b/internal/stream/stream.go
@@ -137,6 +137,60 @@ func (f *FileStream) CacheFullAndWriter(up *model.UpdateProgress, writer io.Writ
if writer != nil {
reader = io.TeeReader(reader, writer)
}
+
+ if f.GetSize() < 0 {
+ if f.peekBuff == nil {
+ f.peekBuff = &buffer.Reader{}
+ }
+ // 检查是否有数据
+ buf := []byte{0}
+ n, err := io.ReadFull(reader, buf)
+ if n > 0 {
+ f.peekBuff.Append(buf[:n])
+ }
+ if err == io.ErrUnexpectedEOF {
+ f.size = f.peekBuff.Size()
+ f.Reader = f.peekBuff
+ return f.peekBuff, nil
+ } else if err != nil {
+ return nil, err
+ }
+ if conf.MaxBufferLimit-n > conf.MmapThreshold && conf.MmapThreshold > 0 {
+ m, err := mmap.Alloc(conf.MaxBufferLimit - n)
+ if err == nil {
+ f.Add(utils.CloseFunc(func() error {
+ return mmap.Free(m)
+ }))
+ n, err = io.ReadFull(reader, m)
+ if n > 0 {
+ f.peekBuff.Append(m[:n])
+ }
+ if err == io.ErrUnexpectedEOF {
+ f.size = f.peekBuff.Size()
+ f.Reader = f.peekBuff
+ return f.peekBuff, nil
+ } else if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ tmpF, err := utils.CreateTempFile(reader, 0)
+ if err != nil {
+ return nil, err
+ }
+ f.Add(utils.CloseFunc(func() error {
+ return errors.Join(tmpF.Close(), os.RemoveAll(tmpF.Name()))
+ }))
+ peekF, err := buffer.NewPeekFile(f.peekBuff, tmpF)
+ if err != nil {
+ return nil, err
+ }
+ f.size = peekF.Size()
+ f.Reader = peekF
+ return peekF, nil
+ }
+
f.Reader = reader
return f.cache(f.GetSize())
}
@@ -162,7 +216,7 @@ func (f *FileStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {
}
size := httpRange.Start + httpRange.Length
- if f.peekBuff != nil && size <= int64(f.peekBuff.Len()) {
+ if f.peekBuff != nil && size <= int64(f.peekBuff.Size()) {
return io.NewSectionReader(f.peekBuff, httpRange.Start, httpRange.Length), nil
}
@@ -194,7 +248,7 @@ func (f *FileStream) cache(maxCacheSize int64) (model.File, error) {
f.peekBuff = &buffer.Reader{}
f.oriReader = f.Reader
}
- bufSize := maxCacheSize - int64(f.peekBuff.Len())
+ bufSize := maxCacheSize - int64(f.peekBuff.Size())
var buf []byte
if conf.MmapThreshold > 0 && bufSize >= int64(conf.MmapThreshold) {
m, err := mmap.Alloc(int(bufSize))
@@ -213,7 +267,7 @@ func (f *FileStream) cache(maxCacheSize int64) (model.File, error) {
return nil, fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", bufSize, n, err)
}
f.peekBuff.Append(buf)
- if int64(f.peekBuff.Len()) >= f.GetSize() {
+ if int64(f.peekBuff.Size()) >= f.GetSize() {
f.Reader = f.peekBuff
f.oriReader = nil
} else {
diff --git a/internal/stream/util.go b/internal/stream/util.go
index 20cb4be0..95f56e8a 100644
--- a/internal/stream/util.go
+++ b/internal/stream/util.go
@@ -8,8 +8,10 @@ import (
"fmt"
"io"
"net/http"
+ "os"
"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/net"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
@@ -26,9 +28,6 @@ func (f RangeReaderFunc) RangeRead(ctx context.Context, httpRange http_range.Ran
}
func GetRangeReaderFromLink(size int64, link *model.Link) (model.RangeReaderIF, error) {
- if link.MFile != nil {
- return GetRangeReaderFromMFile(size, link.MFile), nil
- }
if link.Concurrency > 0 || link.PartSize > 0 {
down := net.NewDownloader(func(d *net.Downloader) {
d.Concurrency = link.Concurrency
@@ -65,7 +64,7 @@ func GetRangeReaderFromLink(size int64, link *model.Link) (model.RangeReaderIF,
}
if len(link.URL) == 0 {
- return nil, errors.New("invalid link: must have at least one of MFile, URL, or RangeReader")
+ return nil, errors.New("invalid link: must have at least one of URL or RangeReader")
}
rangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
if httpRange.Length < 0 || httpRange.Start+httpRange.Length > size {
@@ -77,7 +76,7 @@ func GetRangeReaderFromLink(size int64, link *model.Link) (model.RangeReaderIF,
response, err := net.RequestHttp(ctx, "GET", header, link.URL)
if err != nil {
- if _, ok := errors.Unwrap(err).(net.ErrorHttpStatusCode); ok {
+ if _, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok {
return nil, err
}
return nil, fmt.Errorf("http request failure, err:%w", err)
@@ -151,32 +150,58 @@ func CacheFullAndHash(stream model.FileStreamer, up *model.UpdateProgress, hashT
return tmpF, hex.EncodeToString(h.Sum(nil)), nil
}
-type StreamSectionReader struct {
- file model.FileStreamer
- off int64
- bufPool *pool.Pool[[]byte]
+type StreamSectionReaderIF interface {
+ // 线程不安全
+ GetSectionReader(off, length int64) (io.ReadSeeker, error)
+ FreeSectionReader(sr io.ReadSeeker)
+ // 线程不安全
+ DiscardSection(off int64, length int64) error
}
-func NewStreamSectionReader(file model.FileStreamer, maxBufferSize int, up *model.UpdateProgress) (*StreamSectionReader, error) {
- ss := &StreamSectionReader{file: file}
+func NewStreamSectionReader(file model.FileStreamer, maxBufferSize int, up *model.UpdateProgress) (StreamSectionReaderIF, error) {
if file.GetFile() != nil {
- return ss, nil
+ return &cachedSectionReader{file.GetFile()}, nil
}
maxBufferSize = min(maxBufferSize, int(file.GetSize()))
if maxBufferSize > conf.MaxBufferLimit {
- _, err := file.CacheFullAndWriter(up, nil)
+ f, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
if err != nil {
return nil, err
}
+
+ if f.Truncate((file.GetSize()+int64(maxBufferSize-1))/int64(maxBufferSize)*int64(maxBufferSize)) != nil {
+ // fallback to full cache
+ _, _ = f.Close(), os.Remove(f.Name())
+ cache, err := file.CacheFullAndWriter(up, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &cachedSectionReader{cache}, nil
+ }
+
+ ss := &fileSectionReader{Reader: file, temp: f}
+ ss.bufPool = &pool.Pool[*offsetWriterWithBase]{
+ New: func() *offsetWriterWithBase {
+ base := ss.fileOff
+ ss.fileOff += int64(maxBufferSize)
+ return &offsetWriterWithBase{io.NewOffsetWriter(ss.temp, base), base}
+ },
+ }
+ file.Add(utils.CloseFunc(func() error {
+ ss.bufPool.Reset()
+ return errors.Join(ss.temp.Close(), os.Remove(ss.temp.Name()))
+ }))
return ss, nil
}
+
+ ss := &directSectionReader{file: file}
if conf.MmapThreshold > 0 && maxBufferSize >= conf.MmapThreshold {
ss.bufPool = &pool.Pool[[]byte]{
New: func() []byte {
buf, err := mmap.Alloc(maxBufferSize)
if err == nil {
- file.Add(utils.CloseFunc(func() error {
+ ss.file.Add(utils.CloseFunc(func() error {
return mmap.Free(buf)
}))
} else {
@@ -200,38 +225,113 @@ func NewStreamSectionReader(file model.FileStreamer, maxBufferSize int, up *mode
return ss, nil
}
+type cachedSectionReader struct {
+ cache io.ReaderAt
+}
+
+func (*cachedSectionReader) DiscardSection(off int64, length int64) error {
+ return nil
+}
+func (s *cachedSectionReader) GetSectionReader(off, length int64) (io.ReadSeeker, error) {
+ return io.NewSectionReader(s.cache, off, length), nil
+}
+func (*cachedSectionReader) FreeSectionReader(sr io.ReadSeeker) {}
+
+type fileSectionReader struct {
+ io.Reader
+ off int64
+ temp *os.File
+ fileOff int64
+ bufPool *pool.Pool[*offsetWriterWithBase]
+}
+
+type offsetWriterWithBase struct {
+ *io.OffsetWriter
+ base int64
+}
+
// 线程不安全
-func (ss *StreamSectionReader) GetSectionReader(off, length int64) (*SectionReader, error) {
- var cache io.ReaderAt = ss.file.GetFile()
- var buf []byte
- if cache == nil {
- if off != ss.off {
- return nil, fmt.Errorf("stream not cached: request offset %d != current offset %d", off, ss.off)
- }
- tempBuf := ss.bufPool.Get()
- buf = tempBuf[:length]
- n, err := io.ReadFull(ss.file, buf)
- if int64(n) != length {
- return nil, fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", length, n, err)
- }
- ss.off += int64(n)
- off = 0
- cache = bytes.NewReader(buf)
+func (ss *fileSectionReader) DiscardSection(off int64, length int64) error {
+ if off != ss.off {
+ return fmt.Errorf("stream not cached: request offset %d != current offset %d", off, ss.off)
+ }
+ _, err := utils.CopyWithBufferN(io.Discard, ss.Reader, length)
+ if err != nil {
+ return fmt.Errorf("failed to skip data: (expect =%d) %w", length, err)
}
- return &SectionReader{io.NewSectionReader(cache, off, length), buf}, nil
+ ss.off += length
+ return nil
}
-func (ss *StreamSectionReader) FreeSectionReader(sr *SectionReader) {
- if sr != nil {
- if sr.buf != nil {
- ss.bufPool.Put(sr.buf[0:cap(sr.buf)])
- sr.buf = nil
- }
+type fileBufferSectionReader struct {
+ io.ReadSeeker
+ fileBuf *offsetWriterWithBase
+}
+
+func (ss *fileSectionReader) GetSectionReader(off, length int64) (io.ReadSeeker, error) {
+ if off != ss.off {
+ return nil, fmt.Errorf("stream not cached: request offset %d != current offset %d", off, ss.off)
+ }
+ fileBuf := ss.bufPool.Get()
+ _, _ = fileBuf.Seek(0, io.SeekStart)
+ n, err := utils.CopyWithBufferN(fileBuf, ss.Reader, length)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", length, n, err)
+ }
+ ss.off += length
+ return &fileBufferSectionReader{io.NewSectionReader(ss.temp, fileBuf.base, length), fileBuf}, nil
+}
+
+func (ss *fileSectionReader) FreeSectionReader(rs io.ReadSeeker) {
+ if sr, ok := rs.(*fileBufferSectionReader); ok {
+ ss.bufPool.Put(sr.fileBuf)
+ sr.fileBuf = nil
sr.ReadSeeker = nil
}
}
-type SectionReader struct {
+type directSectionReader struct {
+ file model.FileStreamer
+ off int64
+ bufPool *pool.Pool[[]byte]
+}
+
+// 线程不安全
+func (ss *directSectionReader) DiscardSection(off int64, length int64) error {
+ if off != ss.off {
+ return fmt.Errorf("stream not cached: request offset %d != current offset %d", off, ss.off)
+ }
+ _, err := utils.CopyWithBufferN(io.Discard, ss.file, length)
+ if err != nil {
+ return fmt.Errorf("failed to skip data: (expect =%d) %w", length, err)
+ }
+ ss.off += length
+ return nil
+}
+
+type bufferSectionReader struct {
io.ReadSeeker
buf []byte
}
+
+// 线程不安全
+func (ss *directSectionReader) GetSectionReader(off, length int64) (io.ReadSeeker, error) {
+ if off != ss.off {
+ return nil, fmt.Errorf("stream not cached: request offset %d != current offset %d", off, ss.off)
+ }
+ tempBuf := ss.bufPool.Get()
+ buf := tempBuf[:length]
+ n, err := io.ReadFull(ss.file, buf)
+ if int64(n) != length {
+ return nil, fmt.Errorf("failed to read all data: (expect =%d, actual =%d) %w", length, n, err)
+ }
+ ss.off += int64(n)
+ return &bufferSectionReader{bytes.NewReader(buf), buf}, nil
+}
+func (ss *directSectionReader) FreeSectionReader(rs io.ReadSeeker) {
+ if sr, ok := rs.(*bufferSectionReader); ok {
+ ss.bufPool.Put(sr.buf[0:cap(sr.buf)])
+ sr.buf = nil
+ sr.ReadSeeker = nil
+ }
+}
diff --git a/internal/task/base.go b/internal/task/base.go
index 8976ed90..5bfa03a8 100644
--- a/internal/task/base.go
+++ b/internal/task/base.go
@@ -14,7 +14,7 @@ type TaskExtension struct {
Creator *model.User
startTime *time.Time
endTime *time.Time
- totalBytes int64
+ TotalBytes int64
ApiUrl string
}
@@ -58,11 +58,11 @@ func (t *TaskExtension) ClearEndTime() {
}
func (t *TaskExtension) SetTotalBytes(totalBytes int64) {
- t.totalBytes = totalBytes
+ t.TotalBytes = totalBytes
}
func (t *TaskExtension) GetTotalBytes() int64 {
- return t.totalBytes
+ return t.TotalBytes
}
func (t *TaskExtension) ReinitCtx() error {
diff --git a/internal/task_group/transfer.go b/internal/task_group/transfer.go
index 3cef5786..8c75b77d 100644
--- a/internal/task_group/transfer.go
+++ b/internal/task_group/transfer.go
@@ -24,16 +24,15 @@ func RefreshAndRemove(dstPath string, payloads ...any) {
return
}
_, dstNeedRefresh := dstStorage.(driver.Put)
- dstNeedRefresh = dstNeedRefresh && !dstStorage.Config().NoCache
if dstNeedRefresh {
- op.DeleteCache(dstStorage, dstActualPath)
+ op.Cache.DeleteDirectory(dstStorage, dstActualPath)
}
var ctx context.Context
for _, payload := range payloads {
switch p := payload.(type) {
case DstPathToRefresh:
if dstNeedRefresh {
- op.DeleteCache(dstStorage, string(p))
+ op.Cache.DeleteDirectory(dstStorage, string(p))
}
case SrcPathToRemove:
if ctx == nil {
@@ -79,7 +78,7 @@ func verifyAndRemove(ctx context.Context, srcStorage, dstStorage driver.Driver,
}
if refresh {
- op.DeleteCache(dstStorage, dstObjPath)
+ op.Cache.DeleteDirectory(dstStorage, dstObjPath)
}
hasErr := false
for _, obj := range srcObjs {
diff --git a/pkg/buffer/bytes.go b/pkg/buffer/bytes.go
index 3ee10747..3e6cb540 100644
--- a/pkg/buffer/bytes.go
+++ b/pkg/buffer/bytes.go
@@ -8,83 +8,86 @@ import (
// 用于存储不复用的[]byte
type Reader struct {
bufs [][]byte
- length int
- offset int
+ size int64
+ offset int64
}
-func (r *Reader) Len() int {
- return r.length
+func (r *Reader) Size() int64 {
+ return r.size
}
func (r *Reader) Append(buf []byte) {
- r.length += len(buf)
+ r.size += int64(len(buf))
r.bufs = append(r.bufs, buf)
}
func (r *Reader) Read(p []byte) (int, error) {
- n, err := r.ReadAt(p, int64(r.offset))
+ n, err := r.ReadAt(p, r.offset)
if n > 0 {
- r.offset += n
+ r.offset += int64(n)
}
return n, err
}
func (r *Reader) ReadAt(p []byte, off int64) (int, error) {
- if off < 0 || off >= int64(r.length) {
+ if off < 0 || off >= r.size {
return 0, io.EOF
}
- n, length := 0, int64(0)
+ n := 0
readFrom := false
for _, buf := range r.bufs {
- newLength := length + int64(len(buf))
if readFrom {
- w := copy(p[n:], buf)
- n += w
- } else if off < newLength {
+ nn := copy(p[n:], buf)
+ n += nn
+ if n == len(p) {
+ return n, nil
+ }
+ } else if newOff := off - int64(len(buf)); newOff >= 0 {
+ off = newOff
+ } else {
+ nn := copy(p, buf[off:])
+ if nn == len(p) {
+ return nn, nil
+ }
+ n += nn
readFrom = true
- w := copy(p[n:], buf[int(off-length):])
- n += w
}
- if n == len(p) {
- return n, nil
- }
- length = newLength
}
return n, io.EOF
}
func (r *Reader) Seek(offset int64, whence int) (int64, error) {
- var abs int
switch whence {
case io.SeekStart:
- abs = int(offset)
case io.SeekCurrent:
- abs = r.offset + int(offset)
+ offset = r.offset + offset
case io.SeekEnd:
- abs = r.length + int(offset)
+ offset = r.size + offset
default:
return 0, errors.New("Seek: invalid whence")
}
- if abs < 0 || abs > r.length {
+ if offset < 0 || offset > r.size {
return 0, errors.New("Seek: invalid offset")
}
- r.offset = abs
- return int64(abs), nil
+ r.offset = offset
+ return offset, nil
}
func (r *Reader) Reset() {
clear(r.bufs)
r.bufs = nil
- r.length = 0
+ r.size = 0
r.offset = 0
}
func NewReader(buf ...[]byte) *Reader {
- b := &Reader{}
+ b := &Reader{
+ bufs: make([][]byte, 0, len(buf)),
+ }
for _, b1 := range buf {
b.Append(b1)
}
diff --git a/pkg/buffer/bytes_test.go b/pkg/buffer/bytes_test.go
index b66af229..3f4d8556 100644
--- a/pkg/buffer/bytes_test.go
+++ b/pkg/buffer/bytes_test.go
@@ -13,8 +13,7 @@ func TestReader_ReadAt(t *testing.T) {
}
bs := &Reader{}
bs.Append([]byte("github.com"))
- bs.Append([]byte("/"))
- bs.Append([]byte("OpenList"))
+ bs.Append([]byte("/OpenList"))
bs.Append([]byte("Team/"))
bs.Append([]byte("OpenList"))
tests := []struct {
@@ -71,7 +70,7 @@ func TestReader_ReadAt(t *testing.T) {
off: 24,
},
want: func(a args, n int, err error) error {
- if n != bs.Len()-int(a.off) {
+ if n != int(bs.Size()-a.off) {
return errors.New("read length not match")
}
if string(a.p[:n]) != "OpenList" {
diff --git a/pkg/buffer/file.go b/pkg/buffer/file.go
new file mode 100644
index 00000000..48edf5a4
--- /dev/null
+++ b/pkg/buffer/file.go
@@ -0,0 +1,88 @@
+package buffer
+
+import (
+ "errors"
+ "io"
+ "os"
+)
+
+type PeekFile struct {
+ peek *Reader
+ file *os.File
+ offset int64
+ size int64
+}
+
+func (p *PeekFile) Read(b []byte) (n int, err error) {
+ n, err = p.ReadAt(b, p.offset)
+ if n > 0 {
+ p.offset += int64(n)
+ }
+ return n, err
+}
+
+func (p *PeekFile) ReadAt(b []byte, off int64) (n int, err error) {
+ if off < p.peek.Size() {
+ n, err = p.peek.ReadAt(b, off)
+ if err == nil || n == len(b) {
+ return n, nil
+ }
+ // EOF
+ }
+ var nn int
+ nn, err = p.file.ReadAt(b[n:], off+int64(n)-p.peek.Size())
+ return n + nn, err
+}
+
+func (p *PeekFile) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekStart:
+ case io.SeekCurrent:
+ if offset == 0 {
+ return p.offset, nil
+ }
+ offset = p.offset + offset
+ case io.SeekEnd:
+ offset = p.size + offset
+ default:
+ return 0, errors.New("Seek: invalid whence")
+ }
+
+ if offset < 0 || offset > p.size {
+ return 0, errors.New("Seek: invalid offset")
+ }
+ if offset <= p.peek.Size() {
+ _, err := p.peek.Seek(offset, io.SeekStart)
+ if err != nil {
+ return 0, err
+ }
+ _, err = p.file.Seek(0, io.SeekStart)
+ if err != nil {
+ return 0, err
+ }
+ } else {
+ _, err := p.peek.Seek(p.peek.Size(), io.SeekStart)
+ if err != nil {
+ return 0, err
+ }
+ _, err = p.file.Seek(offset-p.peek.Size(), io.SeekStart)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ p.offset = offset
+ return offset, nil
+}
+
+func (p *PeekFile) Size() int64 {
+ return p.size
+}
+
+func NewPeekFile(peek *Reader, file *os.File) (*PeekFile, error) {
+ stat, err := file.Stat()
+ if err == nil {
+ return &PeekFile{peek: peek, file: file, size: stat.Size() + peek.Size()}, nil
+ }
+ return nil, err
+}
diff --git a/pkg/gowebdav/errors.go b/pkg/gowebdav/errors.go
index bbf1e929..de03e7b9 100644
--- a/pkg/gowebdav/errors.go
+++ b/pkg/gowebdav/errors.go
@@ -1,8 +1,8 @@
package gowebdav
import (
- "fmt"
"os"
+ "strconv"
)
// StatusError implements error and wraps
@@ -12,7 +12,7 @@ type StatusError struct {
}
func (se StatusError) Error() string {
- return fmt.Sprintf("%d", se.Status)
+ return strconv.Itoa(se.Status)
}
// IsErrCode returns true if the given error
diff --git a/pkg/singleflight/singleflight.go b/pkg/singleflight/singleflight.go
index 3555d5bd..48383478 100644
--- a/pkg/singleflight/singleflight.go
+++ b/pkg/singleflight/singleflight.go
@@ -73,9 +73,6 @@ type call[T any] struct {
type Group[T any] struct {
mu sync.Mutex // protects m
m map[string]*call[T] // lazily initialized
-
- // Won't remember error
- Remember bool
}
// Result holds the results of Do, so they can be passed
@@ -159,7 +156,7 @@ func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) {
g.mu.Lock()
defer g.mu.Unlock()
c.wg.Done()
- if (!g.Remember || c.err != nil) && g.m[key] == c {
+ if g.m[key] == c {
delete(g.m, key)
}
diff --git a/pkg/utils/hash.go b/pkg/utils/hash.go
index 0b70e4e1..596e61e5 100644
--- a/pkg/utils/hash.go
+++ b/pkg/utils/hash.go
@@ -57,6 +57,11 @@ var (
Supported []*HashType
)
+func GetHashByName(name string) (ht *HashType, ok bool) {
+ ht, ok = name2hash[name]
+ return
+}
+
// RegisterHash adds a new Hash to the list and returns its Type
func RegisterHash(name, alias string, width int, newFunc func() hash.Hash) *HashType {
return RegisterHashWithParam(name, alias, width, func(a ...any) hash.Hash { return newFunc() })
diff --git a/pkg/utils/io.go b/pkg/utils/io.go
index 172dc41c..f398161f 100644
--- a/pkg/utils/io.go
+++ b/pkg/utils/io.go
@@ -187,40 +187,41 @@ func NewClosers(c ...io.Closer) Closers {
return Closers(c)
}
-type SyncClosersIF interface {
- ClosersIF
- AcquireReference() bool
-}
-
type SyncClosers struct {
closers []io.Closer
ref int32
}
-var _ SyncClosersIF = (*SyncClosers)(nil)
-
+// if closed, return false
func (c *SyncClosers) AcquireReference() bool {
ref := atomic.AddInt32(&c.ref, 1)
if ref > 0 {
- // log.Debugf("SyncClosers.AcquireReference %p,ref=%d\n", c, ref)
+ // log.Debugf("AcquireReference %p: %d", c, ref)
return true
}
- atomic.StoreInt32(&c.ref, math.MinInt16)
+ atomic.StoreInt32(&c.ref, closersClosed)
return false
}
+const closersClosed = math.MinInt32
+
func (c *SyncClosers) Close() error {
- ref := atomic.AddInt32(&c.ref, -1)
- if ref < -1 {
- atomic.StoreInt32(&c.ref, math.MinInt16)
- return nil
- }
- // log.Debugf("SyncClosers.Close %p,ref=%d\n", c, ref+1)
- if ref > 0 {
- return nil
+ for {
+ ref := atomic.LoadInt32(&c.ref)
+ if ref < 0 {
+ return nil
+ }
+ if ref > 1 {
+ if atomic.CompareAndSwapInt32(&c.ref, ref, ref-1) {
+ // log.Debugf("ReleaseReference %p: %d", c, ref)
+ return nil
+ }
+ } else if atomic.CompareAndSwapInt32(&c.ref, ref, closersClosed) {
+ break
+ }
}
- atomic.StoreInt32(&c.ref, math.MinInt16)
+ // log.Debugf("FinalClose %p", c)
var errs []error
for _, closer := range c.closers {
if closer != nil {
@@ -250,6 +251,16 @@ func (c *SyncClosers) AddIfCloser(a any) {
}
}
+var _ ClosersIF = (*SyncClosers)(nil)
+
+// 实现cache.Expirable接口
+func (c *SyncClosers) Expired() bool {
+ return atomic.LoadInt32(&c.ref) < 0
+}
+func (c *SyncClosers) Length() int {
+ return len(c.closers)
+}
+
func NewSyncClosers(c ...io.Closer) SyncClosers {
return SyncClosers{closers: c}
}
diff --git a/server/common/proxy.go b/server/common/proxy.go
index d352582e..c7c975d2 100644
--- a/server/common/proxy.go
+++ b/server/common/proxy.go
@@ -18,11 +18,11 @@ import (
)
func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model.Obj) error {
- if link.MFile != nil {
- attachHeader(w, file, link)
- http.ServeContent(w, r, file.GetName(), file.ModTime(), link.MFile)
- return nil
- }
+ // if link.MFile != nil {
+ // attachHeader(w, file, link)
+ // http.ServeContent(w, r, file.GetName(), file.ModTime(), link.MFile)
+ // return nil
+ // }
if link.Concurrency > 0 || link.PartSize > 0 {
attachHeader(w, file, link)
@@ -101,7 +101,7 @@ func GetEtag(file model.Obj, size int64) string {
}
func ProxyRange(ctx context.Context, link *model.Link, size int64) *model.Link {
- if link.MFile == nil && link.RangeReader == nil && !strings.HasPrefix(link.URL, GetApiUrl(ctx)+"/") {
+ if link.RangeReader == nil && !strings.HasPrefix(link.URL, GetApiUrl(ctx)+"/") {
if link.ContentLength > 0 {
size = link.ContentLength
}
diff --git a/server/ftp.go b/server/ftp.go
index 40fb3716..fb7d86c8 100644
--- a/server/ftp.go
+++ b/server/ftp.go
@@ -13,6 +13,7 @@ import (
"strings"
"sync"
+ "github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
@@ -32,6 +33,7 @@ type FtpMainDriver struct {
}
func NewMainDriver() (*FtpMainDriver, error) {
+ ftp.InitStage()
transferType := ftpserver.TransferTypeASCII
if conf.Conf.FTP.DefaultTransferBinary {
transferType = ftpserver.TransferTypeBinary
@@ -79,7 +81,7 @@ func NewMainDriver() (*FtpMainDriver, error) {
PasvConnectionsCheck: pasvConnCheck,
},
proxyHeader: http.Header{
- "User-Agent": {setting.GetStr(conf.FTPProxyUserAgent)},
+ "User-Agent": {base.UserAgent},
},
clients: make(map[uint32]ftpserver.ClientContext),
shutdownLock: sync.RWMutex{},
diff --git a/server/ftp/afero.go b/server/ftp/afero.go
index f5bfda5f..8b7b467f 100644
--- a/server/ftp/afero.go
+++ b/server/ftp/afero.go
@@ -3,6 +3,8 @@ package ftp
import (
"context"
"errors"
+ "fmt"
+ "io"
"os"
"strings"
"time"
@@ -97,6 +99,23 @@ func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserve
if err != nil {
return nil, err
}
+ if f, err := Borrow(a.ctx, path); !errors.Is(err, errs.ObjectNotFound) {
+ if err != nil {
+ return nil, err
+ }
+ if (flags & os.O_EXCL) != 0 {
+ return nil, errors.New("file already exists")
+ }
+ if (flags & os.O_WRONLY) != 0 {
+ return nil, errors.New("cannot write to uploading file")
+ }
+ _, err = f.Seek(offset, io.SeekStart)
+ if err != nil {
+ _ = f.Close()
+ return nil, fmt.Errorf("failed seek borrow: %+v", err)
+ }
+ return f, nil
+ }
_, err = fs.Get(a.ctx, path, &fs.GetArgs{})
exists := err == nil
if (flags&os.O_CREATE) == 0 && !exists {
diff --git a/server/ftp/fsmanage.go b/server/ftp/fsmanage.go
index 045ce76c..48f72794 100644
--- a/server/ftp/fsmanage.go
+++ b/server/ftp/fsmanage.go
@@ -2,7 +2,6 @@ package ftp
import (
"context"
- "fmt"
stdpath "path"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
@@ -43,6 +42,9 @@ func Remove(ctx context.Context, path string) error {
if err != nil {
return err
}
+ if err = RemoveStage(reqPath); !errors.Is(err, errs.ObjectNotFound) {
+ return err
+ }
return fs.Remove(ctx, reqPath)
}
@@ -62,23 +64,24 @@ func Rename(ctx context.Context, oldPath, newPath string) error {
if !user.CanRename() || !user.CanFTPManage() {
return errs.PermissionDenied
}
+ if err = MoveStage(srcPath, dstPath); !errors.Is(err, errs.ObjectNotFound) {
+ return err
+ }
return fs.Rename(ctx, srcPath, dstBase)
} else {
if !user.CanFTPManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {
return errs.PermissionDenied
}
- if _, err = fs.Move(ctx, srcPath, dstDir); err != nil {
- if srcBase != dstBase {
- return err
- }
- if _, err1 := fs.Copy(ctx, srcPath, dstDir); err1 != nil {
- return fmt.Errorf("failed move for %+v, and failed try copying for %+v", err, err1)
- }
- return nil
+ if err = MoveStage(srcPath, dstPath); !errors.Is(err, errs.ObjectNotFound) {
+ return err
}
if srcBase != dstBase {
- return fs.Rename(ctx, stdpath.Join(dstDir, srcBase), dstBase)
+ err = fs.Rename(ctx, srcPath, dstBase, true)
+ if err != nil {
+ return err
+ }
}
- return nil
+ _, err = fs.Move(ctx, stdpath.Join(srcDir, dstBase), dstDir)
+ return err
}
}
diff --git a/server/ftp/fsread.go b/server/ftp/fsread.go
index 61244c01..9080bae1 100644
--- a/server/ftp/fsread.go
+++ b/server/ftp/fsread.go
@@ -63,19 +63,19 @@ func OpenDownload(ctx context.Context, reqPath string, offset int64) (*FileDownl
func (f *FileDownloadProxy) Read(p []byte) (n int, err error) {
n, err = f.File.Read(p)
if err != nil {
- return
+ return n, err
}
err = stream.ClientDownloadLimit.WaitN(f.ctx, n)
- return
+ return n, err
}
func (f *FileDownloadProxy) ReadAt(p []byte, off int64) (n int, err error) {
n, err = f.File.ReadAt(p, off)
if err != nil {
- return
+ return n, err
}
err = stream.ClientDownloadLimit.WaitN(f.ctx, n)
- return
+ return n, err
}
func (f *FileDownloadProxy) Write(p []byte) (n int, err error) {
@@ -95,7 +95,7 @@ func (o *OsFileInfoAdapter) Size() int64 {
}
func (o *OsFileInfoAdapter) Mode() fs2.FileMode {
- var mode fs2.FileMode = 0755
+ var mode fs2.FileMode = 0o755
if o.IsDir() {
mode |= fs2.ModeDir
}
@@ -130,6 +130,9 @@ func Stat(ctx context.Context, path string) (os.FileInfo, error) {
if !common.CanAccess(user, meta, reqPath, ctx.Value(conf.MetaPassKey).(string)) {
return nil, errs.PermissionDenied
}
+ if ret, err := StatStage(reqPath); !errors.Is(err, errs.ObjectNotFound) {
+ return ret, err
+ }
obj, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
if err != nil {
return nil, err
@@ -157,6 +160,13 @@ func List(ctx context.Context, path string) ([]os.FileInfo, error) {
if err != nil {
return nil, err
}
+ uploading := ListStage(reqPath)
+ for _, o := range objs {
+ delete(uploading, o.GetName())
+ }
+ for _, u := range uploading {
+ objs = append(objs, u)
+ }
ret := make([]os.FileInfo, len(objs))
for i, obj := range objs {
ret[i] = &OsFileInfoAdapter{obj: obj}
diff --git a/server/ftp/fsup.go b/server/ftp/fsup.go
index 7b6241ff..34fb36c6 100644
--- a/server/ftp/fsup.go
+++ b/server/ftp/fsup.go
@@ -3,6 +3,7 @@ package ftp
import (
"bytes"
"context"
+ "fmt"
"io"
"net/http"
"os"
@@ -62,10 +63,10 @@ func (f *FileUploadProxy) Read(p []byte) (n int, err error) {
func (f *FileUploadProxy) Write(p []byte) (n int, err error) {
n, err = f.buffer.Write(p)
if err != nil {
- return
+ return n, err
}
err = stream.ClientUploadLimit.WaitN(f.ctx, n)
- return
+ return n, err
}
func (f *FileUploadProxy) Seek(offset int64, whence int) (int64, error) {
@@ -89,6 +90,25 @@ func (f *FileUploadProxy) Close() error {
if _, err := f.buffer.Seek(0, io.SeekStart); err != nil {
return err
}
+ user := f.ctx.Value(conf.UserKey).(*model.User)
+ sf, borrow, err := MakeStage(f.ctx, f.buffer, size, f.path, func(target string) {
+ ctx := context.WithValue(context.Background(), conf.UserKey, user)
+ dstDir, dstBase := stdpath.Split(target)
+ if dir == dstDir {
+ _ = fs.Rename(ctx, f.path, dstBase)
+ } else {
+ if name != dstBase {
+ e := fs.Rename(ctx, f.path, dstBase, true)
+ if e != nil {
+ return
+ }
+ }
+ _, _ = fs.Move(ctx, stdpath.Join(dir, dstBase), dstDir)
+ }
+ })
+ if err != nil {
+ return fmt.Errorf("failed make stage for [%s]: %+v", f.path, err)
+ }
if f.trunc {
_ = fs.Remove(f.ctx, f.path)
}
@@ -100,10 +120,18 @@ func (f *FileUploadProxy) Close() error {
},
Mimetype: contentType,
WebPutAsTask: true,
+ Reader: f.buffer,
}
- s.SetTmpFile(f.buffer)
- _, err = fs.PutAsTask(f.ctx, dir, s)
- return err
+ s.Add(borrow)
+ task, err := fs.PutAsTask(f.ctx, dir, s)
+ if err != nil {
+ _ = s.Close()
+ return err
+ }
+ sf.SetRemoveCallback(func() {
+ fs.UploadTaskManager.Cancel(task.GetID())
+ })
+ return nil
}
type FileUploadWithLengthProxy struct {
@@ -182,10 +210,10 @@ func (f *FileUploadWithLengthProxy) write(p []byte) (n int, err error) {
func (f *FileUploadWithLengthProxy) Write(p []byte) (n int, err error) {
n, err = f.write(p)
if err != nil {
- return
+ return n, err
}
err = stream.ClientUploadLimit.WaitN(f.ctx, n)
- return
+ return n, err
}
func (f *FileUploadWithLengthProxy) Seek(offset int64, whence int) (int64, error) {
@@ -214,6 +242,6 @@ func (f *FileUploadWithLengthProxy) Close() error {
WebPutAsTask: false,
Reader: bytes.NewReader(data),
}
- return fs.PutDirectly(f.ctx, dir, s, true)
+ return fs.PutDirectly(f.ctx, dir, s)
}
}
diff --git a/server/ftp/upload_stage.go b/server/ftp/upload_stage.go
new file mode 100644
index 00000000..df0b5083
--- /dev/null
+++ b/server/ftp/upload_stage.go
@@ -0,0 +1,269 @@
+package ftp
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/OpenListTeam/OpenList/v4/internal/errs"
+ "github.com/OpenListTeam/OpenList/v4/internal/model"
+ "github.com/OpenListTeam/OpenList/v4/internal/stream"
+ log "github.com/sirupsen/logrus"
+ "github.com/tchap/go-patricia/v2/patricia"
+)
+
+var (
+ stage *patricia.Trie
+ stageMutex = sync.Mutex{}
+ ErrStagePathConflict = errors.New("upload path conflict")
+ ErrStageMoved = errors.New("uploading file has been moved")
+)
+
+func InitStage() {
+ if stage != nil {
+ return
+ }
+ stage = patricia.NewTrie(patricia.MaxPrefixPerNode(16))
+}
+
+type UploadingFile struct {
+ name string
+ size int64
+ modTime time.Time
+ refCount int
+ currentPath string
+ softLinks []patricia.Prefix
+ mvCallback func(string)
+ rmCallback func()
+}
+
+func (u *UploadingFile) SetRemoveCallback(rm func()) {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ u.rmCallback = rm
+}
+
+type softLink struct {
+ target *UploadingFile
+}
+
+func MakeStage(ctx context.Context, buffer *os.File, size int64, path string, mv func(string)) (*UploadingFile, *BorrowedFile, error) {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ prefix := patricia.Prefix(path)
+ f := &UploadingFile{
+ name: buffer.Name(),
+ size: size,
+ modTime: time.Now(),
+ refCount: 1,
+ currentPath: path,
+ softLinks: []patricia.Prefix{},
+ mvCallback: mv,
+ }
+ if !stage.Insert(prefix, f) {
+ return nil, nil, ErrStagePathConflict
+ }
+ log.Debugf("[ftp-stage] succeed to make [%s] stage", buffer.Name())
+ return f, &BorrowedFile{
+ file: buffer,
+ path: prefix,
+ ctx: ctx,
+ }, nil
+}
+
+func Borrow(ctx context.Context, path string) (*BorrowedFile, error) {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ prefix := patricia.Prefix(path)
+ v := stage.Get(prefix)
+ if v == nil {
+ return nil, errs.ObjectNotFound
+ }
+ s, ok := v.(*UploadingFile)
+ if !ok {
+ s = v.(*softLink).target
+ }
+ if s.currentPath != path {
+ return nil, ErrStageMoved
+ }
+ borrowed, err := os.OpenFile(s.name, os.O_RDONLY, 0o644)
+ if err != nil {
+ return nil, fmt.Errorf("failed borrow [%s]: %+v", s.name, err)
+ }
+ s.refCount++
+ log.Debugf("[ftp-stage] borrow [%s] succeed", s.name)
+ return &BorrowedFile{
+ file: borrowed,
+ path: prefix,
+ ctx: ctx,
+ }, nil
+}
+
+func drop(path patricia.Prefix) {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ v := stage.Get(path)
+ if v == nil {
+ return
+ }
+ s, ok := v.(*UploadingFile)
+ if !ok {
+ s = v.(*softLink).target
+ }
+ s.refCount--
+ log.Debugf("[ftp-stage] dropped [%s]", s.name)
+ if s.refCount == 0 {
+ log.Debugf("[ftp-stage] there is no more reference to [%s], removing temp file", s.name)
+ err := os.RemoveAll(s.name)
+ if err != nil {
+ log.Errorf("[ftp-stage] failed to remove stage file [%s]: %+v", s.name, err)
+ }
+ for _, sl := range s.softLinks {
+ stage.Delete(sl)
+ }
+ stage.Delete(path)
+ if s.currentPath != string(path) {
+ if s.currentPath != "" {
+ go s.mvCallback(s.currentPath)
+ }
+ }
+ }
+}
+
+func ListStage(path string) map[string]model.Obj {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ path = path + "/"
+ prefix := patricia.Prefix(path)
+ ret := make(map[string]model.Obj)
+ _ = stage.VisitSubtree(prefix, func(prefix patricia.Prefix, item patricia.Item) error {
+ visit := string(prefix)
+ visitSub := strings.TrimPrefix(visit, path)
+ name, _, nonDirect := strings.Cut(visitSub, "/")
+ if nonDirect {
+ return nil
+ }
+ f, ok := item.(*UploadingFile)
+ if !ok {
+ f = item.(*softLink).target
+ }
+ if f.currentPath == visit {
+ ret[name] = &model.Object{
+ Path: visit,
+ Name: name,
+ Size: f.size,
+ Modified: f.modTime,
+ IsFolder: false,
+ }
+ }
+ return nil
+ })
+ return ret
+}
+
+func StatStage(path string) (os.FileInfo, error) {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ prefix := patricia.Prefix(path)
+ v := stage.Get(prefix)
+ if v == nil {
+ return nil, errs.ObjectNotFound
+ }
+ s, ok := v.(*UploadingFile)
+ if !ok {
+ s = v.(*softLink).target
+ }
+ if s.currentPath != path {
+ return nil, ErrStageMoved
+ }
+ return os.Stat(s.name)
+}
+
+func MoveStage(from, to string) error {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ prefix := patricia.Prefix(from)
+ v := stage.Get(prefix)
+ if v == nil {
+ return errs.ObjectNotFound
+ }
+ s, ok := v.(*UploadingFile)
+ if !ok {
+ s = v.(*softLink).target
+ }
+ if s.currentPath != from {
+ return ErrStageMoved
+ }
+ slPrefix := patricia.Prefix(to)
+ sl := &softLink{target: s}
+ if !stage.Insert(slPrefix, sl) {
+ return ErrStagePathConflict
+ }
+ s.currentPath = to
+ s.softLinks = append(s.softLinks, slPrefix)
+ return nil
+}
+
+func RemoveStage(path string) error {
+ stageMutex.Lock()
+ defer stageMutex.Unlock()
+ prefix := patricia.Prefix(path)
+ v := stage.Get(prefix)
+ if v == nil {
+ return errs.ObjectNotFound
+ }
+ s, ok := v.(*UploadingFile)
+ if !ok {
+ s = v.(*softLink).target
+ }
+ if s.currentPath != path {
+ return ErrStageMoved
+ }
+ s.currentPath = ""
+ if s.rmCallback != nil {
+ s.rmCallback()
+ }
+ return nil
+}
+
+type BorrowedFile struct {
+ file *os.File
+ path patricia.Prefix
+ ctx context.Context
+}
+
+func (f *BorrowedFile) Read(p []byte) (n int, err error) {
+ n, err = f.file.Read(p)
+ if err != nil {
+ return n, err
+ }
+ err = stream.ClientDownloadLimit.WaitN(f.ctx, n)
+ return n, err
+}
+
+func (f *BorrowedFile) ReadAt(p []byte, off int64) (n int, err error) {
+ n, err = f.file.ReadAt(p, off)
+ if err != nil {
+ return n, err
+ }
+ err = stream.ClientDownloadLimit.WaitN(f.ctx, n)
+ return n, err
+}
+
+func (f *BorrowedFile) Seek(offset int64, whence int) (int64, error) {
+ return f.file.Seek(offset, whence)
+}
+
+func (f *BorrowedFile) Write(_ []byte) (n int, err error) {
+ return 0, errs.NotSupport
+}
+
+func (f *BorrowedFile) Close() error {
+ err := f.file.Close()
+ drop(f.path)
+ return err
+}
diff --git a/server/handles/down.go b/server/handles/down.go
index 157eeafc..db2aac4b 100644
--- a/server/handles/down.go
+++ b/server/handles/down.go
@@ -9,6 +9,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
+ "github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/fs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/net"
@@ -49,13 +50,13 @@ func Down(c *gin.Context) {
func Proxy(c *gin.Context) {
rawPath := c.Request.Context().Value(conf.PathKey).(string)
- //filename := stdpath.Base(rawPath)
+ filename := stdpath.Base(rawPath)
storage, err := fs.GetStorage(rawPath, &fs.GetStoragesArgs{})
if err != nil {
common.ErrorPage(c, err, 500)
return
}
- if true {
+ if canProxy(storage, filename) {
if _, ok := c.GetQuery("d"); !ok {
if url := common.GenerateDownProxyURL(storage.GetStorage(), rawPath); url != "" {
c.Redirect(302, url)
@@ -114,9 +115,8 @@ func proxy(c *gin.Context, link *model.Link, file model.Obj, proxyRange bool) {
link = common.ProxyRange(c, link, file.GetSize())
}
Writer := &common.WrittenResponseWriter{ResponseWriter: c.Writer}
-
- //优先处理md文件
- if utils.Ext(file.GetName()) == "md" && setting.GetBool(conf.FilterReadMeScripts) {
+ raw, _ := strconv.ParseBool(c.DefaultQuery("raw", "false"))
+ if utils.Ext(file.GetName()) == "md" && setting.GetBool(conf.FilterReadMeScripts) && !raw {
buf := bytes.NewBuffer(make([]byte, 0, file.GetSize()))
w := &common.InterceptResponseWriter{ResponseWriter: Writer, Writer: buf}
err = common.Proxy(w, c.Request, link, file)
@@ -148,7 +148,7 @@ func proxy(c *gin.Context, link *model.Link, file model.Obj, proxyRange bool) {
if Writer.IsWritten() {
log.Errorf("%s %s local proxy error: %+v", c.Request.Method, c.Request.URL.Path, err)
} else {
- if statusCode, ok := errors.Unwrap(err).(net.ErrorHttpStatusCode); ok {
+ if statusCode, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok {
common.ErrorPage(c, err, int(statusCode), true)
} else {
common.ErrorPage(c, err, 500, true)
@@ -173,5 +173,5 @@ func canProxy(storage driver.Driver, filename string) bool {
if utils.SliceContains(conf.SlicesMap[conf.TextTypes], utils.Ext(filename)) {
return true
}
- return false
+ return true
}
diff --git a/server/handles/fsmanage.go b/server/handles/fsmanage.go
index f45da69b..3fe86726 100644
--- a/server/handles/fsmanage.go
+++ b/server/handles/fsmanage.go
@@ -386,7 +386,7 @@ func Link(c *gin.Context) {
common.ErrorResp(c, err, 500)
return
}
- if storage.Config().NoLinkURL || storage.Config().OnlyLinkMFile {
+ if storage.Config().NoLinkURL {
common.SuccessResp(c, model.Link{
URL: fmt.Sprintf("%s/p%s?d&sign=%s",
common.GetApiUrl(c),
diff --git a/server/handles/fsread.go b/server/handles/fsread.go
index b9a4f09d..6665094c 100644
--- a/server/handles/fsread.go
+++ b/server/handles/fsread.go
@@ -33,18 +33,19 @@ type DirReq struct {
}
type ObjResp struct {
- Id string `json:"id"`
- Path string `json:"path"`
- Name string `json:"name"`
- Size int64 `json:"size"`
- IsDir bool `json:"is_dir"`
- Modified time.Time `json:"modified"`
- Created time.Time `json:"created"`
- Sign string `json:"sign"`
- Thumb string `json:"thumb"`
- Type int `json:"type"`
- HashInfoStr string `json:"hashinfo"`
- HashInfo map[*utils.HashType]string `json:"hash_info"`
+ Id string `json:"id"`
+ Path string `json:"path"`
+ Name string `json:"name"`
+ Size int64 `json:"size"`
+ IsDir bool `json:"is_dir"`
+ Modified time.Time `json:"modified"`
+ Created time.Time `json:"created"`
+ Sign string `json:"sign"`
+ Thumb string `json:"thumb"`
+ Type int `json:"type"`
+ HashInfoStr string `json:"hashinfo"`
+ HashInfo map[*utils.HashType]string `json:"hash_info"`
+ MountDetails *model.StorageDetailsWithName `json:"mount_details,omitempty"`
}
type FsListResp struct {
@@ -98,7 +99,10 @@ func FsList(c *gin.Context, req *ListReq, user *model.User) {
common.ErrorStrResp(c, "Refresh without permission", 403)
return
}
- objs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{Refresh: req.Refresh})
+ objs, err := fs.List(c.Request.Context(), reqPath, &fs.ListArgs{
+ Refresh: req.Refresh,
+ WithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),
+ })
if err != nil {
common.ErrorResp(c, err, 500)
return
@@ -224,19 +228,21 @@ func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
var resp []ObjResp
for _, obj := range objs {
thumb, _ := model.GetThumb(obj)
+ mountDetails, _ := model.GetStorageDetails(obj)
resp = append(resp, ObjResp{
- Id: obj.GetID(),
- Path: obj.GetPath(),
- Name: obj.GetName(),
- Size: obj.GetSize(),
- IsDir: obj.IsDir(),
- Modified: obj.ModTime(),
- Created: obj.CreateTime(),
- HashInfoStr: obj.GetHash().String(),
- HashInfo: obj.GetHash().Export(),
- Sign: common.Sign(obj, parent, encrypt),
- Thumb: thumb,
- Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
+ Id: obj.GetID(),
+ Path: obj.GetPath(),
+ Name: obj.GetName(),
+ Size: obj.GetSize(),
+ IsDir: obj.IsDir(),
+ Modified: obj.ModTime(),
+ Created: obj.CreateTime(),
+ HashInfoStr: obj.GetHash().String(),
+ HashInfo: obj.GetHash().Export(),
+ Sign: common.Sign(obj, parent, encrypt),
+ Thumb: thumb,
+ Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
+ MountDetails: mountDetails,
})
}
return resp
@@ -293,7 +299,9 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return
}
- obj, err := fs.Get(c.Request.Context(), reqPath, &fs.GetArgs{})
+ obj, err := fs.Get(c.Request.Context(), reqPath, &fs.GetArgs{
+ WithStorageDetails: !user.IsGuest() && !setting.GetBool(conf.HideStorageDetails),
+ })
if err != nil {
common.ErrorResp(c, err, 500)
return
@@ -301,8 +309,8 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
var rawURL string
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
- provider := "unknown"
- if err == nil {
+ provider, ok := model.GetProvider(obj)
+ if !ok && err == nil {
provider = storage.Config().Name
}
if !obj.IsDir() {
@@ -350,20 +358,22 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
}
parentMeta, _ := op.GetNearestMeta(parentPath)
thumb, _ := model.GetThumb(obj)
+ mountDetails, _ := model.GetStorageDetails(obj)
common.SuccessResp(c, FsGetResp{
ObjResp: ObjResp{
- Id: obj.GetID(),
- Path: obj.GetPath(),
- Name: obj.GetName(),
- Size: obj.GetSize(),
- IsDir: obj.IsDir(),
- Modified: obj.ModTime(),
- Created: obj.CreateTime(),
- HashInfoStr: obj.GetHash().String(),
- HashInfo: obj.GetHash().Export(),
- Sign: common.Sign(obj, parentPath, isEncrypt(meta, reqPath)),
- Type: utils.GetFileType(obj.GetName()),
- Thumb: thumb,
+ Id: obj.GetID(),
+ Path: obj.GetPath(),
+ Name: obj.GetName(),
+ Size: obj.GetSize(),
+ IsDir: obj.IsDir(),
+ Modified: obj.ModTime(),
+ Created: obj.CreateTime(),
+ HashInfoStr: obj.GetHash().String(),
+ HashInfo: obj.GetHash().Export(),
+ Sign: common.Sign(obj, parentPath, isEncrypt(meta, reqPath)),
+ Type: utils.GetFileType(obj.GetName()),
+ Thumb: thumb,
+ MountDetails: mountDetails,
},
RawURL: rawURL,
Readme: getReadme(meta, reqPath),
diff --git a/server/handles/fsup.go b/server/handles/fsup.go
index 087a58a9..71d9dbae 100644
--- a/server/handles/fsup.go
+++ b/server/handles/fsup.go
@@ -56,14 +56,17 @@ func FsStream(c *gin.Context) {
}
}
dir, name := stdpath.Split(path)
- sizeStr := c.GetHeader("Content-Length")
- if sizeStr == "" {
- sizeStr = "0"
- }
- size, err := strconv.ParseInt(sizeStr, 10, 64)
- if err != nil {
- common.ErrorResp(c, err, 400)
- return
+ // 如果请求头 Content-Length 和 X-File-Size 都没有,则 size=-1,表示未知大小的流式上传
+ size := c.Request.ContentLength
+ if size < 0 {
+ sizeStr := c.GetHeader("X-File-Size")
+ if sizeStr != "" {
+ size, err = strconv.ParseInt(sizeStr, 10, 64)
+ if err != nil {
+ common.ErrorResp(c, err, 400)
+ return
+ }
+ }
}
h := make(map[*utils.HashType]string)
if md5 := c.GetHeader("X-File-Md5"); md5 != "" {
diff --git a/server/handles/offline_download.go b/server/handles/offline_download.go
index fb652734..9d3f2230 100644
--- a/server/handles/offline_download.go
+++ b/server/handles/offline_download.go
@@ -2,9 +2,10 @@ package handles
import (
"strings"
-
+
_115 "github.com/OpenListTeam/OpenList/v4/drivers/115"
_115_open "github.com/OpenListTeam/OpenList/v4/drivers/115_open"
+ _123_open "github.com/OpenListTeam/OpenList/v4/drivers/123_open"
"github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
"github.com/OpenListTeam/OpenList/v4/drivers/thunder"
"github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser"
@@ -200,6 +201,52 @@ func Set115Open(c *gin.Context) {
common.SuccessResp(c, "ok")
}
+type Set123OpenReq struct {
+ TempDir string `json:"temp_dir" form:"temp_dir"`
+ CallbackUrl string `json:"callback_url" form:"callback_url"`
+}
+
+func Set123Open(c *gin.Context) {
+ var req Set123OpenReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ErrorResp(c, err, 400)
+ return
+ }
+ if req.TempDir != "" {
+ storage, _, err := op.GetStorageAndActualPath(req.TempDir)
+ if err != nil {
+ common.ErrorStrResp(c, "storage does not exists", 400)
+ return
+ }
+ if storage.Config().CheckStatus && storage.GetStorage().Status != op.WORK {
+ common.ErrorStrResp(c, "storage not init: "+storage.GetStorage().Status, 400)
+ return
+ }
+ if _, ok := storage.(*_123_open.Open123); !ok {
+ common.ErrorStrResp(c, "unsupported storage driver for offline download, only 123 Open is supported", 400)
+ return
+ }
+ }
+ items := []model.SettingItem{
+ {Key: conf.Pan123OpenTempDir, Value: req.TempDir, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
+ {Key: conf.Pan123OpenOfflineDownloadCallbackUrl, Value: req.CallbackUrl, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
+ }
+ if err := op.SaveSettingItems(items); err != nil {
+ common.ErrorResp(c, err, 500)
+ return
+ }
+ _tool, err := tool.Tools.Get("123 Open")
+ if err != nil {
+ common.ErrorResp(c, err, 500)
+ return
+ }
+ if _, err := _tool.Init(); err != nil {
+ common.ErrorResp(c, err, 500)
+ return
+ }
+ common.SuccessResp(c, "ok")
+}
+
type SetPikPakReq struct {
TempDir string `json:"temp_dir" form:"temp_dir"`
}
@@ -413,7 +460,7 @@ func AddOfflineDownload(c *gin.Context) {
if trimmedUrl == "" {
continue
}
-
+
t, err := tool.AddURL(c, &tool.AddURLArgs{
URL: trimmedUrl,
DstDirPath: reqPath,
diff --git a/server/handles/sharing.go b/server/handles/sharing.go
index 5b26fcf6..5a6ea265 100644
--- a/server/handles/sharing.go
+++ b/server/handles/sharing.go
@@ -195,6 +195,7 @@ func SharingArchiveList(c *gin.Context, req *ArchiveListReq) {
func SharingDown(c *gin.Context) {
sid := c.Request.Context().Value(conf.SharingIDKey).(string)
path := c.Request.Context().Value(conf.PathKey).(string)
+ path = utils.FixAndCleanPath(path)
pwd := c.Query("pwd")
s, err := op.GetSharingById(sid)
if err == nil {
@@ -219,6 +220,13 @@ func SharingDown(c *gin.Context) {
return
}
if setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) {
+ if _, ok := c.GetQuery("d"); !ok {
+ if url := common.GenerateDownProxyURL(storage.GetStorage(), unwrapPath); url != "" {
+ c.Redirect(302, url)
+ _ = countAccess(c.ClientIP(), s)
+ return
+ }
+ }
link, obj, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{
Header: c.Request.Header,
Type: c.Query("type"),
@@ -252,6 +260,7 @@ func SharingArchiveExtract(c *gin.Context) {
}
sid := c.Request.Context().Value(conf.SharingIDKey).(string)
path := c.Request.Context().Value(conf.PathKey).(string)
+ path = utils.FixAndCleanPath(path)
pwd := c.Query("pwd")
innerPath := utils.FixAndCleanPath(c.Query("inner"))
archivePass := c.Query("pass")
diff --git a/server/handles/storage.go b/server/handles/storage.go
index 80951e54..e4b657cd 100644
--- a/server/handles/storage.go
+++ b/server/handles/storage.go
@@ -2,18 +2,67 @@ package handles
import (
"context"
+ "errors"
"github.com/OpenListTeam/OpenList/v4/internal/bootstrap"
"strconv"
+ "sync"
+ "time"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/db"
+ "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/op"
+ "github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
+type StorageResp struct {
+ model.Storage
+ MountDetails *model.StorageDetails `json:"mount_details,omitempty"`
+}
+
+func makeStorageResp(c *gin.Context, storages []model.Storage) []*StorageResp {
+ ret := make([]*StorageResp, len(storages))
+ var wg sync.WaitGroup
+ for i, s := range storages {
+ ret[i] = &StorageResp{
+ Storage: s,
+ MountDetails: nil,
+ }
+ if setting.GetBool(conf.HideStorageDetailsInManagePage) {
+ continue
+ }
+ d, err := op.GetStorageByMountPath(s.MountPath)
+ if err != nil {
+ continue
+ }
+ _, ok := d.(driver.WithDetails)
+ if !ok {
+ continue
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ ctx, cancel := context.WithTimeout(c, time.Second*3)
+ defer cancel()
+ details, err := op.GetStorageDetails(ctx, d)
+ if err != nil {
+ if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.StorageNotInit) {
+ log.Errorf("failed get %s details: %+v", s.MountPath, err)
+ }
+ return
+ }
+ ret[i].MountDetails = details
+ }()
+ }
+ wg.Wait()
+ return ret
+}
+
func ListStorages(c *gin.Context) {
var req model.PageReq
if err := c.ShouldBind(&req); err != nil {
@@ -28,7 +77,7 @@ func ListStorages(c *gin.Context) {
return
}
common.SuccessResp(c, common.PageResp{
- Content: storages,
+ Content: makeStorageResp(c, storages),
Total: total,
})
}
@@ -165,7 +214,7 @@ func LoadAllStorages(c *gin.Context) {
common.ErrorResp(c, err, 500, true)
return
}
- conf.StoragesLoaded = false
+ conf.ResetStoragesLoadSignal()
go func(storages []model.Storage) {
for _, storage := range storages {
storageDriver, err := op.GetStorageByMountPath(storage.MountPath)
@@ -185,7 +234,7 @@ func LoadAllStorages(c *gin.Context) {
log.Infof("success load storage: [%s], driver: [%s]",
storage.MountPath, storage.Driver)
}
- conf.StoragesLoaded = true
+ conf.SendStoragesLoadedSignal()
}(storages)
common.SuccessResp(c)
}
diff --git a/server/middlewares/check.go b/server/middlewares/check.go
index a1011de3..c7203a49 100644
--- a/server/middlewares/check.go
+++ b/server/middlewares/check.go
@@ -22,9 +22,12 @@ func StoragesLoaded(c *gin.Context) {
return
}
}
- common.ErrorStrResp(c, "Loading storage, please wait", 500)
- c.Abort()
- return
+ select {
+ case <-conf.StoragesLoadSignal():
+ case <-c.Request.Context().Done():
+ c.Abort()
+ return
+ }
}
common.GinWithValue(c,
conf.ApiUrlKey, common.GetApiUrlFromRequest(c.Request),
diff --git a/server/router.go b/server/router.go
index 2811afa4..3d21abc5 100644
--- a/server/router.go
+++ b/server/router.go
@@ -170,6 +170,7 @@ func admin(g *gin.RouterGroup) {
setting.POST("/set_transmission", handles.SetTransmission)
setting.POST("/set_115", handles.Set115)
setting.POST("/set_115_open", handles.Set115Open)
+ setting.POST("/set_123_open", handles.Set123Open)
setting.POST("/set_pikpak", handles.SetPikPak)
setting.POST("/set_thunder", handles.SetThunder)
setting.POST("/set_thunderx", handles.SetThunderX)
diff --git a/server/sftp.go b/server/sftp.go
index 0f9d9125..055f7973 100644
--- a/server/sftp.go
+++ b/server/sftp.go
@@ -5,6 +5,7 @@ import (
"net/http"
"time"
+ "github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
@@ -23,10 +24,11 @@ type SftpDriver struct {
}
func NewSftpDriver() (*SftpDriver, error) {
+ ftp.InitStage()
sftp.InitHostKey()
return &SftpDriver{
proxyHeader: http.Header{
- "User-Agent": {setting.GetStr(conf.FTPProxyUserAgent)},
+ "User-Agent": {base.UserAgent},
},
}, nil
}
@@ -35,10 +37,14 @@ func (d *SftpDriver) GetConfig() *sftpd.Config {
if d.config != nil {
return d.config
}
+ var pwdAuth func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) = nil
+ if !setting.GetBool(conf.SFTPDisablePasswordLogin) {
+ pwdAuth = d.PasswordAuth
+ }
serverConfig := ssh.ServerConfig{
NoClientAuth: true,
NoClientAuthCallback: d.NoClientAuth,
- PasswordCallback: d.PasswordAuth,
+ PasswordCallback: pwdAuth,
PublicKeyCallback: d.PublicKeyAuth,
AuthLogCallback: d.AuthLogCallback,
BannerCallback: d.GetBanner,
@@ -50,7 +56,7 @@ func (d *SftpDriver) GetConfig() *sftpd.Config {
ServerConfig: serverConfig,
HostPort: conf.Conf.SFTP.Listen,
ErrorLogFunc: utils.Log.Error,
- //DebugLogFunc: utils.Log.Debugf,
+ // DebugLogFunc: utils.Log.Debugf,
}
return d.config
}
diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go
index b6f7cdac..4612665b 100644
--- a/server/webdav/webdav.go
+++ b/server/webdav/webdav.go
@@ -14,6 +14,7 @@ import (
"net/url"
"os"
"path"
+ "strconv"
"strings"
"time"
@@ -271,7 +272,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
}
err = common.Proxy(w, r, link, fi)
if err != nil {
- if statusCode, ok := errors.Unwrap(err).(net.ErrorHttpStatusCode); ok {
+ if statusCode, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok {
return int(statusCode), err
}
return http.StatusInternalServerError, fmt.Errorf("webdav proxy error: %+v", err)
@@ -341,9 +342,19 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
if err != nil {
return http.StatusForbidden, err
}
+ size := r.ContentLength
+ if size < 0 {
+ sizeStr := r.Header.Get("X-File-Size")
+ if sizeStr != "" {
+ size, err = strconv.ParseInt(sizeStr, 10, 64)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ }
+ }
obj := model.Object{
Name: path.Base(reqPath),
- Size: r.ContentLength,
+ Size: size,
Modified: h.getModTime(r),
Ctime: h.getCreateTime(r),
}