diff --git a/drivers/189/driver.go b/drivers/189/driver.go index 0489ef015..80926e183 100644 --- a/drivers/189/driver.go +++ b/drivers/189/driver.go @@ -2,6 +2,7 @@ package _189 import ( "context" + "fmt" "net/http" "strings" @@ -204,4 +205,18 @@ func (d *Cloud189) GetDetails(ctx context.Context) (*model.StorageDetails, error }, nil } +func (d *Cloud189) Transfer(ctx context.Context, dst model.Obj, shareURL, validCode string) error { + sharecode := d.extractCode(shareURL) + if sharecode == "" { + return fmt.Errorf("need share code") + } + + shareid, err := d.getSharedID(sharecode) + if err != nil { + return err + } + + return d.transfer(dst, shareid) +} + var _ driver.Driver = (*Cloud189)(nil) diff --git a/drivers/189/help.go b/drivers/189/help.go index 50748a35c..682b6a42e 100644 --- a/drivers/189/help.go +++ b/drivers/189/help.go @@ -23,7 +23,7 @@ import ( ) func random() string { - return fmt.Sprintf("0.%17v", myrand.Rand.Int63n(100000000000000000)) + return fmt.Sprintf("0.%017d", myrand.Rand.Int63n(1e17)) } func RsaEncode(origData []byte, j_rsakey string, hex bool) string { diff --git a/drivers/189/types.go b/drivers/189/types.go index 7a469dc9a..350dea4d2 100644 --- a/drivers/189/types.go +++ b/drivers/189/types.go @@ -84,3 +84,11 @@ type CapacityResp struct { } `json:"familyCapacityInfo"` TotalSize uint64 `json:"totalSize"` } + +type GetSharedInfoResp struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + AccessCode string `json:"accessCode"` + ShareID int64 `json:"shareId"` + ExpireTime int64 `json:"expireTime"` +} diff --git a/drivers/189/util.go b/drivers/189/util.go index bb9a6adb4..9e53c221b 100644 --- a/drivers/189/util.go +++ b/drivers/189/util.go @@ -11,6 +11,7 @@ import ( "io" "math" "net/http" + "net/url" "strconv" "strings" "time" @@ -406,3 +407,54 @@ func (d *Cloud189) getCapacityInfo(ctx context.Context) (*CapacityResp, error) { } return &resp, nil } + +func (d *Cloud189) getSharedID(code string) (int64, error) { + resp := GetSharedInfoResp{} + _, err := d.request("https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action", http.MethodGet, func(req *resty.Request) { + req.SetHeader("Accept", "application/json;charset=UTF-8") + req.SetQueryParams(map[string]string{ + "code": code, + }) + }, &resp) + if err != nil { + return 0, err + } + if resp.ResCode != 0 || resp.ResMessage != "成功" { + return 0, errors.New(resp.ResMessage) + } + return resp.ShareID, nil +} + +func (d *Cloud189) extractCode(str string) string { + u, err := url.Parse(str) + if err != nil { + fmt.Println("invalid share url") + return "" + } + return u.Query().Get("code") +} + +func (d *Cloud189) transfer(dstDir model.Obj, shareID int64) error { + taskInfos := []base.Json{ + { + "fileId": dstDir.GetID(), + "fileName": dstDir.GetName(), + "isFolder": 1, + }, + } + + if !dstDir.IsDir() { + return fmt.Errorf("it should be in the folder") + } + taskInfosBytes, err := utils.Json.Marshal(taskInfos) + form := map[string]string{ + "type": "SHARE_SAVE", + "targetFolderId": dstDir.GetID(), + "taskInfos": string(taskInfosBytes), + "shareId": fmt.Sprintf("%d", shareID), + } + _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) { + req.SetFormData(form) + }, nil) + return err +} diff --git a/drivers/189pc/driver.go b/drivers/189pc/driver.go index 5b04d58f6..472a6d7ea 100644 --- a/drivers/189pc/driver.go +++ b/drivers/189pc/driver.go @@ -428,3 +428,57 @@ func (y *Cloud189PC) GetDetails(ctx context.Context) (*model.StorageDetails, err DiskUsage: driver.DiskUsageFromUsedAndTotal(used, total), }, nil } + +func (y *Cloud189PC) Transfer(ctx context.Context, dst model.Obj, shareURL, validCode string) error { + sharecode := y.extractCode(shareURL) + if sharecode == "" { + return fmt.Errorf("need share code") + } + shareid, fileid, filename, err := y.getSharedInfo(sharecode) + if err != nil { + return err + } + if shareid == -1 { + return fmt.Errorf("failed get share id") + } + + taskInfos := []base.Json{ + { + "fileId": fileid, + "fileName": filename, + "isFolder": 1, + }, + } + + if !dst.IsDir() { + return fmt.Errorf("it should be in the folder") + } + taskInfosBytes, err := utils.Json.Marshal(taskInfos) + if err != nil { + return err + } + + form := map[string]string{ + "targetFolderId": dst.GetID(), + "taskInfos": string(taskInfosBytes), + "shareId": fmt.Sprintf("%d", shareid), + } + + resp, err := y.CreateBatchTask("SHARE_SAVE", y.FamilyID, dst.GetID(), form) + + for { + state, err := y.CheckBatchTask("SHARE_SAVE", resp.TaskID) + if err != nil { + return err + } + switch state.TaskStatus { + case 4: + if state.FailedCount == 0 { + return nil + } else { + return fmt.Errorf("transfer failed") + } + } + time.Sleep(time.Millisecond * 400) + } +} diff --git a/drivers/189pc/help.go b/drivers/189pc/help.go index 6f6c59f30..d03385f0b 100644 --- a/drivers/189pc/help.go +++ b/drivers/189pc/help.go @@ -22,6 +22,10 @@ import ( "github.com/OpenListTeam/OpenList/v4/pkg/utils/random" ) +func random_num() string { + return fmt.Sprintf("0.%017d", random.Rand.Int63n(1e17)) +} + func clientSuffix() map[string]string { rand := random.Rand return map[string]string{ diff --git a/drivers/189pc/types.go b/drivers/189pc/types.go index c0ecacb24..90219836c 100644 --- a/drivers/189pc/types.go +++ b/drivers/189pc/types.go @@ -360,7 +360,9 @@ func (f *OldCommitUploadFileResp) toFile() *Cloud189File { } type CreateBatchTaskResp struct { - TaskID string `json:"taskId"` + ResCode int `json:"res_code"` + TaskID string `json:"taskId"` + ResMessage string `json:"res_message"` } type BatchTaskStateResp struct { @@ -371,6 +373,7 @@ type BatchTaskStateResp struct { SuccessedCount int `json:"successedCount"` SuccessedFileIDList []int64 `json:"successedFileIdList"` TaskID string `json:"taskId"` + ResMessage string `json:"res_message"` TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成 } @@ -427,3 +430,13 @@ type CapacityResp struct { } `json:"familyCapacityInfo"` TotalSize uint64 `json:"totalSize"` } + +type GetSharedInfoResp struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + AccessCode string `json:"accessCode"` + ShareID int64 `json:"shareId"` + ExpireTime int64 `json:"expireTime"` + FileName string `json:"fileName"` + FileId string `json:"fileId"` +} diff --git a/drivers/189pc/utils.go b/drivers/189pc/utils.go index 08ee658ca..a6b29e831 100644 --- a/drivers/189pc/utils.go +++ b/drivers/189pc/utils.go @@ -1483,3 +1483,51 @@ func (y *Cloud189PC) getCapacityInfo(ctx context.Context) (*CapacityResp, error) } return &resp, nil } + +func (y *Cloud189PC) getSharedInfo(code string) (int64, string, string, error) { + resp := GetSharedInfoResp{} + sessionKey := y.getTokenInfo().SessionKey + if sessionKey == "" { + if err := y.refreshSession(); err != nil { + return -1, "", "", err + } + } + req := y.getClient().R(). + SetHeader("Accept", "application/json;charset=UTF-8"). + SetHeader("Referer", fmt.Sprintf("https://cloud.189.cn/web/share?code=%s", code)). + SetHeader("SessionKey", y.getTokenInfo().SessionKey). + SetQueryParams(map[string]string{ + "noCache": random_num(), + "shareCode": code, + }) + req.SetResult(&resp) + res, err := req.Execute(http.MethodGet, "https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action") + + if strings.Contains(res.String(), "userSessionBO is null") { + if err = y.refreshSession(); err != nil { + return -1, "", "", err + } + return y.getSharedInfo(code) + } + + if strings.Contains(res.String(), "InvalidSessionKey") { + if err = y.refreshSession(); err != nil { + return -1, "", "", err + } + return y.getSharedInfo(code) + } + + if resp.ResCode != 0 || resp.ResMessage != "成功" { + return -1, "", "", errors.New(resp.ResMessage) + } + return resp.ShareID, resp.FileId, resp.FileName, nil +} + +func (y *Cloud189PC) extractCode(str string) string { + u, err := url.Parse(str) + if err != nil { + fmt.Println("invalid share url") + return "" + } + return u.Query().Get("code") +} diff --git a/drivers/aliyundrive/driver.go b/drivers/aliyundrive/driver.go index f9093097c..3e32b17e6 100644 --- a/drivers/aliyundrive/driver.go +++ b/drivers/aliyundrive/driver.go @@ -368,4 +368,46 @@ func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{} return resp, nil } +func (d *AliDrive) Transfer(ctx context.Context, dst model.Obj, shareURL, validCode string) error { + shareid := d.extractShareId(shareURL) + if shareid == "" { + return fmt.Errorf("invalid share url") + } + shareToken, err := d.getShareToken(shareid, validCode) + if err != nil { + return err + } + fileid, parent_fileid := d.getFileId(shareid, shareToken) + res, err, _ := d.request("https://api.aliyundrive.com/adrive/v4/batch", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "requests": []base.Json{ + { + "headers": base.Json{ + "Content-Type": "application/json", + }, + "method": "POST", + "id": "0", + "body": base.Json{ + "share_id": shareid, + "file_id": fileid, + "to_drive_id": dst.GetID(), + "to_parent_file_id": parent_fileid, + }, + "url": "/file/copy", + }, + }, + "resource": "file", + }) + req.SetHeader("X-Share-Token", shareToken) + }, nil) + if err != nil { + return err + } + status := utils.Json.Get(res, "responses", 0, "status").ToInt() + if status < 300 && status >= 100 { + return nil + } + return fmt.Errorf("transfer failed: %s", string(res)) +} + var _ driver.Driver = (*AliDrive)(nil) diff --git a/drivers/aliyundrive/types.go b/drivers/aliyundrive/types.go index 043d5e921..5fe9a5583 100644 --- a/drivers/aliyundrive/types.go +++ b/drivers/aliyundrive/types.go @@ -54,3 +54,12 @@ type UploadResp struct { RapidUpload bool `json:"rapid_upload"` } + +type SharedTokenResp struct { + ShareToken string `json:"share_token"` +} + +type ShareInfoResp struct { + FileId string `json:"file_id"` + ParentFileId string `json:"parent_file_id"` +} diff --git a/drivers/aliyundrive/util.go b/drivers/aliyundrive/util.go index 553411047..f5c616b8a 100644 --- a/drivers/aliyundrive/util.go +++ b/drivers/aliyundrive/util.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/op" @@ -202,3 +203,48 @@ func (d *AliDrive) batch(srcId, dstId string, url string) error { } return errors.New(string(res)) } + +func (d *AliDrive) getShareToken(shareid, share_pwd string) (string, error) { + resp := SharedTokenResp{} + _, err, _ := d.request("https://api.aliyundrive.com/v2/share_link/get_share_token", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "share_id": shareid, + "share_pwd": share_pwd, + }) + }, resp) + if err != nil { + return "", err + } + return resp.ShareToken, nil +} + +func (d *AliDrive) getFileId(shareid, token string) (string, string) { + resp := ShareInfoResp{} + _, err, _ := d.request("https://api.aliyundrive.com/adrive/v2/file/list_by_share", http.MethodPost, func(req *resty.Request) { + req.SetHeader("x-share-token", token) + req.SetBody(base.Json{ + "share_id": shareid, + "order_by": "name", + "order_direction": "DESC", + "limit": 20, + "parent_file_id": "root", + "image_thumbnail_process": "image/resize,w_256/format,jpeg", + "image_url_process": "image/resize,w_1920/format,jpeg/interlace,1", + "video_thumbnail_process": "video/snapshot,t_1000,f_jpg,ar_auto,w_256", + }) + }, &resp) + if err != nil { + return "", "" + } + return resp.FileId, resp.ParentFileId +} + +func (d *AliDrive) extractShareId(shareUrl string) string { + // https://www.alipan.com/s/XVDFP7mpuZK + re := regexp.MustCompile(`https?://(?:[a-zA-Z0-9-]+\.)*alipan\.com(?:\:\d+)?/s/([a-zA-Z0-9_-]+)`) + matches := re.FindStringSubmatch(shareUrl) + if len(matches) >= 2 { + return matches[1] + } + return "" +} diff --git a/drivers/quark_uc/driver.go b/drivers/quark_uc/driver.go index 71f3ffca3..7f2a83f58 100644 --- a/drivers/quark_uc/driver.go +++ b/drivers/quark_uc/driver.go @@ -3,6 +3,7 @@ package quark import ( "context" "encoding/hex" + "errors" "hash" "io" "net/http" @@ -231,4 +232,19 @@ func (d *QuarkOrUC) GetDetails(ctx context.Context) (*model.StorageDetails, erro }, nil } +func (d *QuarkOrUC) Transfer(ctx context.Context, dst model.Obj, shareURL, validCode string) error { + pwd_id := d.getPwdID(shareURL) + if pwd_id == "" { + return errors.New("invalid share url") + } + shareToken, err := d.getShareToken(pwd_id, validCode) + if err != nil { + return err + } + if shareToken == "" { + return errors.New("getting share token error") + } + return d.transfer(dst, shareToken, pwd_id) +} + var _ driver.Driver = (*QuarkOrUC)(nil) diff --git a/drivers/quark_uc/types.go b/drivers/quark_uc/types.go index cea2b8830..a973c5282 100644 --- a/drivers/quark_uc/types.go +++ b/drivers/quark_uc/types.go @@ -282,3 +282,33 @@ type MemberResp struct { ServerCurTime uint64 `json:"server_cur_time"` } `json:"metadata"` } + +type ShareTokenResp struct { + Resp + Data struct { + ShareToken string `json:"stoken"` + } +} + +type ShareInfoResp struct { + Resp + Data struct { + Share struct { + ShareID string `json:"share_id"` + } `json:"share"` + } `json:"data"` +} + +type SaveShareResp struct { + Resp + Data struct { + TaskID string `json:"task_id"` + } +} + +type TaskCheckResp struct { + Resp + Data struct { + Status int `json:"status"` + } `json:"data"` +} diff --git a/drivers/quark_uc/util.go b/drivers/quark_uc/util.go index 3c49f1f3f..54fad6d4a 100644 --- a/drivers/quark_uc/util.go +++ b/drivers/quark_uc/util.go @@ -17,6 +17,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/pkg/cookie" "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/pkg/utils/random" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" ) @@ -356,3 +357,86 @@ func (d *QuarkOrUC) memberInfo(ctx context.Context) (*MemberResp, error) { } return &resp, nil } + +func (d *QuarkOrUC) getPwdID(shareURL string) string { + // 从分享链接中提取分享码,如https://pan.quark.cn/s/75079b0bc0ee#/list/share + parts := strings.Split(shareURL, "/s/") + if len(parts) < 2 { + return "" + } + codeParts := strings.SplitN(parts[1], "/", 2) + if len(codeParts) == 0 { + return "" + } + pwd_id := strings.Split(codeParts[0], "#")[0] + return pwd_id +} + +func (d *QuarkOrUC) getShareToken(pwd_id, passcode string) (string, error) { + // + resp := ShareTokenResp{} + _, err := d.request("/share/sharepage/token", http.MethodPost, func(req *resty.Request) { + req.SetQueryParam("__t", strconv.FormatInt(time.Now().UnixMilli(), 10)) + req.SetQueryParam("__dt", strconv.FormatInt(random.RangeInt64(100, 500), 10)) + req.SetBody(base.Json{ + "pwd_id": pwd_id, + "passcode": passcode, + "share_from": "", + "support_visit_limit_private_share": true, + }) + }, &resp) + if err != nil { + return "", err + } + if resp.Status == 200 && resp.Code == 0 { + return resp.Data.ShareToken, nil + } + return "", errors.New("unknown error") +} + +func (d *QuarkOrUC) transfer(dstDir model.Obj, shareToken, pwdId string) error { + resp := SaveShareResp{} + data := base.Json{ + "stoken": shareToken, + "pdir_fid": "0", + "to_pdir_fid": dstDir.GetID(), + "pwd_id": pwdId, + "scene": "link", + "pdir_save_all": true, + } + _, err := d.request("/share/sharepage/save", http.MethodPost, func(req *resty.Request) { + req.SetBody(data) + req.SetQueryParam("__t", strconv.FormatInt(time.Now().UnixMilli(), 10)) + req.SetQueryParam("__dt", strconv.FormatInt(random.RangeInt64(100, 500), 10)) + }, &resp) + if err != nil { + return err + } + if resp.Status == 200 && resp.Code == 0 && resp.Data.TaskID != "" { + retry_index := 0 + for { + check_resp := TaskCheckResp{} + _, err := d.request("/task", http.MethodGet, func(req *resty.Request) { + req.SetQueryParam("task_id", resp.Data.TaskID) + req.SetQueryParam("__t", strconv.FormatInt(time.Now().UnixMilli(), 10)) + req.SetQueryParam("__dt", strconv.FormatInt(random.RangeInt64(100, 500), 10)) + req.SetQueryParam("retry_index", strconv.Itoa(retry_index)) + req.SetQueryParam("uc_param_str", "") + }, &check_resp) + if err != nil { + return err + } + if check_resp.Status == 200 && check_resp.Code == 0 { + if check_resp.Data.Status == 2 { + return nil + } + } + retry_index++ + time.Sleep(time.Second * time.Duration(retry_index)) + if retry_index >= 10 { + return errors.New("transfer timeout") + } + } + } + return err +} diff --git a/go.sum b/go.sum index 029e5d514..97ac10348 100644 --- a/go.sum +++ b/go.sum @@ -398,8 +398,6 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg 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/halalcloud/golang-sdk-lite v0.0.0-20251105081800-78cbb6786c38 h1:lsK2GVgI2Ox0NkRpQnN09GBOH7jtsjFK5tcIgxXlLr0= github.com/halalcloud/golang-sdk-lite v0.0.0-20251105081800-78cbb6786c38/go.mod h1:8x1h4rm3s8xMcTyJrq848sQ6BJnKzl57mDY4CNshdPM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/internal/driver/driver.go b/internal/driver/driver.go index 373bb5653..d4c2ea952 100644 --- a/internal/driver/driver.go +++ b/internal/driver/driver.go @@ -110,6 +110,29 @@ type PutURL interface { PutURL(ctx context.Context, dstDir model.Obj, name, url string) error } +type Transfer interface { + // Transfer save the sharing file to dstDir + Transfer(ctx context.Context, dst model.Obj, shareURL, validCode string) error +} + +type TransferResult interface { + // Transfer save the sharing file to dstDir and return results + Transfer(ctx context.Context, dst model.Obj, shareURL, validCode string) ([]model.Obj, error) +} + +type MaybeCannotTransfer interface { + CanTransfer(path string) bool +} + +//type WriteResult interface { +// MkdirResult +// MoveResult +// RenameResult +// CopyResult +// PutResult +// Remove +//} + type MkdirResult interface { MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) } diff --git a/internal/fs/archive.go b/internal/fs/archive.go index 784ba587a..1ae1763c0 100644 --- a/internal/fs/archive.go +++ b/internal/fs/archive.go @@ -363,7 +363,7 @@ func archiveList(ctx context.Context, path string, args model.ArchiveListArgs) ( return op.ListArchive(ctx, storage, actualPath, args) } -func archiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) (task.TaskExtensionInfo, error) { +func archiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args model.ArchiveDecompressArgs) (task.TaskExtensionInfo, error) { srcStorage, srcObjActualPath, err := op.GetStorageAndActualPath(srcObjPath) if err != nil { return nil, errors.WithMessage(err, "failed get src storage") @@ -373,7 +373,7 @@ func archiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args return nil, errors.WithMessage(err, "failed get dst storage") } if srcStorage.GetStorage() == dstStorage.GetStorage() { - err = op.ArchiveDecompress(ctx, srcStorage, srcObjActualPath, dstDirActualPath, args, lazyCache...) + err = op.ArchiveDecompress(ctx, srcStorage, srcObjActualPath, dstDirActualPath, args) if !errors.Is(err, errs.NotImplement) { return nil, err } diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 3d459c6be..57cbe5238 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -140,8 +140,8 @@ func ArchiveList(ctx context.Context, path string, args model.ArchiveListArgs) ( return objs, err } -func ArchiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) (task.TaskExtensionInfo, error) { - t, err := archiveDecompress(ctx, srcObjPath, dstDirPath, args, lazyCache...) +func ArchiveDecompress(ctx context.Context, srcObjPath, dstDirPath string, args model.ArchiveDecompressArgs) (task.TaskExtensionInfo, error) { + t, err := archiveDecompress(ctx, srcObjPath, dstDirPath, args) if err != nil { log.Errorf("failed decompress [%s]%s: %+v", srcObjPath, args.InnerPath, err) } @@ -206,3 +206,12 @@ func GetDirectUploadInfo(ctx context.Context, tool, path, dstName string, fileSi } return info, err } + +func Transfer(ctx context.Context, dstPath, shareURL, validCode string) error { + err := transferShare(ctx, dstPath, shareURL, validCode) + if err != nil { + log.Errorf("failed transfer %s: %+v", dstPath, err) + return err + } + return nil +} diff --git a/internal/fs/other.go b/internal/fs/other.go index a23beb73b..6758bd860 100644 --- a/internal/fs/other.go +++ b/internal/fs/other.go @@ -2,6 +2,7 @@ package fs import ( "context" + "fmt" "github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/driver" @@ -39,6 +40,14 @@ func remove(ctx context.Context, path string) error { return op.Remove(ctx, storage, actualPath) } +func transferShare(ctx context.Context, dstPath, shareURL, validCode string) error { + storage, actualPath, err := op.GetStorageAndActualPath(dstPath) + if err != nil { + return fmt.Errorf("failed to load driver") + } + return op.TransferShare(ctx, storage, actualPath, shareURL, validCode) +} + func other(ctx context.Context, args model.FsOtherArgs) (interface{}, error) { storage, actualPath, err := op.GetStorageAndActualPath(args.Path) if err != nil { diff --git a/internal/op/archive.go b/internal/op/archive.go index d0a53a919..61298d854 100644 --- a/internal/op/archive.go +++ b/internal/op/archive.go @@ -21,7 +21,6 @@ import ( gocache "github.com/OpenListTeam/go-cache" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "golang.org/x/time/rate" ) var ( @@ -490,7 +489,7 @@ func InternalExtract(ctx context.Context, storage driver.Driver, path string, ar return &streamWithParent{rc: rc, parents: ss}, size, nil } -func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, args model.ArchiveDecompressArgs, lazyCache ...bool) error { +func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string, args model.ArchiveDecompressArgs) error { if storage.Config().CheckStatus && storage.GetStorage().Status != WORK { return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status) } @@ -500,10 +499,17 @@ func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstD if err != nil { return errors.WithMessage(err, "failed to get src object") } + err = MakeDir(ctx, storage, dstDirPath) + if err != nil { + return errors.WithMessagef(err, "failed to make dir [%s]", dstDirPath) + } dstDir, err := GetUnwrap(ctx, storage, dstDirPath) if err != nil { return errors.WithMessage(err, "failed to get dst dir") } + if model.ObjHasMask(dstDir, model.NoWrite) { + return errors.WithStack(errs.PermissionDenied) + } var newObjs []model.Obj switch s := storage.(type) { @@ -518,24 +524,26 @@ func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstD } } } - } else if !utils.IsBool(lazyCache...) { + } else { Cache.DeleteDirectory(storage, dstDirPath) } } case driver.ArchiveDecompress: err = s.ArchiveDecompress(ctx, srcObj, dstDir, args) - if err == nil && !utils.IsBool(lazyCache...) { + if err == nil { Cache.DeleteDirectory(storage, dstDirPath) } default: - return errs.NotImplement + return errors.WithStack(errs.NotImplement) } - if !utils.IsBool(lazyCache...) && err == nil && needHandleObjsUpdateHook() { + if err == nil && ctx.Value(conf.SkipHookKey) == nil && needHandleObjsUpdateHook() { onlyList := false targetPath := dstDirPath - if len(newObjs) == 1 && newObjs[0].IsDir() { + if newObjs != nil && len(newObjs) == 1 && newObjs[0].IsDir() { targetPath = stdpath.Join(dstDirPath, newObjs[0].GetName()) - } else if len(newObjs) == 1 && !newObjs[0].IsDir() { + } else if newObjs != nil && !utils.SliceMeet(newObjs, nil, func(item model.Obj, _ any) bool { + return item.IsDir() + }) { onlyList = true } else if args.PutIntoNewDir { targetPath = stdpath.Join(dstDirPath, strings.TrimSuffix(srcObj.GetName(), stdpath.Ext(srcObj.GetName()))) @@ -543,18 +551,11 @@ func ArchiveDecompress(ctx context.Context, storage driver.Driver, srcPath, dstD targetPath = stdpath.Join(dstDirPath, innerBase) dstObj, e := Get(ctx, storage, targetPath) onlyList = e != nil || !dstObj.IsDir() - } - if onlyList { - go List(context.Background(), storage, dstDirPath, model.ListArgs{Refresh: true}) - } else { - var limiter *rate.Limiter - if l, _ := GetSettingItemByKey(conf.HandleHookRateLimit); l != nil { - if f, e := strconv.ParseFloat(l.Value, 64); e == nil && f > .0 { - limiter = rate.NewLimiter(rate.Limit(f), 1) - } + if onlyList { + targetPath = dstDirPath } - go RecursivelyListStorage(context.Background(), storage, targetPath, limiter, nil) } + go objsUpdateHook(ctx, storage, targetPath, !onlyList) } return errors.WithStack(err) } diff --git a/internal/op/fs.go b/internal/op/fs.go index 5116bbef5..c53efcdd5 100644 --- a/internal/op/fs.go +++ b/internal/op/fs.go @@ -765,6 +765,79 @@ func PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url return errors.WithStack(err) } +func CanTransfer(storage driver.Driver, path string) bool { + _, ok := storage.(driver.Transfer) + _, okResult := storage.(driver.TransferResult) + if !ok && !okResult { + return false + } + if storage.Config().CheckStatus && storage.GetStorage().Status != WORK { + return false + } + if maybe, ok := storage.(driver.MaybeCannotTransfer); ok { + return maybe.CanTransfer(path) + } + return true +} + +func TransferShare(ctx context.Context, storage driver.Driver, dstDirPath, shareURL, validCode string) error { + if storage.Config().CheckStatus && storage.GetStorage().Status != WORK { + return errors.WithMessagef(errs.StorageNotInit, "storage status: %s", storage.GetStorage().Status) + } + err := MakeDir(ctx, storage, dstDirPath) + if err != nil { + return errors.WithMessagef(err, "failed to make dir [%s]", dstDirPath) + } + parentDir, err := GetUnwrap(ctx, storage, dstDirPath) + if err != nil { + return errors.WithMessagef(err, "failed to get dir [%s]", dstDirPath) + } + if model.ObjHasMask(parentDir, model.NoWrite) { + return errors.WithStack(errs.PermissionDenied) + } + var newObjs []model.Obj + switch s := storage.(type) { + case driver.TransferResult: + newObjs, err = s.Transfer(ctx, parentDir, shareURL, validCode) + if err == nil { + if len(newObjs) > 0 { + if !storage.Config().NoCache { + if cache, exist := Cache.dirCache.Get(Key(storage, dstDirPath)); exist { + for _, newObj := range newObjs { + cache.UpdateObject(newObj.GetName(), newObj) + } + } + } + } else { + Cache.DeleteDirectory(storage, dstDirPath) + } + } else { + err = errors.WithMessagef(err, "failed to transfer share to [%s]", dstDirPath) + } + case driver.Transfer: + if err = s.Transfer(ctx, parentDir, shareURL, validCode); err != nil { + err = errors.WithMessagef(err, "failed to transfer share to [%s]", dstDirPath) + } else { + Cache.DeleteDirectory(storage, dstDirPath) + } + default: + return errors.WithStack(errs.NotImplement) + } + if err == nil && ctx.Value(conf.SkipHookKey) == nil && needHandleObjsUpdateHook() { + onlyList := false + targetPath := dstDirPath + if newObjs != nil && len(newObjs) == 1 && newObjs[0].IsDir() { + targetPath = stdpath.Join(dstDirPath, newObjs[0].GetName()) + } else if newObjs != nil && !utils.SliceMeet(newObjs, nil, func(item model.Obj, _ any) bool { + return item.IsDir() + }) { + onlyList = true + } + go objsUpdateHook(ctx, storage, targetPath, !onlyList) + } + return err +} + func GetDirectUploadTools(storage driver.Driver) []string { du, ok := storage.(driver.DirectUploader) if !ok { diff --git a/server/handles/fsmanage.go b/server/handles/fsmanage.go index 8247fa8cb..b24bb37ba 100644 --- a/server/handles/fsmanage.go +++ b/server/handles/fsmanage.go @@ -424,3 +424,36 @@ func Link(c *gin.Context) { defer link.Close() common.SuccessResp(c, link) } + +type TransferReq struct { + SrcURL string `json:"url" binding:"required,url" form:"url"` + DstDir string `json:"dst_dir" binding:"required" form:"dst_dir"` + ValidCode string `json:"valid_code" form:"valid_code"` +} + +func FsTransfer(c *gin.Context) { + req := TransferReq{} + if err := c.ShouldBind(&req); err != nil { + common.ErrorResp(c, err, 400) + return + } + if req.DstDir == "" || req.SrcURL == "" { + common.ErrorStrResp(c, "dst_dir and url are required", 400) + return + } + user := c.Request.Context().Value(conf.UserKey).(*model.User) + if !user.CanWrite() { + common.ErrorResp(c, errs.PermissionDenied, 403) + return + } + reqDir, err := user.JoinPath(req.DstDir) + if err != nil { + common.ErrorResp(c, err, 403) + return + } + if err := fs.Transfer(c.Request.Context(), reqDir, req.SrcURL, req.ValidCode); err != nil { + common.ErrorResp(c, err, 500) + return + } + common.SuccessResp(c) +} diff --git a/server/handles/fsread.go b/server/handles/fsread.go index 65bc25248..e6cbccf48 100644 --- a/server/handles/fsread.go +++ b/server/handles/fsread.go @@ -54,6 +54,7 @@ type FsListResp struct { Write bool `json:"write"` Provider string `json:"provider"` DirectUploadTools []string `json:"direct_upload_tools,omitempty"` + CanTransfer bool `json:"can_transfer"` } func FsListSplit(c *gin.Context) { @@ -108,10 +109,12 @@ func FsList(c *gin.Context, req *ListReq, user *model.User) { } total, objs := pagination(objs, &req.PageReq) provider := "unknown" + canTransfer := false var directUploadTools []string if user.CanWrite() { - if storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}); err == nil { + if storage, actualPath, err := op.GetStorageAndActualPath(reqPath); err == nil { directUploadTools = op.GetDirectUploadTools(storage) + canTransfer = op.CanTransfer(storage, actualPath) } } common.SuccessResp(c, FsListResp{ @@ -122,6 +125,7 @@ func FsList(c *gin.Context, req *ListReq, user *model.User) { Write: user.CanWrite() || common.CanWrite(meta, reqPath), Provider: provider, DirectUploadTools: directUploadTools, + CanTransfer: canTransfer, }) } diff --git a/server/router.go b/server/router.go index 32e933de3..83a8b5432 100644 --- a/server/router.go +++ b/server/router.go @@ -206,6 +206,7 @@ func _fs(g *gin.RouterGroup) { g.POST("/recursive_move", handles.FsRecursiveMove) g.POST("/copy", handles.FsCopy) g.POST("/remove", handles.FsRemove) + g.POST("/transfer", handles.FsTransfer) g.POST("/remove_empty_directory", handles.FsRemoveEmptyDirectory) uploadLimiter := middlewares.UploadRateLimiter(stream.ClientUploadLimit) g.PUT("/put", middlewares.FsUp, uploadLimiter, handles.FsStream)