From 9f16512ef0053236a17618ae39e6af98c4487df8 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 4 Jul 2026 14:28:16 +0800 Subject: [PATCH 01/24] fix(drivers/139): update path handling for family Signed-off-by: MadDogOwner --- drivers/139/util.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/drivers/139/util.go b/drivers/139/util.go index 3ff123ba3..0d33a6832 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -339,15 +339,18 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) { }, "sortDirection": 1, }) + // 传入 catalogID 是文件夹的ID,而不是完整路径 + // 当传入catalogID为根目录时,不能使用 catalogID + if catalogID == d.RootFolderID { + delete(data, "catalogID") + } var resp QueryContentListResp _, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp) if err != nil { return nil, err } + // 返回的是完整的Path: root:///.../ path := resp.Data.Path - if catalogID == d.RootFolderID { - d.RootPath = path - } for _, catalog := range resp.Data.CloudCatalogList { f := model.Object{ ID: catalog.CatalogID, @@ -1326,7 +1329,7 @@ func (d *Yun139) getGroupRootByCloudID(cloudID string) (string, error) { } // getFamilyRootPath 查询 family 的上层 path(data.path) -// 返回值已去除前缀 "root:/"(或 "root:"),直接返回纯 ID 或 path 部分,便于持久化为 RootFolderID。 +// 返回值为完整Path func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { // 使用 v1.2 接口(代码日志中已有该请求),pageSize 取 1 足够获取 path 字段 pathname := "/orchestration/familyCloud-rebuild/content/v1.2/queryContentList" @@ -1355,21 +1358,14 @@ func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { if dataObj == nil { return "", fmt.Errorf("invalid family response data") } - // helper to strip "root:/" or "root:" prefix - stripRoot := func(s string) string { - s = strings.TrimSpace(s) - s = strings.TrimPrefix(s, "root:/") - s = strings.TrimPrefix(s, "root:") - return s - } if p, ok := dataObj["path"].(string); ok && p != "" { - return stripRoot(p), nil + return p, nil } // 回退:有时 path 在 cloudCatalogList.catalogList 中 if cl, ok := dataObj["cloudCatalogList"].([]interface{}); ok && len(cl) > 0 { if first, ok := cl[0].(map[string]interface{}); ok { if p, ok := first["path"].(string); ok && p != "" { - return stripRoot(p), nil + return p, nil } } } From 1c8e268962e82cb6ecacd129d193f16e83869adb Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 4 Jul 2026 14:26:09 +0800 Subject: [PATCH 02/24] perf(drivers/139): add retry go for family upload Signed-off-by: MadDogOwner --- drivers/139/driver.go | 98 ++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 6386a2f25..b3beb3a0b 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -19,6 +19,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/pkg/cron" "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils/random" + "github.com/avast/retry-go" log "github.com/sirupsen/logrus" ) @@ -858,56 +859,85 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr } size := stream.GetSize() + partSize := d.getPartSize(size) + // Progress p := driver.NewProgress(size, up) - partSize := d.getPartSize(size) + rateLimited := driver.NewLimitedUploadStream(ctx, stream) + + // StreamSectionReader for per-chunk buffering and retry + ss, err := streamPkg.NewStreamSectionReader(&streamPkg.FileStream{ + Ctx: ctx, + Reader: rateLimited, + Obj: &model.Object{Size: size}, + }, int(partSize), &up) + if err != nil { + return err + } + part := int64(1) if size > partSize { part = (size + partSize - 1) / partSize } - rateLimited := driver.NewLimitedUploadStream(ctx, stream) for i := int64(0); i < part; i++ { if utils.IsCanceled(ctx) { return ctx.Err() } - start := i * partSize byteSize := min(size-start, partSize) - limitReader := io.LimitReader(rateLimited, byteSize) - // Update Progress - r := io.TeeReader(limitReader, p) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, resp.Data.UploadResult.RedirectionURL, r) - if err != nil { - return err - } - req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) - req.Header.Set("contentSize", strconv.FormatInt(size, 10)) - req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1)) - req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID) - req.Header.Set("rangeType", "0") - req.ContentLength = byteSize - - res, err := base.HttpClient.Do(req) + var rd io.ReadSeeker + err = retry.Do( + func() error { + var getErr error + rd, getErr = ss.GetSectionReader(start, byteSize) + if getErr != nil { + return getErr + } + rd.Seek(0, io.SeekStart) + req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, resp.Data.UploadResult.RedirectionURL, + io.TeeReader(rd, p)) + if reqErr != nil { + return reqErr + } + req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) + req.Header.Set("contentSize", strconv.FormatInt(size, 10)) + req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1)) + req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID) + req.Header.Set("rangeType", "0") + req.ContentLength = byteSize + + res, doErr := base.HttpClient.Do(req) + if doErr != nil { + return doErr + } + defer res.Body.Close() + bodyBytes, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("error reading response body: %v", readErr) + } + if res.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(bodyBytes)) + } + var result InterLayerUploadResult + xmlErr := xml.Unmarshal(bodyBytes, &result) + if xmlErr != nil { + return fmt.Errorf("error parsing XML: %v", xmlErr) + } + if result.ResultCode != 0 { + return fmt.Errorf("upload failed with result code: %d, message: %s", result.ResultCode, result.Msg) + } + return nil + }, + retry.Context(ctx), + retry.Attempts(10), + retry.DelayType(retry.BackOffDelay), + retry.Delay(time.Second), + ) + ss.FreeSectionReader(rd) if err != nil { return err } - if res.StatusCode != http.StatusOK { - res.Body.Close() - return fmt.Errorf("unexpected status code: %d", res.StatusCode) - } - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("error reading response body: %v", err) - } - var result InterLayerUploadResult - err = xml.Unmarshal(bodyBytes, &result) - if err != nil { - return fmt.Errorf("error parsing XML: %v", err) - } - if result.ResultCode != 0 { - return fmt.Errorf("upload failed with result code: %d, message: %s", result.ResultCode, result.Msg) - } } return nil default: From 031a9ae9b3edcdd7b04393270e56688c5886c515 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 4 Jul 2026 14:43:28 +0800 Subject: [PATCH 03/24] feat(drivers/139): add FamilyCloudHost and GroupCloudHost handling Signed-off-by: MadDogOwner --- drivers/139/driver.go | 16 ++++++++++++++-- drivers/139/util.go | 19 +++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index b3beb3a0b..2e23bfbc5 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -30,6 +30,8 @@ type Yun139 struct { Account string ref *Yun139 PersonalCloudHost string + FamilyCloudHost string + GroupCloudHost string RootPath string } @@ -74,14 +76,24 @@ func (d *Yun139) Init(ctx context.Context) error { return err } for _, policyItem := range resp.Data.RoutePolicyList { - if policyItem.ModName == "personal" { + switch policyItem.ModName { + case "personal": d.PersonalCloudHost = policyItem.HttpsUrl - break + case "group": + d.GroupCloudHost = policyItem.HttpsUrl + case "family": + d.FamilyCloudHost = policyItem.HttpsUrl } } if len(d.PersonalCloudHost) == 0 { return fmt.Errorf("PersonalCloudHost is empty") } + if len(d.GroupCloudHost) == 0 { + return fmt.Errorf("GroupCloudHost is empty") + } + if len(d.FamilyCloudHost) == 0 { + return fmt.Errorf("FamilyCloudHost is empty") + } d.cron = cron.NewCron(time.Hour * 12) d.cron.Do(func() { diff --git a/drivers/139/util.go b/drivers/139/util.go index 0d33a6832..e3690e24e 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -684,6 +684,20 @@ func (d *Yun139) getPersonalCloudHost() string { return d.PersonalCloudHost } +func (d *Yun139) getFamilyCloudHost() string { + if d.ref != nil { + return d.ref.getFamilyCloudHost() + } + return d.FamilyCloudHost +} + +func (d *Yun139) getGroupCloudHost() string { + if d.ref != nil { + return d.ref.getGroupCloudHost() + } + return d.GroupCloudHost +} + 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 { @@ -884,7 +898,6 @@ func (d *Yun139) step2_get_single_token(sid string) (string, error) { } exchangePassidHeaders := map[string]string{ - "Host": "smsrebuild1.mail.10086.cn", "Cookie": rmkey, "Content-Type": "text/xml; charset=utf-8", "Accept-Encoding": "gzip", @@ -1159,7 +1172,6 @@ func (d *Yun139) step3_third_party_login(dycpwd string) (string, error) { "x-UserAgent": "android|23116PN5BC|android15|1.2.6|||1440x3200|10246600", "x-DeviceInfo": "4|127.0.0.1|5|1.2.6|Xiaomi|23116PN5BC||02-00-00-00-00-00|android 15|1440x3200|android|||", "Content-Type": "text/plain;charset=UTF-8", - "Host": "user-njs.yun.139.com", "Accept-Encoding": "gzip", "User-Agent": "okhttp/3.12.2", } @@ -1240,10 +1252,9 @@ func (d *Yun139) loginWithPassword() (string, error) { } func (d *Yun139) andAlbumRequest(pathname string, body interface{}, resp interface{}) ([]byte, error) { - url := "https://group.yun.139.com/hcy/family/adapter/andAlbum/openApi" + pathname + url := d.getFamilyCloudHost() + "/andAlbum/openApi" + pathname headers := map[string]string{ - "Host": "group.yun.139.com", "authorization": "Basic " + d.getAuthorization(), "x-svctype": "2", "hcy-cool-flag": "1", From fcb1904af71fa6fc966c49eb6a364f8e3477b9c2 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 4 Jul 2026 15:21:51 +0800 Subject: [PATCH 04/24] perf(drivers/139): add retry go for personalnew upload Signed-off-by: MadDogOwner --- drivers/139/driver.go | 10 ++++-- drivers/139/util.go | 74 ++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 2e23bfbc5..af0d527cc 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -703,10 +703,14 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr if resp.Data.PartInfos != nil { // Progress p := driver.NewProgress(size, up) - rateLimited := driver.NewLimitedUploadStream(ctx, stream) + + ss, err := streamPkg.NewStreamSectionReader(stream, int(partSize), &up) + if err != nil { + return err + } // 先上传前100个分片 - err = d.uploadPersonalParts(ctx, partInfos, resp.Data.PartInfos, rateLimited, p) + err = d.uploadPersonalParts(ctx, partInfos, resp.Data.PartInfos, ss, p) if err != nil { return err } @@ -730,7 +734,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr if err != nil { return err } - err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, rateLimited, p) + err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, ss, p) if err != nil { return err } diff --git a/drivers/139/util.go b/drivers/139/util.go index e3690e24e..ea417c25d 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -25,8 +25,10 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/op" + streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/pkg/utils/random" + "github.com/avast/retry-go" "github.com/go-resty/resty/v2" jsoniter "github.com/json-iterator/go" log "github.com/sirupsen/logrus" @@ -698,7 +700,7 @@ func (d *Yun139) getGroupCloudHost() string { return d.GroupCloudHost } -func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, uploadPartInfos []PersonalPartInfo, rateLimited *driver.RateLimitReader, p *driver.Progress) error { +func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, uploadPartInfos []PersonalPartInfo, ss streamPkg.StreamSectionReader, p *driver.Progress) error { // 确保数组以 PartNumber 从小到大排序 sort.Slice(uploadPartInfos, func(i, j int) bool { return uploadPartInfos[i].PartNumber < uploadPartInfos[j].PartNumber @@ -710,31 +712,53 @@ func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, return fmt.Errorf("invalid PartNumber %d: index out of bounds (partInfos length: %d)", uploadPartInfo.PartNumber, len(partInfos)) } partSize := partInfos[index].PartSize + offset := partInfos[index].ParallelHashCtx.PartOffset 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 - }() + + var rd io.ReadSeeker + err := retry.Do( + func() error { + var getErr error + rd, getErr = ss.GetSectionReader(offset, partSize) + if getErr != nil { + return getErr + } + _, seekErr := rd.Seek(0, io.SeekStart) + if seekErr != nil { + ss.FreeSectionReader(rd) + return seekErr + } + req, reqErr := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, io.TeeReader(rd, p)) + if reqErr != nil { + ss.FreeSectionReader(rd) + return reqErr + } + 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, doErr := base.HttpClient.Do(req) + if doErr != nil { + ss.FreeSectionReader(rd) + return doErr + } + defer res.Body.Close() + log.Debugf("[139] uploaded: %+v", res) + if res.StatusCode != http.StatusOK { + body, _ := io.ReadAll(res.Body) + ss.FreeSectionReader(rd) + return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(body)) + } + return nil + }, + retry.Context(ctx), + retry.Attempts(10), + retry.DelayType(retry.BackOffDelay), + retry.Delay(time.Second), + ) + ss.FreeSectionReader(rd) if err != nil { return err } From 51d7d59e8e8cc975ec8513d3a598840ba4c1e029 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Sat, 4 Jul 2026 15:24:55 +0800 Subject: [PATCH 05/24] fix(drivers/139): use new batchpartinfos for remaining parts Signed-off-by: MadDogOwner --- drivers/139/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index af0d527cc..cd6951071 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -734,7 +734,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr if err != nil { return err } - err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, ss, p) + err = d.uploadPersonalParts(ctx, batchPartInfos, moreresp.Data.PartInfos, ss, p) if err != nil { return err } From deef17157cbfb03551d4d910f11277c87ca850b5 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Mon, 6 Jul 2026 20:19:35 +0800 Subject: [PATCH 06/24] fix(driver/139): do not use path in id Signed-off-by: MadDogOwner --- drivers/139/driver.go | 3 ++- drivers/139/util.go | 33 +++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index cd6951071..655d007c5 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -123,6 +123,7 @@ func (d *Yun139) Init(ctx context.Context) error { case MetaFamily: if len(d.Addition.RootFolderID) == 0 { // Attempt to obtain data.path as the root via a query and persist it. + d.RootFolderID = stripRootPath(d.RootFolderID) if root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != "" { d.RootFolderID = root op.MustSaveDriverStorage(d) @@ -843,7 +844,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr }, } pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" - if d.isFamily() || d.Addition.Type == MetaGroup { + if d.isFamily() || d.isGroup() { uploadPath := path.Join(dstDir.GetPath(), dstDir.GetID()) // if dstDir is root folder if dstDir.GetID() == d.RootFolderID { diff --git a/drivers/139/util.go b/drivers/139/util.go index ea417c25d..61ee6df88 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -41,7 +41,11 @@ const ( // do others that not defined in Driver interface func (d *Yun139) isFamily() bool { - return d.Type == "family" + return d.Type == MetaFamily +} + +func (d *Yun139) isGroup() bool { + return d.Type == MetaGroup } func encodeURIComponent(str string) string { @@ -353,6 +357,10 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) { } // 返回的是完整的Path: root:///.../ path := resp.Data.Path + if catalogID == d.RootFolderID { + path = ensureRootPath(d.RootPath) + d.RootPath = path + } for _, catalog := range resp.Data.CloudCatalogList { f := model.Object{ ID: catalog.CatalogID, @@ -1363,8 +1371,25 @@ func (d *Yun139) getGroupRootByCloudID(cloudID string) (string, error) { return "", fmt.Errorf("no root found in group response") } +// helper to strip "root:/" or "root:" prefix +func stripRootPath(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "root:/") + s = strings.TrimPrefix(s, "root:") + return s +} + +// helper to ensure "root:/" prefix +func ensureRootPath(s string) string { + s = strings.TrimSpace(s) + if !strings.HasPrefix(s, "root:") { + s = "root:/" + s + } + return s +} + // getFamilyRootPath 查询 family 的上层 path(data.path) -// 返回值为完整Path +// 返回值已去除前缀 "root:/"(或 "root:"),直接返回纯 ID 或 path 部分,便于持久化为 RootFolderID。 func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { // 使用 v1.2 接口(代码日志中已有该请求),pageSize 取 1 足够获取 path 字段 pathname := "/orchestration/familyCloud-rebuild/content/v1.2/queryContentList" @@ -1394,13 +1419,13 @@ func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { return "", fmt.Errorf("invalid family response data") } if p, ok := dataObj["path"].(string); ok && p != "" { - return p, nil + return stripRootPath(p), nil } // 回退:有时 path 在 cloudCatalogList.catalogList 中 if cl, ok := dataObj["cloudCatalogList"].([]interface{}); ok && len(cl) > 0 { if first, ok := cl[0].(map[string]interface{}); ok { if p, ok := first["path"].(string); ok && p != "" { - return p, nil + return stripRootPath(p), nil } } } From b5d6c6b2322e797dfa700dc210aa1841b62d9715 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Mon, 6 Jul 2026 20:53:50 +0800 Subject: [PATCH 07/24] fix(driver/139): refactor RootPath handling Signed-off-by: MadDogOwner --- drivers/139/driver.go | 19 +++++++------------ drivers/139/util.go | 31 +++++++++++++++---------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 655d007c5..12efd90ae 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -32,7 +32,6 @@ type Yun139 struct { PersonalCloudHost string FamilyCloudHost string GroupCloudHost string - RootPath string } func (d *Yun139) Config() driver.Config { @@ -226,7 +225,7 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin "accountType": 1, }, "docLibName": dirName, - "path": path.Join(parentDir.GetPath(), parentDir.GetID()), + "path": d.dirPath(parentDir), } pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc" _, err = d.post(pathname, data, nil) @@ -239,7 +238,7 @@ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin }, "groupID": d.CloudID, "parentFileId": parentDir.GetID(), - "path": path.Join(parentDir.GetPath(), parentDir.GetID()), + "path": d.dirPath(parentDir), } pathname := "/orchestration/group-rebuild/catalog/v1.0/createGroupCatalog" _, err = d.post(pathname, data, nil) @@ -324,9 +323,9 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, var contentList []string var catalogList []string if srcObj.IsDir() { - catalogList = append(catalogList, path.Join(srcObj.GetPath(), srcObj.GetID())) + catalogList = append(catalogList, d.dirPath(srcObj)) } else { - contentList = append(contentList, path.Join(srcObj.GetPath(), srcObj.GetID())) + contentList = append(contentList, d.dirPath(srcObj)) } body := base.Json{ @@ -338,7 +337,7 @@ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, "contentList": contentList, "destCatalogID": dstDir.GetID(), "destGroupID": d.CloudID, - "destPath": path.Join(dstDir.GetPath(), dstDir.GetID()), + "destPath": d.dirPath(dstDir), "destType": 0, "srcGroupID": d.CloudID, "srcType": 0, @@ -439,7 +438,7 @@ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) e }, "docLibName": newName, "docLibraryID": srcObj.GetID(), - "path": path.Join(srcObj.GetPath(), srcObj.GetID()), + "path": d.dirPath(srcObj), } var resp ModifyCloudDocV2Resp _, err = d.andAlbumRequest(pathname, data, &resp) @@ -845,11 +844,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr } pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" if d.isFamily() || d.isGroup() { - uploadPath := path.Join(dstDir.GetPath(), dstDir.GetID()) - // if dstDir is root folder - if dstDir.GetID() == d.RootFolderID { - uploadPath = d.RootPath - } + uploadPath := d.dirPath(dstDir) data = d.newJson(base.Json{ "fileCount": 1, "manualRename": 2, diff --git a/drivers/139/util.go b/drivers/139/util.go index 61ee6df88..be1ebdd37 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -357,10 +357,6 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) { } // 返回的是完整的Path: root:///.../ path := resp.Data.Path - if catalogID == d.RootFolderID { - path = ensureRootPath(d.RootPath) - d.RootPath = path - } for _, catalog := range resp.Data.CloudCatalogList { f := model.Object{ ID: catalog.CatalogID, @@ -416,9 +412,6 @@ func (d *Yun139) groupGetFiles(catalogID string) ([]model.Obj, error) { return nil, err } path := resp.Data.GetGroupContentResult.ParentCatalogID - if catalogID == d.RootFolderID { - d.RootPath = path - } for _, catalog := range resp.Data.GetGroupContentResult.CatalogList { f := model.Object{ ID: catalog.CatalogID, @@ -1371,6 +1364,21 @@ func (d *Yun139) getGroupRootByCloudID(cloudID string) (string, error) { return "", fmt.Errorf("no root found in group response") } +// dirPath returns the full path for a directory object. +// For family root (Path="" from framework), needs "root:/"+id prefix. +// Non-root objects already have their API path in GetPath() from List responses. +func (d *Yun139) dirPath(dir model.Obj) string { + p := dir.GetPath() + id := dir.GetID() + if p == "" { + if d.isFamily() { + return "root:/" + id + } + return id + } + return path.Join(p, id) +} + // helper to strip "root:/" or "root:" prefix func stripRootPath(s string) string { s = strings.TrimSpace(s) @@ -1379,15 +1387,6 @@ func stripRootPath(s string) string { return s } -// helper to ensure "root:/" prefix -func ensureRootPath(s string) string { - s = strings.TrimSpace(s) - if !strings.HasPrefix(s, "root:") { - s = "root:/" + s - } - return s -} - // getFamilyRootPath 查询 family 的上层 path(data.path) // 返回值已去除前缀 "root:/"(或 "root:"),直接返回纯 ID 或 path 部分,便于持久化为 RootFolderID。 func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { From a7a193f74c270d4f6123c184f1d5d94274cd0a06 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Mon, 6 Jul 2026 20:59:13 +0800 Subject: [PATCH 08/24] fix(driver/139): remove root path stripping in Init method Signed-off-by: MadDogOwner --- drivers/139/driver.go | 1 - drivers/139/util.go | 19 +++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 12efd90ae..77a80d140 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -122,7 +122,6 @@ func (d *Yun139) Init(ctx context.Context) error { case MetaFamily: if len(d.Addition.RootFolderID) == 0 { // Attempt to obtain data.path as the root via a query and persist it. - d.RootFolderID = stripRootPath(d.RootFolderID) if root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != "" { d.RootFolderID = root op.MustSaveDriverStorage(d) diff --git a/drivers/139/util.go b/drivers/139/util.go index be1ebdd37..db11eaa32 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -1379,14 +1379,6 @@ func (d *Yun139) dirPath(dir model.Obj) string { return path.Join(p, id) } -// helper to strip "root:/" or "root:" prefix -func stripRootPath(s string) string { - s = strings.TrimSpace(s) - s = strings.TrimPrefix(s, "root:/") - s = strings.TrimPrefix(s, "root:") - return s -} - // getFamilyRootPath 查询 family 的上层 path(data.path) // 返回值已去除前缀 "root:/"(或 "root:"),直接返回纯 ID 或 path 部分,便于持久化为 RootFolderID。 func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { @@ -1417,14 +1409,21 @@ func (d *Yun139) getFamilyRootPath(cloudID string) (string, error) { if dataObj == nil { return "", fmt.Errorf("invalid family response data") } + // helper to strip "root:/" or "root:" prefix + stripRoot := func(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "root:/") + s = strings.TrimPrefix(s, "root:") + return s + } if p, ok := dataObj["path"].(string); ok && p != "" { - return stripRootPath(p), nil + return stripRoot(p), nil } // 回退:有时 path 在 cloudCatalogList.catalogList 中 if cl, ok := dataObj["cloudCatalogList"].([]interface{}); ok && len(cl) > 0 { if first, ok := cl[0].(map[string]interface{}); ok { if p, ok := first["path"].(string); ok && p != "" { - return stripRootPath(p), nil + return stripRoot(p), nil } } } From 0e7d4c1d31ead0feb7dbdd362c3655c71b1b955b Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Mon, 6 Jul 2026 21:54:40 +0800 Subject: [PATCH 09/24] chore(drivers/139): reduce upload retry attempts to 3 Signed-off-by: MadDogOwner --- drivers/139/driver.go | 2 +- drivers/139/util.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 77a80d140..68a2ea6a9 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -941,7 +941,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr return nil }, retry.Context(ctx), - retry.Attempts(10), + retry.Attempts(3), retry.DelayType(retry.BackOffDelay), retry.Delay(time.Second), ) diff --git a/drivers/139/util.go b/drivers/139/util.go index db11eaa32..58872bc13 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -755,7 +755,7 @@ func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, return nil }, retry.Context(ctx), - retry.Attempts(10), + retry.Attempts(3), retry.DelayType(retry.BackOffDelay), retry.Delay(time.Second), ) From 550e9c83e4ea0523eec8c2d38dca60bea9325cce Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Tue, 14 Jul 2026 14:25:32 +0800 Subject: [PATCH 10/24] fix(drivers/139): add rate limiting for MetaPersonalNew upload - Wrap stream with LimitedUploadStream before creating StreamSectionReader - Aligns with the same pattern used in the MetaPersonal/MetaGroup/MetaFamily path Co-authored-by: GitHub Copilot --- drivers/139/driver.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 68a2ea6a9..2f4adc1ca 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -703,7 +703,12 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr // Progress p := driver.NewProgress(size, up) - ss, err := streamPkg.NewStreamSectionReader(stream, int(partSize), &up) + rateLimited := driver.NewLimitedUploadStream(ctx, stream) + ss, err := streamPkg.NewStreamSectionReader(&streamPkg.FileStream{ + Ctx: ctx, + Reader: rateLimited, + Obj: &model.Object{Size: size}, + }, int(partSize), &up) if err != nil { return err } From 73c6fe3958cad4dd01acf3008159af543acbb5f0 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Tue, 14 Jul 2026 14:29:59 +0800 Subject: [PATCH 11/24] fix(drivers/139): fix section reader leak and seek check in retry block - Check rd.Seek() return value and free the section reader on error - Call ss.FreeSectionReader(rd) on all error paths within retry.Do closure - Prevents buffer.Block leak when retrying failed upload chunks Co-authored-by: GitHub Copilot Signed-off-by: MadDogOwner --- drivers/139/driver.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 2f4adc1ca..965c77848 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -910,10 +910,14 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr if getErr != nil { return getErr } - rd.Seek(0, io.SeekStart) + if _, err := rd.Seek(0, io.SeekStart); err != nil { + ss.FreeSectionReader(rd) + return err + } req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, resp.Data.UploadResult.RedirectionURL, io.TeeReader(rd, p)) if reqErr != nil { + ss.FreeSectionReader(rd) return reqErr } req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) @@ -925,22 +929,27 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr res, doErr := base.HttpClient.Do(req) if doErr != nil { + ss.FreeSectionReader(rd) return doErr } defer res.Body.Close() bodyBytes, readErr := io.ReadAll(res.Body) if readErr != nil { + ss.FreeSectionReader(rd) return fmt.Errorf("error reading response body: %v", readErr) } if res.StatusCode != http.StatusOK { + ss.FreeSectionReader(rd) return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(bodyBytes)) } var result InterLayerUploadResult xmlErr := xml.Unmarshal(bodyBytes, &result) if xmlErr != nil { + ss.FreeSectionReader(rd) return fmt.Errorf("error parsing XML: %v", xmlErr) } if result.ResultCode != 0 { + ss.FreeSectionReader(rd) return fmt.Errorf("upload failed with result code: %d, message: %s", result.ResultCode, result.Msg) } return nil From b9b9f54b700a0fc0b3896ff786e957c8daf51239 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Thu, 16 Jul 2026 20:23:02 +0800 Subject: [PATCH 12/24] fix(drivers/139): move GetSectionReader outside retry loop - Move ss.GetSectionReader() before retry.Do() so the sequential stream section reader is only called once per chunk - Remove redundant ss.FreeSectionReader() calls from inside the retry closure since the reader is now owned by the outer scope - Fixes 'stream not cached' errors when retrying failed chunk uploads during cross-storage WebDAV COPY operations Co-authored-by: GitHub Copilot --- drivers/139/driver.go | 18 +++++------------- drivers/139/util.go | 18 ++++++------------ 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 965c77848..a3eb2b350 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -902,22 +902,19 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr start := i * partSize byteSize := min(size-start, partSize) - var rd io.ReadSeeker + rd, getErr := ss.GetSectionReader(start, byteSize) + if getErr != nil { + return getErr + } + err = retry.Do( func() error { - var getErr error - rd, getErr = ss.GetSectionReader(start, byteSize) - if getErr != nil { - return getErr - } if _, err := rd.Seek(0, io.SeekStart); err != nil { - ss.FreeSectionReader(rd) return err } req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, resp.Data.UploadResult.RedirectionURL, io.TeeReader(rd, p)) if reqErr != nil { - ss.FreeSectionReader(rd) return reqErr } req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) @@ -929,27 +926,22 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr res, doErr := base.HttpClient.Do(req) if doErr != nil { - ss.FreeSectionReader(rd) return doErr } defer res.Body.Close() bodyBytes, readErr := io.ReadAll(res.Body) if readErr != nil { - ss.FreeSectionReader(rd) return fmt.Errorf("error reading response body: %v", readErr) } if res.StatusCode != http.StatusOK { - ss.FreeSectionReader(rd) return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(bodyBytes)) } var result InterLayerUploadResult xmlErr := xml.Unmarshal(bodyBytes, &result) if xmlErr != nil { - ss.FreeSectionReader(rd) return fmt.Errorf("error parsing XML: %v", xmlErr) } if result.ResultCode != 0 { - ss.FreeSectionReader(rd) return fmt.Errorf("upload failed with result code: %d, message: %s", result.ResultCode, result.Msg) } return nil diff --git a/drivers/139/util.go b/drivers/139/util.go index 58872bc13..ff03c2cd5 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -716,22 +716,18 @@ func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, offset := partInfos[index].ParallelHashCtx.PartOffset log.Debugf("[139] uploading part %+v/%+v", index, len(partInfos)) - var rd io.ReadSeeker + rd, getErr := ss.GetSectionReader(offset, partSize) + if getErr != nil { + return getErr + } + err := retry.Do( func() error { - var getErr error - rd, getErr = ss.GetSectionReader(offset, partSize) - if getErr != nil { - return getErr - } - _, seekErr := rd.Seek(0, io.SeekStart) - if seekErr != nil { - ss.FreeSectionReader(rd) + if _, seekErr := rd.Seek(0, io.SeekStart); seekErr != nil { return seekErr } req, reqErr := http.NewRequestWithContext(ctx, http.MethodPut, uploadPartInfo.UploadUrl, io.TeeReader(rd, p)) if reqErr != nil { - ss.FreeSectionReader(rd) return reqErr } req.Header.Set("Content-Type", "application/octet-stream") @@ -742,14 +738,12 @@ func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, res, doErr := base.HttpClient.Do(req) if doErr != nil { - ss.FreeSectionReader(rd) return doErr } defer res.Body.Close() log.Debugf("[139] uploaded: %+v", res) if res.StatusCode != http.StatusOK { body, _ := io.ReadAll(res.Body) - ss.FreeSectionReader(rd) return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(body)) } return nil From 57e152372e45fff6ad9e737f0e90fd0aa4b794e5 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 17 Jul 2026 00:57:46 +0800 Subject: [PATCH 13/24] feat(drivers/139): add UseOldStreamUpload option - Add UseOldStreamUpload option - Implement newRequest, newPost - Support rapid upload for PersonalNew and Group/Family Signed-off-by: MadDogOwner --- drivers/139/driver.go | 36 ++++++++++++++++++++++++------------ drivers/139/meta.go | 1 + drivers/139/util.go | 19 ++++++++++++++++--- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index a3eb2b350..8a5d04f9c 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -627,8 +627,22 @@ func (d *Yun139) getPartSize(size int64) int64 { } func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { - switch d.Addition.Type { - case MetaPersonalNew: + // PersonalNew 以及 Group/Family 在非旧流模式时走新上传路径 + if d.Addition.Type == MetaPersonalNew || + ((d.isGroup() || d.isFamily()) && !d.UseOldStreamUpload) { + var createPath, getUploadUrlPath, completePath string + if d.isGroup() || d.isFamily() { + // 家庭盘和小组盘共用同一套新上传 API + createPath = "/dynamic/file/create" + getUploadUrlPath = "/dynamic/file/getUploadUrl" + completePath = "/dynamic/file/complete" + } else { + // MetaPersonalNew + createPath = "/file/create" + getUploadUrlPath = "/file/getUploadUrl" + completePath = "/file/complete" + } + var err error fullHash := stream.GetHash().GetHash(utils.SHA256) if len(fullHash) != utils.SHA256.Width { @@ -683,9 +697,8 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "type": "file", "fileRenameMode": "auto_rename", } - pathname := "/file/create" var resp PersonalUploadResp - _, err = d.personalPost(pathname, data, &resp) + _, err = d.newPost(createPath, data, &resp) if err != nil { return err } @@ -732,9 +745,8 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "accountType": 1, }, } - pathname := "/file/getUploadUrl" var moreresp PersonalUploadUrlResp - _, err = d.personalPost(pathname, moredata, &moreresp) + _, err = d.newPost(getUploadUrlPath, moredata, &moreresp) if err != nil { return err } @@ -751,7 +763,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "fileId": resp.Data.FileId, "uploadId": resp.Data.UploadId, } - _, err = d.personalPost("/file/complete", data, nil) + _, err = d.newPost(completePath, data, nil) if err != nil { return err } @@ -796,11 +808,11 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr } } return nil - case MetaPersonal: - fallthrough - case MetaGroup: - fallthrough - case MetaFamily: + } + + // 旧上传路径 + switch d.Addition.Type { + case MetaPersonal, MetaGroup, MetaFamily: // 处理冲突 // 获取文件列表 files, err := d.List(ctx, dstDir, model.ListArgs{}) diff --git a/drivers/139/meta.go b/drivers/139/meta.go index 91d54fd30..87de92e64 100644 --- a/drivers/139/meta.go +++ b/drivers/139/meta.go @@ -18,6 +18,7 @@ type Addition struct { 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"` + UseOldStreamUpload bool `json:"use_old_stream_upload" type:"bool" default:"false" help:"Enable to use old stream upload method (not support rapid upload)"` } var config = driver.Config{ diff --git a/drivers/139/util.go b/drivers/139/util.go index ff03c2cd5..201186de0 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -497,8 +497,7 @@ func unicode(str string) string { return textUnquoted } -func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { - url := d.getPersonalCloudHost() + pathname +func (d *Yun139) newRequest(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") @@ -560,7 +559,21 @@ func (d *Yun139) personalRequest(pathname string, method string, callback base.R } func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) { - return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) { + return d.newRequest(d.getFamilyCloudHost()+pathname, http.MethodPost, func(req *resty.Request) { + req.SetBody(data) + }, resp) +} + +func (d *Yun139) newPost(pathname string, data interface{}, resp interface{}) ([]byte, error) { + var url string + switch d.Type { + case MetaFamily, MetaGroup: + // this is on purpose + url = d.getGroupCloudHost() + pathname + default: + url = d.getPersonalCloudHost() + pathname + } + return d.newRequest(url, http.MethodPost, func(req *resty.Request) { req.SetBody(data) }, resp) } From bd9984423e2bcfcb363df0b4c76f0be2d04bb79e Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 17 Jul 2026 01:09:31 +0800 Subject: [PATCH 14/24] fix(drivers/139): add more param for group/family put Signed-off-by: MadDogOwner --- drivers/139/driver.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 8a5d04f9c..093335498 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -697,6 +697,16 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "type": "file", "fileRenameMode": "auto_rename", } + // 家庭盘和小组盘需要额外的参数 + if d.isGroup() || d.isFamily() { + if d.CloudID == "" { + return fmt.Errorf("cloud_id is required for group/family upload") + } + data["groupId"] = d.CloudID + data["groupType"] = 1 + data["catalogType"] = 3 + data["seqNo"] = random.String(32) + } var resp PersonalUploadResp _, err = d.newPost(createPath, data, &resp) if err != nil { From e947e3fdbfdbbb75149910710dcaa66b2677104b Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 17 Jul 2026 01:43:12 +0800 Subject: [PATCH 15/24] fix(drivers/139): correct group params for new put Signed-off-by: MadDogOwner --- drivers/139/driver.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 093335498..a5194005f 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -703,7 +703,11 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr return fmt.Errorf("cloud_id is required for group/family upload") } data["groupId"] = d.CloudID - data["groupType"] = 1 + if d.isGroup() { + data["groupType"] = 2 + } else if d.isFamily() { + data["groupType"] = 1 + } data["catalogType"] = 3 data["seqNo"] = random.String(32) } @@ -773,6 +777,10 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "fileId": resp.Data.FileId, "uploadId": resp.Data.UploadId, } + // 家庭盘和小组盘需要额外的参数 + if d.isGroup() || d.isFamily() { + data["groupId"] = d.CloudID + } _, err = d.newPost(completePath, data, nil) if err != nil { return err From ad6dfd611162f779f518cb909c32712705706c0d Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 17 Jul 2026 15:49:09 +0800 Subject: [PATCH 16/24] fix(drivers/139): update personalPost to use getPersonalCloudHost Signed-off-by: MadDogOwner --- drivers/139/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/139/util.go b/drivers/139/util.go index 201186de0..cf205e0a0 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -559,7 +559,7 @@ func (d *Yun139) newRequest(url string, method string, callback base.ReqCallback } func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) { - return d.newRequest(d.getFamilyCloudHost()+pathname, http.MethodPost, func(req *resty.Request) { + return d.newRequest(d.getPersonalCloudHost()+pathname, http.MethodPost, func(req *resty.Request) { req.SetBody(data) }, resp) } From cd91c64340afa2fe723c4059b51575f097866883 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Fri, 17 Jul 2026 16:15:13 +0800 Subject: [PATCH 17/24] chore(drivers/139): correct typo Signed-off-by: MadDogOwner --- drivers/139/driver.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index a5194005f..c1bdf7bb4 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -632,7 +632,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr ((d.isGroup() || d.isFamily()) && !d.UseOldStreamUpload) { var createPath, getUploadUrlPath, completePath string if d.isGroup() || d.isFamily() { - // 家庭盘和小组盘共用同一套新上传 API + // 家庭云和共享群共用同一套新上传 API createPath = "/dynamic/file/create" getUploadUrlPath = "/dynamic/file/getUploadUrl" completePath = "/dynamic/file/complete" @@ -697,7 +697,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "type": "file", "fileRenameMode": "auto_rename", } - // 家庭盘和小组盘需要额外的参数 + // 家庭云和共享群需要额外的参数 if d.isGroup() || d.isFamily() { if d.CloudID == "" { return fmt.Errorf("cloud_id is required for group/family upload") @@ -777,7 +777,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr "fileId": resp.Data.FileId, "uploadId": resp.Data.UploadId, } - // 家庭盘和小组盘需要额外的参数 + // 家庭云和共享群需要额外的参数 if d.isGroup() || d.isFamily() { data["groupId"] = d.CloudID } From 3dd3d71793de78dccea49406603d23e9099a09be Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Tue, 21 Jul 2026 23:21:16 +0800 Subject: [PATCH 18/24] fix(drivers/139): pass full partInfos slice for batched upload - Pass the global partInfos slice instead of the batch subset to uploadPersonalParts, so that PartNumber-1 indexing does not go out of bounds when a file has more than 100 parts. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/139/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index c1bdf7bb4..1f61f0573 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -764,7 +764,7 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr if err != nil { return err } - err = d.uploadPersonalParts(ctx, batchPartInfos, moreresp.Data.PartInfos, ss, p) + err = d.uploadPersonalParts(ctx, partInfos, moreresp.Data.PartInfos, ss, p) if err != nil { return err } From 7c12fbb9d5d588635e2fb50dfc4192b6a5ce83f8 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Tue, 21 Jul 2026 23:49:38 +0800 Subject: [PATCH 19/24] fix(drivers/139): avoid double-counting progress on upload retries - Save p.Done before each part and reset it inside the retry function so that bytes from failed attempts are not counted toward progress. - Each part is counted exactly once, on the successful attempt. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/139/util.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/139/util.go b/drivers/139/util.go index cf205e0a0..e3981147e 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -734,8 +734,12 @@ func (d *Yun139) uploadPersonalParts(ctx context.Context, partInfos []PartInfo, return getErr } + // Save progress before this part so retries don't double-count bytes + partDoneStart := p.Done err := retry.Do( func() error { + // Reset progress to the start of this part on each attempt + p.Done = partDoneStart if _, seekErr := rd.Seek(0, io.SeekStart); seekErr != nil { return seekErr } From 972d5665b474b98086d5296d0d57f49455fb1921 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Tue, 21 Jul 2026 23:26:05 +0800 Subject: [PATCH 20/24] fix(drivers/139): gate route-host checks by cloud type - Only validate GroupCloudHost and FamilyCloudHost for MetaGroup and MetaFamily storage types, so that personal-only accounts do not fail initialization when the route policy omits group/family. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- drivers/139/driver.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 1f61f0573..f2a941629 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -87,11 +87,13 @@ func (d *Yun139) Init(ctx context.Context) error { if len(d.PersonalCloudHost) == 0 { return fmt.Errorf("PersonalCloudHost is empty") } - if len(d.GroupCloudHost) == 0 { - return fmt.Errorf("GroupCloudHost is empty") - } - if len(d.FamilyCloudHost) == 0 { - return fmt.Errorf("FamilyCloudHost is empty") + if d.Addition.Type == MetaGroup || d.Addition.Type == MetaFamily { + if len(d.GroupCloudHost) == 0 { + return fmt.Errorf("GroupCloudHost is empty") + } + if len(d.FamilyCloudHost) == 0 { + return fmt.Errorf("FamilyCloudHost is empty") + } } d.cron = cron.NewCron(time.Hour * 12) From fd79574331b8c95a08988e316cd813c7fef10287 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 22 Jul 2026 02:16:15 +0800 Subject: [PATCH 21/24] fix(drivers/139): add ProviderRoot back for groupgetfiles Signed-off-by: MadDogOwner --- drivers/139/driver.go | 8 +++++--- drivers/139/util.go | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index f2a941629..5f79d0db5 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -32,6 +32,7 @@ type Yun139 struct { PersonalCloudHost string FamilyCloudHost string GroupCloudHost string + ProviderRoot string } func (d *Yun139) Config() driver.Config { @@ -122,9 +123,10 @@ func (d *Yun139) Init(ctx context.Context) error { return err } case MetaFamily: - if len(d.Addition.RootFolderID) == 0 { - // Attempt to obtain data.path as the root via a query and persist it. - if root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != "" { + // Attempt to obtain data.path as the root via a query and persist it. + if root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != "" { + d.ProviderRoot = root + if len(d.Addition.RootFolderID) == 0 { d.RootFolderID = root op.MustSaveDriverStorage(d) } diff --git a/drivers/139/util.go b/drivers/139/util.go index e3981147e..a01d67316 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -346,9 +346,9 @@ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) { "sortDirection": 1, }) // 传入 catalogID 是文件夹的ID,而不是完整路径 - // 当传入catalogID为根目录时,不能使用 catalogID - if catalogID == d.RootFolderID { - delete(data, "catalogID") + // 当传入catalogID为家庭云根目录时,直接留空 + if catalogID == d.ProviderRoot { + data["catalogID"] = "" } var resp QueryContentListResp _, err := d.post("/orchestration/familyCloud-rebuild/content/v1.2/queryContentList", data, &resp) From ca48b737f96eda0ab86a2e5d0d9475b3d183461f Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 22 Jul 2026 02:16:45 +0800 Subject: [PATCH 22/24] fix(drivers/139): set path to 0 when group old upload Signed-off-by: MadDogOwner --- drivers/139/driver.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 5f79d0db5..97f1ee380 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -883,6 +883,10 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" if d.isFamily() || d.isGroup() { uploadPath := d.dirPath(dstDir) + // 共享群的根目录上传路径为 0 + if d.isGroup() && dstDir.GetID() == d.RootFolderID { + uploadPath = "0" + } data = d.newJson(base.Json{ "fileCount": 1, "manualRename": 2, From 97c24ca705f6cc02a5c9fe246f78f6a8186978bc Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 22 Jul 2026 13:44:11 +0800 Subject: [PATCH 23/24] fix(drivers/139): improve error handling for family root path retrieval Signed-off-by: MadDogOwner --- drivers/139/driver.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index 97f1ee380..cddc921a2 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -124,14 +124,16 @@ func (d *Yun139) Init(ctx context.Context) error { } case MetaFamily: // Attempt to obtain data.path as the root via a query and persist it. - if root, err := d.getFamilyRootPath(d.CloudID); err == nil && root != "" { - d.ProviderRoot = root - if len(d.Addition.RootFolderID) == 0 { - d.RootFolderID = root - op.MustSaveDriverStorage(d) - } + root, err := d.getFamilyRootPath(d.CloudID) + if err != nil || root == "" { + return fmt.Errorf("failed to get family root path: %w", err) + } + d.ProviderRoot = root + if len(d.Addition.RootFolderID) == 0 { + d.RootFolderID = root + op.MustSaveDriverStorage(d) } - _, err := d.familyGetFiles(d.RootFolderID) + _, err = d.familyGetFiles(d.RootFolderID) if err != nil { return err } From dd56d492559ae12cb311ad1713ebedf0508cc252 Mon Sep 17 00:00:00 2001 From: MadDogOwner Date: Wed, 22 Jul 2026 13:49:30 +0800 Subject: [PATCH 24/24] refactor(drivers/139): update condition checks for CloudHost initialization Signed-off-by: MadDogOwner --- drivers/139/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/139/driver.go b/drivers/139/driver.go index cddc921a2..bae183d2e 100644 --- a/drivers/139/driver.go +++ b/drivers/139/driver.go @@ -88,7 +88,7 @@ func (d *Yun139) Init(ctx context.Context) error { if len(d.PersonalCloudHost) == 0 { return fmt.Errorf("PersonalCloudHost is empty") } - if d.Addition.Type == MetaGroup || d.Addition.Type == MetaFamily { + if d.isGroup() || d.isFamily() { if len(d.GroupCloudHost) == 0 { return fmt.Errorf("GroupCloudHost is empty") }