天翼支持家庭云
This commit is contained in:
1 parent
85922d5c2e
commit
8bcfa7df2f
169 files changed
+568
-102
No files matched your search
@@ -110,12 +110,18 @@ func (d *Driver) refreshSession(ctx context.Context, accessToken string) error {
|
|||||||
}
|
}
|
||||||
return domain.Errorf(domain.CodeAuthExpired, "%s", msg)
|
return domain.Errorf(domain.CodeAuthExpired, "%s", msg)
|
||||||
}
|
}
|
||||||
if out.SessionKey == "" || out.SessionSecret == "" {
|
if d.isFamily() {
|
||||||
return domain.Errorf(domain.CodeAuthExpired, "刷新会话成功但缺少 sessionKey/sessionSecret")
|
if out.FamilySessionKey == "" || out.FamilySessionSecret == "" {
|
||||||
|
return domain.Errorf(domain.CodeAuthExpired, "刷新会话成功但缺少家庭云会话信息")
|
||||||
|
}
|
||||||
|
} else if out.SessionKey == "" || out.SessionSecret == "" {
|
||||||
|
return domain.Errorf(domain.CodeAuthExpired, "刷新会话成功但缺少个人云会话信息")
|
||||||
}
|
}
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
d.sessionKey = out.SessionKey
|
d.sessionKey = out.SessionKey
|
||||||
d.sessionSecret = out.SessionSecret
|
d.sessionSecret = out.SessionSecret
|
||||||
|
d.familyKey = out.FamilySessionKey
|
||||||
|
d.familySecret = out.FamilySessionSecret
|
||||||
d.loginName = out.LoginName
|
d.loginName = out.LoginName
|
||||||
if out.RefreshToken != "" {
|
if out.RefreshToken != "" {
|
||||||
d.refreshToken = out.RefreshToken
|
d.refreshToken = out.RefreshToken
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import "litepan/pkg/jsonvalue"
|
|||||||
type flexString = jsonvalue.FlexibleString
|
type flexString = jsonvalue.FlexibleString
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
RefreshToken string `json:"refresh_token" label:"Token" type:"password" form:"required,full"`
|
AccessToken string `json:"access_token" label:"访问令牌 access_token" type:"password" form:"pair=auth"`
|
||||||
|
RefreshToken string `json:"refresh_token" label:"刷新令牌 refresh_token" type:"password" form:"required,pair=auth"`
|
||||||
|
SpaceType string `json:"space_type" label:"云空间" type:"select" options:"personal:个人云,family:家庭云" default:"personal" form:"pair=space"`
|
||||||
DeleteMode string `json:"delete_mode" label:"删除模式" type:"select" options:"trash:移动到回收站,delete:永久删除" default:"trash" form:"pair=opts1"`
|
DeleteMode string `json:"delete_mode" label:"删除模式" type:"select" options:"trash:移动到回收站,delete:永久删除" default:"trash" form:"pair=opts1"`
|
||||||
DownloadMode string `json:"download_mode" label:"下载模式" type:"select" options:"redirect:302重定向,proxy:本机代理" default:"redirect" form:"pair=opts1"`
|
DownloadMode string `json:"download_mode" label:"下载模式" type:"select" options:"redirect:302重定向,proxy:本机代理" default:"redirect" form:"pair=opts1"`
|
||||||
RootFolderID string `json:"root_folder_id" label:"根目录ID(默认 -11)" default:"-11" form:"pair=opts2"`
|
RootFolderID string `json:"root_folder_id" label:"根目录ID(默认空间根目录)" default:"-11" form:"pair=space" default_by:"space_type" defaults:"personal=-11,family=/"`
|
||||||
CacheTTL flexString `json:"cache_ttl" label:"缓存时间(分钟)" type:"number" default:"30" form:"pair=opts2"`
|
CacheTTL flexString `json:"cache_ttl" label:"缓存时间(分钟)" type:"number" default:"30" form:"pair=opts2"`
|
||||||
}
|
}
|
||||||
+61
-16
@@ -25,14 +25,19 @@ type Driver struct {
|
|||||||
refreshToken string
|
refreshToken string
|
||||||
sessionKey string
|
sessionKey string
|
||||||
sessionSecret string
|
sessionSecret string
|
||||||
|
familyKey string
|
||||||
|
familySecret string
|
||||||
|
familyID string
|
||||||
loginName string
|
loginName string
|
||||||
|
itemCache map[string]domain.FileItem
|
||||||
|
itemOrder []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "189_cloud",
|
Name: "189_cloud",
|
||||||
DisplayName: "天翼云盘",
|
DisplayName: "天翼云盘",
|
||||||
Description: "天翼云盘 PC 接口接入,支持扫码登录和文件管理",
|
Description: "天翼云盘 PC 接口接入,支持个人云、家庭云和文件管理",
|
||||||
CardTags: []string{"个人云", "扫码登录", "支持302", "支持秒传"},
|
CardTags: []string{"个人云", "家庭云", "扫码登录", "支持302", "支持秒传"},
|
||||||
SortOrder: 7,
|
SortOrder: 7,
|
||||||
AuthLabel: "扫码登录",
|
AuthLabel: "扫码登录",
|
||||||
CardColor: "#FEC52C",
|
CardColor: "#FEC52C",
|
||||||
@@ -77,16 +82,36 @@ func (d *Driver) Init(ctx context.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
|
if d.accessToken == "" {
|
||||||
|
d.accessToken = strings.TrimSpace(d.add.AccessToken)
|
||||||
|
}
|
||||||
if d.refreshToken == "" {
|
if d.refreshToken == "" {
|
||||||
d.refreshToken = strings.TrimSpace(d.add.RefreshToken)
|
d.refreshToken = strings.TrimSpace(d.add.RefreshToken)
|
||||||
}
|
}
|
||||||
|
access := d.accessToken
|
||||||
empty := d.refreshToken == ""
|
empty := d.refreshToken == ""
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
if empty {
|
if empty {
|
||||||
return domain.Errorf(domain.CodeValidation, "请先扫码登录获取天翼云盘授权")
|
return domain.Errorf(domain.CodeValidation, "请先扫码登录获取天翼云盘授权")
|
||||||
}
|
}
|
||||||
if !d.hasSession() {
|
if !d.hasSession() {
|
||||||
if _, err := d.doRefresh(ctx); err != nil {
|
if access != "" {
|
||||||
|
if err := d.refreshSession(ctx, access); err != nil {
|
||||||
|
if !isSessionExpired(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := d.doRefresh(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := d.doRefresh(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
if err := d.ensureFamilyID(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,21 +148,34 @@ func (d *Driver) ExplainConnectionError(technical string, saving bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) ListFiles(ctx context.Context, parentID string) ([]domain.FileItem, error) {
|
func (d *Driver) ListFiles(ctx context.Context, parentID string) ([]domain.FileItem, error) {
|
||||||
parent := d.normalizeParent(parentID)
|
parent := d.apiParentID(parentID)
|
||||||
|
endpoint := apiURL + "/listFiles.action"
|
||||||
|
baseParams := map[string]string{
|
||||||
|
"folderId": parent,
|
||||||
|
"fileType": "0",
|
||||||
|
"mediaAttr": "0",
|
||||||
|
"iconOption": "5",
|
||||||
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
endpoint = apiURL + "/family/file/listFiles.action"
|
||||||
|
baseParams["familyId"] = d.currentFamilyID()
|
||||||
|
baseParams["orderBy"] = "1"
|
||||||
|
baseParams["descending"] = "false"
|
||||||
|
} else {
|
||||||
|
baseParams["recursive"] = "0"
|
||||||
|
baseParams["orderBy"] = "filename"
|
||||||
|
baseParams["descending"] = "false"
|
||||||
|
}
|
||||||
var out []domain.FileItem
|
var out []domain.FileItem
|
||||||
for page := 1; page < 10000; page++ {
|
for page := 1; page < 10000; page++ {
|
||||||
var resp listResp
|
var resp listResp
|
||||||
err := d.apiRequest(ctx, http.MethodGet, apiURL+"/listFiles.action", map[string]string{
|
params := make(map[string]string, len(baseParams)+2)
|
||||||
"folderId": parent,
|
for key, value := range baseParams {
|
||||||
"fileType": "0",
|
params[key] = value
|
||||||
"mediaAttr": "0",
|
}
|
||||||
"iconOption": "5",
|
params["pageNum"] = itoa(page)
|
||||||
"pageNum": itoa(page),
|
params["pageSize"] = itoa(listPageSize)
|
||||||
"pageSize": itoa(listPageSize),
|
err := d.apiRequest(ctx, http.MethodGet, endpoint, params, &resp)
|
||||||
"recursive": "0",
|
|
||||||
"orderBy": "filename",
|
|
||||||
"descending": "false",
|
|
||||||
}, &resp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -158,12 +196,13 @@ func (d *Driver) ListFiles(ctx context.Context, parentID string) ([]domain.FileI
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
d.rememberItems(out)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) GetFileInfo(ctx context.Context, fileID string) (*domain.FileItem, error) {
|
func (d *Driver) GetFileInfo(ctx context.Context, fileID string) (*domain.FileItem, error) {
|
||||||
id := strings.TrimSpace(fileID)
|
id := strings.TrimSpace(fileID)
|
||||||
if id == "" || id == "/" || id == d.rootID() {
|
if id == "" || id == "/" || id == d.rootID() || (d.isFamily() && id == "-11") {
|
||||||
return &domain.FileItem{
|
return &domain.FileItem{
|
||||||
ID: d.rootID(),
|
ID: d.rootID(),
|
||||||
Name: "根目录",
|
Name: "根目录",
|
||||||
@@ -171,6 +210,12 @@ func (d *Driver) GetFileInfo(ctx context.Context, fileID string) (*domain.FileIt
|
|||||||
IDKind: domain.IDStable,
|
IDKind: domain.IDStable,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
if item, ok := d.cachedItem(id); ok {
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
return d.findFamilyItem(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
raw, err := d.fetchFileInfo(ctx, id)
|
raw, err := d.fetchFileInfo(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package cloud189
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"litepan/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const familyItemCacheLimit = 4096
|
||||||
|
|
||||||
|
func (d *Driver) isFamily() bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(d.add.SpaceType), "family")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) currentFamilyID() string {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
return d.familyID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) uploadSpaceKey() string {
|
||||||
|
if d.isFamily() {
|
||||||
|
return "family:" + d.currentFamilyID()
|
||||||
|
}
|
||||||
|
return "personal"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) ensureFamilyID(ctx context.Context) error {
|
||||||
|
if d.currentFamilyID() != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var resp familyListResp
|
||||||
|
if err := d.apiRequest(ctx, http.MethodGet, apiURL+"/family/manage/getFamilyList.action", nil, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(resp.FamilyInfoResp) == 0 {
|
||||||
|
return domain.Errorf(domain.CodeValidation, "当前天翼账号没有可用的家庭云")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
loginName := d.loginName
|
||||||
|
d.mu.Unlock()
|
||||||
|
selected := ""
|
||||||
|
for _, info := range resp.FamilyInfoResp {
|
||||||
|
remark := strings.TrimSpace(info.RemarkName)
|
||||||
|
if remark != "" && strings.Contains(loginName, remark) {
|
||||||
|
selected = info.FamilyID.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == "" {
|
||||||
|
for _, info := range resp.FamilyInfoResp {
|
||||||
|
if info.UseFlag != 0 {
|
||||||
|
selected = info.FamilyID.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == "" {
|
||||||
|
selected = resp.FamilyInfoResp[0].FamilyID.String()
|
||||||
|
}
|
||||||
|
if selected == "" {
|
||||||
|
return domain.Errorf(domain.CodeDriverError, "天翼云盘未返回家庭云ID")
|
||||||
|
}
|
||||||
|
d.mu.Lock()
|
||||||
|
d.familyID = selected
|
||||||
|
d.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) rememberItems(items []domain.FileItem) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if d.itemCache == nil {
|
||||||
|
d.itemCache = make(map[string]domain.FileItem)
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := d.itemCache[item.ID]; !exists {
|
||||||
|
d.itemOrder = append(d.itemOrder, item.ID)
|
||||||
|
}
|
||||||
|
d.itemCache[item.ID] = item
|
||||||
|
}
|
||||||
|
for len(d.itemOrder) > familyItemCacheLimit {
|
||||||
|
id := d.itemOrder[0]
|
||||||
|
d.itemOrder = d.itemOrder[1:]
|
||||||
|
delete(d.itemCache, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) cachedItem(id string) (domain.FileItem, bool) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
item, ok := d.itemCache[id]
|
||||||
|
return item, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) forgetItems(ids []string) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
for _, id := range ids {
|
||||||
|
delete(d.itemCache, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) findFamilyItem(ctx context.Context, fileID string) (*domain.FileItem, error) {
|
||||||
|
queue := []string{d.rootID()}
|
||||||
|
visited := map[string]struct{}{}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
parent := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
if _, ok := visited[parent]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[parent] = struct{}{}
|
||||||
|
items, err := d.ListFiles(ctx, parent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ID == fileID {
|
||||||
|
found := item
|
||||||
|
return &found, nil
|
||||||
|
}
|
||||||
|
if item.IsDir {
|
||||||
|
queue = append(queue, item.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, domain.Errf(domain.CodeNotFound)
|
||||||
|
}
|
||||||
@@ -20,12 +20,25 @@ type oauthRefreshResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sessionResp struct {
|
type sessionResp struct {
|
||||||
ResCode json.RawMessage `json:"res_code"`
|
ResCode json.RawMessage `json:"res_code"`
|
||||||
ResMessage string `json:"res_message"`
|
ResMessage string `json:"res_message"`
|
||||||
SessionKey string `json:"sessionKey"`
|
AccessToken string `json:"accessToken"`
|
||||||
SessionSecret string `json:"sessionSecret"`
|
SessionKey string `json:"sessionKey"`
|
||||||
LoginName string `json:"loginName"`
|
SessionSecret string `json:"sessionSecret"`
|
||||||
RefreshToken string `json:"refreshToken"`
|
FamilySessionKey string `json:"familySessionKey"`
|
||||||
|
FamilySessionSecret string `json:"familySessionSecret"`
|
||||||
|
LoginName string `json:"loginName"`
|
||||||
|
RefreshToken string `json:"refreshToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type familyListResp struct {
|
||||||
|
FamilyInfoResp []familyInfo `json:"familyInfoResp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type familyInfo struct {
|
||||||
|
FamilyID flexString `json:"familyId"`
|
||||||
|
RemarkName string `json:"remarkName"`
|
||||||
|
UseFlag int `json:"useFlag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type listResp struct {
|
type listResp struct {
|
||||||
|
|||||||
+64
-21
@@ -25,12 +25,21 @@ func (d *Driver) ResolveDownload(ctx context.Context, req driver.DownloadRequest
|
|||||||
if fileID == "" {
|
if fileID == "" {
|
||||||
return nil, domain.Errorf(domain.CodeValidation, "file_id 不能为空")
|
return nil, domain.Errorf(domain.CodeValidation, "file_id 不能为空")
|
||||||
}
|
}
|
||||||
var out map[string]any
|
endpoint := apiURL + "/getFileDownloadUrl.action"
|
||||||
if err := d.apiRequest(ctx, http.MethodGet, apiURL+"/getFileDownloadUrl.action", map[string]string{
|
params := map[string]string{
|
||||||
"fileId": fileID,
|
"fileId": fileID,
|
||||||
"dt": "3",
|
"dt": "3",
|
||||||
"flag": "1",
|
"flag": "1",
|
||||||
}, &out); err != nil {
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
endpoint = apiURL + "/family/file/getFileDownloadUrl.action"
|
||||||
|
params = map[string]string{
|
||||||
|
"fileId": fileID,
|
||||||
|
"familyId": d.currentFamilyID(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var out map[string]any
|
||||||
|
if err := d.apiRequest(ctx, http.MethodGet, endpoint, params, &out); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
downloadURL := firstString(anyString(out["fileDownloadUrl"]), anyString(out["downloadUrl"]), anyString(out["url"]))
|
downloadURL := firstString(anyString(out["fileDownloadUrl"]), anyString(out["downloadUrl"]), anyString(out["url"]))
|
||||||
@@ -41,9 +50,13 @@ func (d *Driver) ResolveDownload(ctx context.Context, req driver.DownloadRequest
|
|||||||
downloadURL = regexp.MustCompile(`(?i)^http://`).ReplaceAllString(downloadURL, "https://")
|
downloadURL = regexp.MustCompile(`(?i)^http://`).ReplaceAllString(downloadURL, "https://")
|
||||||
fileName := ""
|
fileName := ""
|
||||||
size := int64(0)
|
size := int64(0)
|
||||||
if raw, err := d.fetchFileInfo(ctx, fileID); err == nil && raw != nil {
|
if d.isFamily() {
|
||||||
fileName = strings.TrimSpace(raw.Name)
|
if item, err := d.GetFileInfo(ctx, fileID); err == nil && item != nil {
|
||||||
size = raw.size()
|
fileName = item.Name
|
||||||
|
size = item.Size
|
||||||
|
}
|
||||||
|
} else if raw, err := d.fetchFileInfo(ctx, fileID); err == nil && raw != nil {
|
||||||
|
fileName, size = strings.TrimSpace(raw.Name), raw.size()
|
||||||
}
|
}
|
||||||
headers := http.Header{}
|
headers := http.Header{}
|
||||||
headers.Set("User-Agent", resolveUA(req.UA))
|
headers.Set("User-Agent", resolveUA(req.UA))
|
||||||
@@ -82,12 +95,22 @@ func (d *Driver) CreateFolder(ctx context.Context, parentID, name string) (*doma
|
|||||||
if err := uploadutil.ValidateFileName(folderName); err != nil {
|
if err := uploadutil.ValidateFileName(folderName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var entry fileEntry
|
endpoint := apiURL + "/createFolder.action"
|
||||||
if err := d.apiRequest(ctx, http.MethodPost, apiURL+"/createFolder.action", map[string]string{
|
params := map[string]string{
|
||||||
"parentFolderId": d.normalizeParent(parentID),
|
"parentFolderId": d.apiParentID(parentID),
|
||||||
"folderName": folderName,
|
"folderName": folderName,
|
||||||
"relativePath": "",
|
"relativePath": "",
|
||||||
}, &entry); err != nil {
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
endpoint = apiURL + "/family/file/createFolder.action"
|
||||||
|
params = map[string]string{
|
||||||
|
"familyId": d.currentFamilyID(),
|
||||||
|
"parentId": d.apiParentID(parentID),
|
||||||
|
"folderName": folderName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var entry fileEntry
|
||||||
|
if err := d.apiRequest(ctx, http.MethodPost, endpoint, params, &entry); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entry.isDir = true
|
entry.isDir = true
|
||||||
@@ -98,6 +121,7 @@ func (d *Driver) CreateFolder(ctx context.Context, parentID, name string) (*doma
|
|||||||
if item.Name == "" {
|
if item.Name == "" {
|
||||||
item.Name = folderName
|
item.Name = folderName
|
||||||
}
|
}
|
||||||
|
d.rememberItems([]domain.FileItem{item})
|
||||||
return &item, nil
|
return &item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,16 +144,30 @@ func (d *Driver) RenameFile(ctx context.Context, fileID, newName string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if item.IsDir {
|
method := http.MethodPost
|
||||||
return d.apiRequest(ctx, http.MethodPost, apiURL+"/renameFolder.action", map[string]string{
|
prefix := apiURL
|
||||||
"folderId": id,
|
base := map[string]string{}
|
||||||
"destFolderName": name,
|
if d.isFamily() {
|
||||||
}, nil)
|
method = http.MethodGet
|
||||||
|
prefix = apiURL + "/family/file"
|
||||||
|
base["familyId"] = d.currentFamilyID()
|
||||||
}
|
}
|
||||||
return d.apiRequest(ctx, http.MethodPost, apiURL+"/renameFile.action", map[string]string{
|
if item.IsDir {
|
||||||
"fileId": id,
|
base["folderId"] = id
|
||||||
"destFileName": name,
|
base["destFolderName"] = name
|
||||||
}, nil)
|
if err := d.apiRequest(ctx, method, prefix+"/renameFolder.action", base, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
base["fileId"] = id
|
||||||
|
base["destFileName"] = name
|
||||||
|
if err := d.apiRequest(ctx, method, prefix+"/renameFile.action", base, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.Name = name
|
||||||
|
d.rememberItems([]domain.FileItem{*item})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) DeleteFiles(ctx context.Context, fileIDs []string) error {
|
func (d *Driver) DeleteFiles(ctx context.Context, fileIDs []string) error {
|
||||||
@@ -159,8 +197,10 @@ func (d *Driver) DeleteFiles(ctx context.Context, fileIDs []string) error {
|
|||||||
if err := d.waitBatchTask(ctx, "CLEAR_RECYCLE", clearID, time.Second, 40*time.Second); err != nil {
|
if err := d.waitBatchTask(ctx, "CLEAR_RECYCLE", clearID, time.Second, 40*time.Second); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
d.forgetItems(ids)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
d.forgetItems(ids)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +224,7 @@ func (d *Driver) transferFiles(ctx context.Context, taskType string, fileIDs []s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
taskID, err := d.createBatchTask(ctx, taskType, taskInfos, d.normalizeParent(targetParentID), nil)
|
taskID, err := d.createBatchTask(ctx, taskType, taskInfos, d.apiParentID(targetParentID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -250,6 +290,9 @@ func (d *Driver) createBatchTask(ctx context.Context, taskType string, taskInfos
|
|||||||
form.Set("type", taskType)
|
form.Set("type", taskType)
|
||||||
form.Set("taskInfos", string(raw))
|
form.Set("taskInfos", string(raw))
|
||||||
form.Set("targetFolderId", targetFolderID)
|
form.Set("targetFolderId", targetFolderID)
|
||||||
|
if d.isFamily() {
|
||||||
|
form.Set("familyId", d.currentFamilyID())
|
||||||
|
}
|
||||||
for k, v := range extra {
|
for k, v := range extra {
|
||||||
form.Set(k, v)
|
form.Set(k, v)
|
||||||
}
|
}
|
||||||
@@ -271,7 +314,7 @@ func (d *Driver) waitBatchTask(ctx context.Context, taskType, taskID string, int
|
|||||||
form.Set("type", taskType)
|
form.Set("type", taskType)
|
||||||
form.Set("taskId", taskID)
|
form.Set("taskId", taskID)
|
||||||
var resp map[string]any
|
var resp map[string]any
|
||||||
if err := d.formRequest(ctx, http.MethodPost, apiURL+"/batch/checkBatchTask.action", form, &resp); err != nil {
|
if err := d.formRequestFor(ctx, http.MethodPost, apiURL+"/batch/checkBatchTask.action", form, &resp, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
status := anyInt(resp["taskStatus"])
|
status := anyInt(resp["taskStatus"])
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ func finalizeQRLogin(ctx context.Context, client *http.Client, redirectURL strin
|
|||||||
return &driver.QRPollResult{
|
return &driver.QRPollResult{
|
||||||
Status: driver.QRSuccess,
|
Status: driver.QRSuccess,
|
||||||
Credentials: domain.AuthCredentials{
|
Credentials: domain.AuthCredentials{
|
||||||
|
AccessToken: out.AccessToken,
|
||||||
RefreshToken: out.RefreshToken,
|
RefreshToken: out.RefreshToken,
|
||||||
TokenExpires: time.Now().Add(7 * 24 * time.Hour),
|
TokenExpires: time.Now().Add(7 * 24 * time.Hour),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,29 +47,66 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (d *Driver) rootID() string {
|
func (d *Driver) rootID() string {
|
||||||
if id := strings.TrimSpace(d.add.RootFolderID); id != "" && id != "/" && id != "0" {
|
id := strings.TrimSpace(d.add.RootFolderID)
|
||||||
|
if !d.isRootAlias(id) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
return "-11"
|
return "-11"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) normalizeParent(parentID string) string {
|
func (d *Driver) normalizeParent(parentID string) string {
|
||||||
p := strings.TrimSpace(parentID)
|
p := strings.TrimSpace(parentID)
|
||||||
if p == "" || p == "/" || p == "0" || p == "root" {
|
if d.isRootAlias(p) {
|
||||||
return d.rootID()
|
return d.rootID()
|
||||||
}
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Driver) isRootAlias(id string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(id)) {
|
||||||
|
case "", "/", "0", "root":
|
||||||
|
return true
|
||||||
|
case "-11", "home":
|
||||||
|
return d.isFamily()
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) apiParentID(parentID string) string {
|
||||||
|
parent := d.normalizeParent(parentID)
|
||||||
|
if d.isFamily() && d.isRootAlias(parent) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Driver) hasSession() bool {
|
func (d *Driver) hasSession() bool {
|
||||||
|
return d.hasSessionFor(d.isFamily())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) hasSessionFor(family bool) bool {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
|
if family {
|
||||||
|
return d.familyKey != "" && d.familySecret != ""
|
||||||
|
}
|
||||||
return d.sessionKey != "" && d.sessionSecret != ""
|
return d.sessionKey != "" && d.sessionSecret != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) currentSession() (string, string) {
|
func (d *Driver) currentSession() (string, string) {
|
||||||
|
return d.currentSessionFor(d.isFamily())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) currentSessionFor(family bool) (string, string) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
|
if family {
|
||||||
|
return d.familyKey, d.familySecret
|
||||||
|
}
|
||||||
return d.sessionKey, d.sessionSecret
|
return d.sessionKey, d.sessionSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,16 +152,20 @@ func (d *Driver) apiRequest(ctx context.Context, method, rawURL string, params m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) formRequest(ctx context.Context, method, rawURL string, form url.Values, out any) error {
|
func (d *Driver) formRequest(ctx context.Context, method, rawURL string, form url.Values, out any) error {
|
||||||
if !d.hasSession() {
|
return d.formRequestFor(ctx, method, rawURL, form, out, d.isFamily())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) formRequestFor(ctx context.Context, method, rawURL string, form url.Values, out any, family bool) error {
|
||||||
|
if !d.hasSessionFor(family) {
|
||||||
if _, err := d.doRefresh(ctx); err != nil {
|
if _, err := d.doRefresh(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := d.signedForm(ctx, method, rawURL, form, out); isSessionExpired(err) {
|
if err := d.signedFormFor(ctx, method, rawURL, form, out, family); isSessionExpired(err) {
|
||||||
if _, rerr := d.doRefresh(ctx); rerr != nil {
|
if _, rerr := d.doRefresh(ctx); rerr != nil {
|
||||||
return rerr
|
return rerr
|
||||||
}
|
}
|
||||||
return d.signedForm(ctx, method, rawURL, form, out)
|
return d.signedFormFor(ctx, method, rawURL, form, out, family)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -134,9 +175,7 @@ func (d *Driver) formRequest(ctx context.Context, method, rawURL string, form ur
|
|||||||
func (d *Driver) signedJSON(ctx context.Context, method, rawURL string, params map[string]string, out any) error {
|
func (d *Driver) signedJSON(ctx context.Context, method, rawURL string, params map[string]string, out any) error {
|
||||||
query := clientSuffix()
|
query := clientSuffix()
|
||||||
for k, v := range params {
|
for k, v := range params {
|
||||||
if strings.TrimSpace(v) != "" {
|
query.Set(k, v)
|
||||||
query.Set(k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
headers, err := d.signatureHeaders(method, rawURL, "")
|
headers, err := d.signatureHeaders(method, rawURL, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -146,8 +185,12 @@ func (d *Driver) signedJSON(ctx context.Context, method, rawURL string, params m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) signedForm(ctx context.Context, method, rawURL string, form url.Values, out any) error {
|
func (d *Driver) signedForm(ctx context.Context, method, rawURL string, form url.Values, out any) error {
|
||||||
|
return d.signedFormFor(ctx, method, rawURL, form, out, d.isFamily())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) signedFormFor(ctx context.Context, method, rawURL string, form url.Values, out any, family bool) error {
|
||||||
query := clientSuffix()
|
query := clientSuffix()
|
||||||
headers, err := d.signatureHeaders(method, rawURL, "")
|
headers, err := d.signatureHeadersFor(method, rawURL, "", family)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -168,6 +211,9 @@ func (d *Driver) rawJSON(ctx context.Context, method, rawURL string, query url.V
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Wrap(domain.CodeDriverError, err)
|
return domain.Wrap(domain.CodeDriverError, err)
|
||||||
}
|
}
|
||||||
|
if is189AuthExpiredPayload(data) {
|
||||||
|
return domain.Errorf(domain.CodeAuthExpired, "天翼云盘认证会话已失效")
|
||||||
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return domain.Errorf(domain.CodeDriverError, "天翼云盘 API HTTP %d: %s", resp.StatusCode, httpx.Truncate(data, 300))
|
return domain.Errorf(domain.CodeDriverError, "天翼云盘 API HTTP %d: %s", resp.StatusCode, httpx.Truncate(data, 300))
|
||||||
}
|
}
|
||||||
@@ -193,6 +239,9 @@ func (d *Driver) rawForm(ctx context.Context, method, rawURL string, query url.V
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Wrap(domain.CodeDriverError, err)
|
return domain.Wrap(domain.CodeDriverError, err)
|
||||||
}
|
}
|
||||||
|
if is189AuthExpiredPayload(data) {
|
||||||
|
return domain.Errorf(domain.CodeAuthExpired, "天翼云盘认证会话已失效")
|
||||||
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return domain.Errorf(domain.CodeDriverError, "天翼云盘 API HTTP %d: %s", resp.StatusCode, httpx.Truncate(data, 300))
|
return domain.Errorf(domain.CodeDriverError, "天翼云盘 API HTTP %d: %s", resp.StatusCode, httpx.Truncate(data, 300))
|
||||||
}
|
}
|
||||||
@@ -200,7 +249,7 @@ func (d *Driver) rawForm(ctx context.Context, method, rawURL string, query url.V
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parse189Response(data []byte, out any) error {
|
func parse189Response(data []byte, out any) error {
|
||||||
if bytes.Contains(data, []byte("InvalidSessionKey")) || bytes.Contains(data, []byte("userSessionBO is null")) {
|
if is189AuthExpiredPayload(data) {
|
||||||
return domain.Errorf(domain.CodeAuthExpired, "天翼云盘会话已失效")
|
return domain.Errorf(domain.CodeAuthExpired, "天翼云盘会话已失效")
|
||||||
}
|
}
|
||||||
trimmed := bytes.TrimSpace(data)
|
trimmed := bytes.TrimSpace(data)
|
||||||
@@ -255,6 +304,14 @@ func parse189XMLResponse(data []byte, out any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func is189AuthExpiredPayload(data []byte) bool {
|
||||||
|
lower := bytes.ToLower(data)
|
||||||
|
return bytes.Contains(lower, []byte("invalidsessionkey")) ||
|
||||||
|
bytes.Contains(lower, []byte("usersessionbo is null")) ||
|
||||||
|
bytes.Contains(lower, []byte("userinvalidopentoken")) ||
|
||||||
|
bytes.Contains(lower, []byte("unifyaccountinfo is null"))
|
||||||
|
}
|
||||||
|
|
||||||
func responseMessage(env map[string]json.RawMessage, keys ...string) string {
|
func responseMessage(env map[string]json.RawMessage, keys ...string) string {
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
var s string
|
var s string
|
||||||
@@ -288,11 +345,19 @@ func isSessionExpired(err error) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
return strings.Contains(msg, "InvalidSessionKey") || strings.Contains(msg, "userSessionBO is null")
|
lower := strings.ToLower(msg)
|
||||||
|
return strings.Contains(lower, "invalidsessionkey") ||
|
||||||
|
strings.Contains(lower, "usersessionbo is null") ||
|
||||||
|
strings.Contains(lower, "userinvalidopentoken") ||
|
||||||
|
strings.Contains(lower, "unifyaccountinfo is null")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) signatureHeaders(method, rawURL, params string) (map[string]string, error) {
|
func (d *Driver) signatureHeaders(method, rawURL, params string) (map[string]string, error) {
|
||||||
sessionKey, sessionSecret := d.currentSession()
|
return d.signatureHeadersFor(method, rawURL, params, d.isFamily())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) signatureHeadersFor(method, rawURL, params string, family bool) (map[string]string, error) {
|
||||||
|
sessionKey, sessionSecret := d.currentSessionFor(family)
|
||||||
if sessionKey == "" || sessionSecret == "" {
|
if sessionKey == "" || sessionSecret == "" {
|
||||||
return nil, domain.Errorf(domain.CodeAuthExpired, "天翼云盘会话未初始化")
|
return nil, domain.Errorf(domain.CodeAuthExpired, "天翼云盘会话未初始化")
|
||||||
}
|
}
|
||||||
|
|||||||
+149
-28
@@ -7,8 +7,10 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,6 +18,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"litepan/internal/domain"
|
"litepan/internal/domain"
|
||||||
"litepan/internal/driver"
|
"litepan/internal/driver"
|
||||||
@@ -30,6 +33,7 @@ type uploadHashInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cloud189ResumeCtx struct {
|
type cloud189ResumeCtx struct {
|
||||||
|
space string
|
||||||
parentID string
|
parentID string
|
||||||
requestedName string
|
requestedName string
|
||||||
targetName string
|
targetName string
|
||||||
@@ -58,8 +62,9 @@ func (d *Driver) UploadLocalFile(ctx context.Context, req driver.LocalUploadRequ
|
|||||||
fileSize := local.Size
|
fileSize := local.Size
|
||||||
partSize := uploadPartSize(fileSize)
|
partSize := uploadPartSize(fileSize)
|
||||||
policy := uploadutil.NormalizeConflictPolicy(req.ConflictPolicy)
|
policy := uploadutil.NormalizeConflictPolicy(req.ConflictPolicy)
|
||||||
|
space := d.uploadSpaceKey()
|
||||||
|
|
||||||
resume := normalize189ResumeState(req.ResumeState, parentID, requestedName, fileSize, partSize, uploadHashInfo{})
|
resume := normalize189ResumeState(req.ResumeState, space, parentID, requestedName, fileSize, partSize, uploadHashInfo{})
|
||||||
hadResumeCandidate := resume != nil
|
hadResumeCandidate := resume != nil
|
||||||
targetName := requestedName
|
targetName := requestedName
|
||||||
if resume != nil {
|
if resume != nil {
|
||||||
@@ -85,7 +90,7 @@ func (d *Driver) UploadLocalFile(ctx context.Context, req driver.LocalUploadRequ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resume = normalize189ResumeState(req.ResumeState, parentID, requestedName, fileSize, partSize, hashInfo)
|
resume = normalize189ResumeState(req.ResumeState, space, parentID, requestedName, fileSize, partSize, hashInfo)
|
||||||
if hadResumeCandidate && resume == nil {
|
if hadResumeCandidate && resume == nil {
|
||||||
var skipped bool
|
var skipped bool
|
||||||
targetName, skipped, err = d.prepareUploadName(ctx, parentID, requestedName, policy)
|
targetName, skipped, err = d.prepareUploadName(ctx, parentID, requestedName, policy)
|
||||||
@@ -110,11 +115,14 @@ func (d *Driver) UploadLocalFile(ctx context.Context, req driver.LocalUploadRequ
|
|||||||
} else {
|
} else {
|
||||||
uploadutil.NotifyProgress(req.OnProgress, 0, fileSize, "正在创建上传会话")
|
uploadutil.NotifyProgress(req.OnProgress, 0, fileSize, "正在创建上传会话")
|
||||||
initParams := map[string]string{
|
initParams := map[string]string{
|
||||||
"parentFolderId": parentID,
|
"parentFolderId": d.apiParentID(parentID),
|
||||||
"fileName": url.QueryEscape(targetName),
|
"fileName": url.QueryEscape(targetName),
|
||||||
"fileSize": fmt.Sprintf("%d", fileSize),
|
"fileSize": fmt.Sprintf("%d", fileSize),
|
||||||
"sliceSize": fmt.Sprintf("%d", partSize),
|
"sliceSize": fmt.Sprintf("%d", partSize),
|
||||||
}
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
initParams["familyId"] = d.currentFamilyID()
|
||||||
|
}
|
||||||
if singlePart || emptyFile {
|
if singlePart || emptyFile {
|
||||||
initParams["fileMd5"] = hashInfo.fileMD5
|
initParams["fileMd5"] = hashInfo.fileMD5
|
||||||
initParams["sliceMd5"] = hashInfo.sliceMD5
|
initParams["sliceMd5"] = hashInfo.sliceMD5
|
||||||
@@ -132,6 +140,7 @@ func (d *Driver) UploadLocalFile(ctx context.Context, req driver.LocalUploadRequ
|
|||||||
}
|
}
|
||||||
rapid = anyInt(initData["fileDataExists"]) == 1 || anyInt(initResp["fileDataExists"]) == 1
|
rapid = anyInt(initData["fileDataExists"]) == 1 || anyInt(initResp["fileDataExists"]) == 1
|
||||||
resume = &cloud189ResumeCtx{
|
resume = &cloud189ResumeCtx{
|
||||||
|
space: space,
|
||||||
parentID: parentID,
|
parentID: parentID,
|
||||||
requestedName: requestedName,
|
requestedName: requestedName,
|
||||||
targetName: targetName,
|
targetName: targetName,
|
||||||
@@ -153,10 +162,7 @@ func (d *Driver) UploadLocalFile(ctx context.Context, req driver.LocalUploadRequ
|
|||||||
if _, completed := resume.completedParts[partNumber]; completed {
|
if _, completed := resume.completedParts[partNumber]; completed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
urlsResp, err := d.uploadEncryptedRequest(ctx, "getMultiUploadUrls", map[string]string{
|
urlsResp, err := d.getMultiUploadURLs(ctx, uploadFileID, partInfo, partNumber, totalParts)
|
||||||
"uploadFileId": uploadFileID,
|
|
||||||
"partInfo": partInfo,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -214,8 +220,70 @@ func (d *Driver) UploadLocalFile(ctx context.Context, req driver.LocalUploadRequ
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalize189ResumeState(state map[string]any, parentID, requestedName string, fileSize, partSize int64, hashes uploadHashInfo) *cloud189ResumeCtx {
|
func (d *Driver) getMultiUploadURLs(ctx context.Context, uploadFileID, partInfo string, partNumber, totalParts int) (map[string]any, error) {
|
||||||
if len(state) == 0 || strings.TrimSpace(uploadutil.AnyString(state["parent_id"])) != parentID ||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
delay := time.Duration(attempt) * 300 * time.Millisecond
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
timer.Stop()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := d.uploadEncryptedRequest(ctx, "getMultiUploadUrls", map[string]string{
|
||||||
|
"uploadFileId": uploadFileID,
|
||||||
|
"partInfo": partInfo,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
if !retryableUploadURLFailure(ctx, err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, domain.Errorf(
|
||||||
|
domain.CodeDriverError,
|
||||||
|
"获取第 %d/%d 个分片上传地址失败(已自动重试 2 次):%s",
|
||||||
|
partNumber,
|
||||||
|
totalParts,
|
||||||
|
rootErrorMessage(lastErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryableUploadURLFailure(ctx context.Context, err error) bool {
|
||||||
|
if err == nil || ctx.Err() != nil || isSessionExpired(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var urlErr *url.Error
|
||||||
|
var netErr net.Error
|
||||||
|
return errors.As(err, &urlErr) ||
|
||||||
|
errors.As(err, &netErr) ||
|
||||||
|
errors.Is(err, io.EOF) ||
|
||||||
|
errors.Is(err, io.ErrUnexpectedEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootErrorMessage(err error) string {
|
||||||
|
for err != nil {
|
||||||
|
next := errors.Unwrap(err)
|
||||||
|
if next == nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
err = next
|
||||||
|
}
|
||||||
|
return "未知网络错误"
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalize189ResumeState(state map[string]any, space, parentID, requestedName string, fileSize, partSize int64, hashes uploadHashInfo) *cloud189ResumeCtx {
|
||||||
|
stateSpace := strings.TrimSpace(uploadutil.AnyString(state["space"]))
|
||||||
|
if stateSpace == "" {
|
||||||
|
stateSpace = "personal"
|
||||||
|
}
|
||||||
|
if len(state) == 0 || stateSpace != space ||
|
||||||
|
strings.TrimSpace(uploadutil.AnyString(state["parent_id"])) != parentID ||
|
||||||
strings.TrimSpace(uploadutil.AnyString(state["requested_name"])) != requestedName {
|
strings.TrimSpace(uploadutil.AnyString(state["requested_name"])) != requestedName {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -236,6 +304,7 @@ func normalize189ResumeState(state map[string]any, parentID, requestedName strin
|
|||||||
totalParts = int((fileSize + partSize - 1) / partSize)
|
totalParts = int((fileSize + partSize - 1) / partSize)
|
||||||
}
|
}
|
||||||
return &cloud189ResumeCtx{
|
return &cloud189ResumeCtx{
|
||||||
|
space: space,
|
||||||
parentID: parentID,
|
parentID: parentID,
|
||||||
requestedName: requestedName,
|
requestedName: requestedName,
|
||||||
targetName: targetName,
|
targetName: targetName,
|
||||||
@@ -259,6 +328,7 @@ func persist189ResumeState(onState driver.UploadStateCallback, resume *cloud189R
|
|||||||
progress = 99
|
progress = 99
|
||||||
}
|
}
|
||||||
onState(map[string]any{
|
onState(map[string]any{
|
||||||
|
"space": resume.space,
|
||||||
"parent_id": resume.parentID,
|
"parent_id": resume.parentID,
|
||||||
"requested_name": resume.requestedName,
|
"requested_name": resume.requestedName,
|
||||||
"target_name": resume.targetName,
|
"target_name": resume.targetName,
|
||||||
@@ -410,7 +480,11 @@ func (d *Driver) uploadEncryptedRequestOnce(ctx context.Context, endpoint string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rawURL := uploadURL + "/person/" + strings.TrimLeft(endpoint, "/")
|
space := "person"
|
||||||
|
if d.isFamily() {
|
||||||
|
space = "family"
|
||||||
|
}
|
||||||
|
rawURL := uploadURL + "/" + space + "/" + strings.TrimLeft(endpoint, "/")
|
||||||
query := clientSuffix()
|
query := clientSuffix()
|
||||||
query.Set("params", encrypted)
|
query.Set("params", encrypted)
|
||||||
headers, err := d.signatureHeaders(http.MethodGet, rawURL, encrypted)
|
headers, err := d.signatureHeaders(http.MethodGet, rawURL, encrypted)
|
||||||
@@ -582,6 +656,13 @@ func (d *Driver) ResolveTransferHash(ctx context.Context, item *domain.FileItem,
|
|||||||
if !allowStream || item == nil || strings.TrimSpace(item.ID) == "" {
|
if !allowStream || item == nil || strings.TrimSpace(item.ID) == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
if d.isFamily() {
|
||||||
|
info, err := d.GetFileInfo(ctx, item.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return driver.HashFromItem(info, "md5"), nil
|
||||||
|
}
|
||||||
info, err := d.fetchFileInfo(ctx, item.ID)
|
info, err := d.fetchFileInfo(ctx, item.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -608,13 +689,19 @@ func (d *Driver) RapidUploadByHash(ctx context.Context, req driver.RapidUploadRe
|
|||||||
operation = "3"
|
operation = "3"
|
||||||
}
|
}
|
||||||
var committed rapidCommitResp
|
var committed rapidCommitResp
|
||||||
if err := d.formRequest(ctx, http.MethodPost, commitURL, url.Values{
|
if d.isFamily() {
|
||||||
"opertype": {operation},
|
if err := d.commitFamilyRapidUpload(ctx, commitURL, uploadFileID, &committed); err != nil {
|
||||||
"resumePolicy": {"1"},
|
return nil, err
|
||||||
"uploadFileId": {uploadFileID},
|
}
|
||||||
"isLog": {"0"},
|
} else {
|
||||||
}, &committed); err != nil {
|
if err := d.formRequest(ctx, http.MethodPost, commitURL, url.Values{
|
||||||
return nil, err
|
"opertype": {operation},
|
||||||
|
"resumePolicy": {"1"},
|
||||||
|
"uploadFileId": {uploadFileID},
|
||||||
|
"isLog": {"0"},
|
||||||
|
}, &committed); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &driver.RapidUploadResult{
|
return &driver.RapidUploadResult{
|
||||||
Reuse: true,
|
Reuse: true,
|
||||||
@@ -659,21 +746,55 @@ func (d *Driver) createRapidUpload(ctx context.Context, req driver.RapidUploadRe
|
|||||||
|
|
||||||
parentID := d.normalizeParent(req.ParentID)
|
parentID := d.normalizeParent(req.ParentID)
|
||||||
var created rapidCreateResp
|
var created rapidCreateResp
|
||||||
if err := d.formRequest(ctx, http.MethodPost, apiURL+"/createUploadFile.action", url.Values{
|
if d.isFamily() {
|
||||||
"parentFolderId": {parentID},
|
if err := d.apiRequest(ctx, http.MethodPost, apiURL+"/family/file/createFamilyFile.action", map[string]string{
|
||||||
"fileName": {fileName},
|
"familyId": d.currentFamilyID(),
|
||||||
"size": {strconv.FormatInt(req.Size, 10)},
|
"parentId": d.apiParentID(parentID),
|
||||||
"md5": {hash},
|
"fileMd5": hash,
|
||||||
"opertype": {"3"},
|
"fileName": fileName,
|
||||||
"flag": {"1"},
|
"fileSize": strconv.FormatInt(req.Size, 10),
|
||||||
"resumePolicy": {"1"},
|
"resumePolicy": "1",
|
||||||
"isLog": {"0"},
|
}, &created); err != nil {
|
||||||
}, &created); err != nil {
|
return rapidCreateResp{}, "", err
|
||||||
return rapidCreateResp{}, "", err
|
}
|
||||||
|
} else {
|
||||||
|
if err := d.formRequest(ctx, http.MethodPost, apiURL+"/createUploadFile.action", url.Values{
|
||||||
|
"parentFolderId": {parentID},
|
||||||
|
"fileName": {fileName},
|
||||||
|
"size": {strconv.FormatInt(req.Size, 10)},
|
||||||
|
"md5": {hash},
|
||||||
|
"opertype": {"3"},
|
||||||
|
"flag": {"1"},
|
||||||
|
"resumePolicy": {"1"},
|
||||||
|
"isLog": {"0"},
|
||||||
|
}, &created); err != nil {
|
||||||
|
return rapidCreateResp{}, "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return created, parentID, nil
|
return created, parentID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Driver) commitFamilyRapidUpload(ctx context.Context, commitURL, uploadFileID string, out any) error {
|
||||||
|
commit := func() error {
|
||||||
|
headers, err := d.signatureHeadersFor(http.MethodPost, commitURL, "", true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
headers["ResumePolicy"] = "1"
|
||||||
|
headers["UploadFileId"] = uploadFileID
|
||||||
|
headers["FamilyId"] = d.currentFamilyID()
|
||||||
|
return d.rawForm(ctx, http.MethodPost, commitURL, clientSuffix(), url.Values{}, headers, out)
|
||||||
|
}
|
||||||
|
if err := commit(); isSessionExpired(err) {
|
||||||
|
if _, refreshErr := d.doRefresh(ctx); refreshErr != nil {
|
||||||
|
return refreshErr
|
||||||
|
}
|
||||||
|
return commit()
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalize189RapidProbeError(err error) error {
|
func normalize189RapidProbeError(err error) error {
|
||||||
message := err.Error()
|
message := err.Error()
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 169 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user