From d06f8b15f3c8fbda9150b884e71981d39630f3fb Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sat, 30 May 2026 20:15:59 +0800 Subject: [PATCH 01/20] feat(drivers/alidoc): add DingTalk Docs driver Generated with OpenAI Codex --- .gitignore | 4 +- drivers/alidoc/driver.go | 131 +++++++++++++++++++++++++++++++++++++ drivers/alidoc/meta.go | 24 +++++++ drivers/alidoc/types.go | 67 +++++++++++++++++++ drivers/alidoc/util.go | 135 +++++++++++++++++++++++++++++++++++++++ drivers/all.go | 1 + drivers/wps/driver.go | 12 ++++ 7 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 drivers/alidoc/driver.go create mode 100644 drivers/alidoc/meta.go create mode 100644 drivers/alidoc/types.go create mode 100644 drivers/alidoc/util.go diff --git a/.gitignore b/.gitignore index 1d71f0d608..5fc683c384 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ output/ /public/dist/* /!public/dist/README.md -.VSCodeCounter \ No newline at end of file +.VSCodeCounter + +/alidoc/ \ No newline at end of file diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go new file mode 100644 index 0000000000..b51e557a83 --- /dev/null +++ b/drivers/alidoc/driver.go @@ -0,0 +1,131 @@ +package alidoc + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/go-resty/resty/v2" +) + +type AliDoc struct { + model.Storage + Addition + + client *resty.Client +} + +func (d *AliDoc) Config() driver.Config { + return config +} + +func (d *AliDoc) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *AliDoc) Init(ctx context.Context) error { + d.Cookie = strings.TrimSpace(d.Cookie) + d.RootFolderID = strings.TrimSpace(d.RootFolderID) + if d.Cookie == "" { + return fmt.Errorf("cookie is empty") + } + if d.RootFolderID == "" { + return fmt.Errorf("root folder id is empty") + } + d.client = newClient() + if _, err := d.list(ctx, d.RootFolderID); err != nil { + return err + } + return nil +} + +func (d *AliDoc) Drop(ctx context.Context) error { + d.client = nil + return nil +} + +func (d *AliDoc) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + parentID := d.RootFolderID + parentPath := "/" + if dir != nil { + if id := strings.TrimSpace(dir.GetID()); id != "" { + parentID = id + } + if p := dir.GetPath(); p != "" { + parentPath = p + } + } + + items, err := d.list(ctx, parentID) + if err != nil { + return nil, err + } + + objs := make([]model.Obj, 0, len(items)) + for _, item := range items { + if strings.TrimSpace(item.DentryUUID) == "" || strings.TrimSpace(item.Name) == "" { + continue + } + objs = append(objs, toObj(parentPath, item)) + } + return objs, nil +} + +func (d *AliDoc) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + if file == nil || file.IsDir() { + return nil, fmt.Errorf("alidoc does not support directory links") + } + resp, err := d.download(ctx, file.GetID()) + if err != nil { + return nil, err + } + url, err := firstDownloadURL(resp) + if err != nil { + return nil, err + } + return &model.Link{ + URL: url, + Header: http.Header{ + "User-Agent": []string{base.UserAgent}, + "Referer": []string{apiBase + "/"}, + }, + }, nil +} + +func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { + return nil, readonlyError() +} + +func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { + return nil, readonlyError() +} + +func (d *AliDoc) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { + return nil, readonlyError() +} + +func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { + return nil, readonlyError() +} + +func (d *AliDoc) Remove(ctx context.Context, obj model.Obj) error { + return readonlyError() +} + +func (d *AliDoc) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { + return nil, readonlyError() +} + +var ( + _ driver.Driver = (*AliDoc)(nil) + _ driver.MkdirResult = (*AliDoc)(nil) + _ driver.MoveResult = (*AliDoc)(nil) + _ driver.RenameResult = (*AliDoc)(nil) + _ driver.CopyResult = (*AliDoc)(nil) + _ driver.Remove = (*AliDoc)(nil) + _ driver.PutResult = (*AliDoc)(nil) +) diff --git a/drivers/alidoc/meta.go b/drivers/alidoc/meta.go new file mode 100644 index 0000000000..e714fa5c49 --- /dev/null +++ b/drivers/alidoc/meta.go @@ -0,0 +1,24 @@ +package alidoc + +import ( + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/op" +) + +type Addition struct { + driver.RootID + Cookie string `json:"cookie" type:"text" required:"true" help:"DingTalk AliDoc web cookie"` +} + +var config = driver.Config{ + Name: "AliDoc", + LocalSort: true, + DefaultRoot: "", + Alert: "warning|AliDoc uses web cookies captured from the DingTalk document site. Keep the cookie private. This driver is read-only.", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &AliDoc{} + }) +} diff --git a/drivers/alidoc/types.go b/drivers/alidoc/types.go new file mode 100644 index 0000000000..5c064fb0e7 --- /dev/null +++ b/drivers/alidoc/types.go @@ -0,0 +1,67 @@ +package alidoc + +import "github.com/OpenListTeam/OpenList/v4/internal/model" + +type apiResp struct { + Status int `json:"status"` + IsSuccess bool `json:"isSuccess"` + Message string `json:"message"` + Msg string `json:"msg"` +} + +func (r apiResp) ErrMessage() string { + if r.Message != "" { + return r.Message + } + if r.Msg != "" { + return r.Msg + } + return "" +} + +type listResp struct { + apiResp + Data listData `json:"data"` +} + +type listData struct { + Children []dentry `json:"children"` +} + +type dentry struct { + DentryType string `json:"dentryType"` + DentryUUID string `json:"dentryUuid"` + Name string `json:"name"` + FileSize int64 `json:"fileSize"` + CreatedTime int64 `json:"createdTime"` + UpdatedTime int64 `json:"updatedTime"` + ContentType string `json:"contentType"` + Extension string `json:"extension"` + DentryStatistic struct { + ChildrenCount int `json:"childrenCount"` + } `json:"dentryStatistic"` + URL struct { + PCChildAppPreviewURL string `json:"pcChildAppPreviewUrl"` + PCChildAppURL string `json:"pcChildAppUrl"` + } `json:"url"` +} + +type downloadResp struct { + apiResp + Data downloadData `json:"data"` +} + +type downloadData struct { + OSSURLPreSignatureInfo struct { + PreSignURLs []string `json:"preSignUrls"` + } `json:"ossUrlPreSignatureInfo"` +} + +type Object struct { + model.Object + DentryType string + ContentType string + Extension string + PreviewURL string + EditURL string +} diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go new file mode 100644 index 0000000000..3c5cbe54b1 --- /dev/null +++ b/drivers/alidoc/util.go @@ -0,0 +1,135 @@ +package alidoc + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/go-resty/resty/v2" +) + +const apiBase = "https://alidocs.dingtalk.com" + +func (d *AliDoc) request(ctx context.Context) *resty.Request { + return d.client.R(). + SetContext(ctx). + SetHeader("Cookie", d.Cookie). + SetHeader("Accept", "application/json, text/plain, */*"). + SetHeader("Referer", apiBase+"/"). + SetHeader("Origin", apiBase) +} + +func joinPath(basePath, name string) string { + if basePath == "" || basePath == "/" { + return "/" + name + } + return strings.TrimRight(basePath, "/") + "/" + name +} + +func msToTime(v int64) time.Time { + if v <= 0 { + return time.Time{} + } + return time.UnixMilli(v) +} + +func checkResp(resp *resty.Response, result apiResp) error { + if resp != nil && resp.IsError() { + if msg := result.ErrMessage(); msg != "" { + return fmt.Errorf("%s", msg) + } + return fmt.Errorf("http error: %d", resp.StatusCode()) + } + if !result.IsSuccess || result.Status != 200 { + msg := result.ErrMessage() + if msg == "" { + msg = "request failed" + } + return fmt.Errorf("%s", msg) + } + return nil +} + +func toObj(parentPath string, item dentry) model.Obj { + obj := &Object{ + Object: model.Object{ + ID: item.DentryUUID, + Path: joinPath(parentPath, item.Name), + Name: item.Name, + Size: item.FileSize, + Modified: msToTime(item.UpdatedTime), + Ctime: msToTime(item.CreatedTime), + IsFolder: item.DentryType == "folder", + }, + DentryType: item.DentryType, + ContentType: item.ContentType, + Extension: item.Extension, + PreviewURL: item.URL.PCChildAppPreviewURL, + EditURL: item.URL.PCChildAppURL, + } + if obj.IsDir() && item.DentryStatistic.ChildrenCount > 0 && obj.Size == 0 { + // Keep size as 0 for folders; childrenCount is intentionally ignored. + } + return obj +} + +func readonlyError() error { + return fmt.Errorf("alidoc driver is read-only: %w", errs.NotSupport) +} + +func firstDownloadURL(resp downloadResp) (string, error) { + if len(resp.Data.OSSURLPreSignatureInfo.PreSignURLs) == 0 { + return "", fmt.Errorf("empty download url") + } + return resp.Data.OSSURLPreSignatureInfo.PreSignURLs[0], nil +} + +func newClient() *resty.Client { + client := base.NewRestyClient() + client.SetHeader("User-Agent", base.UserAgent) + return client +} + +func (d *AliDoc) list(ctx context.Context, dentryUUID string) ([]dentry, error) { + var result listResp + resp, err := d.request(ctx). + SetQueryParam("dentryUuid", dentryUUID). + SetQueryParam("withParentAncestors", "true"). + SetQueryParam("orderType", "SORT_KEY"). + SetQueryParam("sortType", "desc"). + SetQueryParam("listDentrySource", "2"). + SetQueryParam("pageSize", "1000"). + SetResult(&result). + SetError(&result). + Get(apiBase + "/box/api/v2/dentry/list") + if err != nil { + return nil, err + } + if err := checkResp(resp, result.apiResp); err != nil { + return nil, err + } + return result.Data.Children, nil +} + +func (d *AliDoc) download(ctx context.Context, dentryUUID string) (downloadResp, error) { + var result downloadResp + resp, err := d.request(ctx). + SetQueryParam("dentryUuid", dentryUUID). + SetQueryParam("version", "1"). + SetQueryParam("supportDownloadTypes", "URL_PRE_SIGNATURE,HTTP_TO_CENTER"). + SetQueryParam("downloadType", "URL_PRE_SIGNATURE"). + SetResult(&result). + SetError(&result). + Get(apiBase + "/box/api/v2/file/download") + if err != nil { + return result, err + } + if err := checkResp(resp, result.apiResp); err != nil { + return result, err + } + return result, nil +} diff --git a/drivers/all.go b/drivers/all.go index fb68d03950..67cd6434e6 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -13,6 +13,7 @@ import ( _ "github.com/OpenListTeam/OpenList/v4/drivers/189_tv" _ "github.com/OpenListTeam/OpenList/v4/drivers/189pc" _ "github.com/OpenListTeam/OpenList/v4/drivers/alias" + _ "github.com/OpenListTeam/OpenList/v4/drivers/alidoc" _ "github.com/OpenListTeam/OpenList/v4/drivers/alist_v3" _ "github.com/OpenListTeam/OpenList/v4/drivers/aliyundrive" _ "github.com/OpenListTeam/OpenList/v4/drivers/aliyundrive_open" diff --git a/drivers/wps/driver.go b/drivers/wps/driver.go index 847425b110..6e18d2110f 100644 --- a/drivers/wps/driver.go +++ b/drivers/wps/driver.go @@ -45,6 +45,9 @@ func (d *Wps) Init(ctx context.Context) error { if !resp.IsSuccess() { return fmt.Errorf("failed to check login status, status code: %d, body: %s", resp.StatusCode(), resp.String()) } + if d.login == nil { + return fmt.Errorf("wps login failed: empty login state, please check cookie") + } return nil } @@ -382,6 +385,10 @@ func (d *Wps) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer } func (d *Wps) GetDetails(ctx context.Context) (*model.StorageDetails, error) { + if d.login == nil { + return nil, fmt.Errorf("wps login state is nil, please reinitialize or check cookie") + } + if d.isPersonal() { url := ENDPOINT_PERSONAL + "/api/v3/spaces" var resp spacesResp @@ -399,6 +406,11 @@ func (d *Wps) GetDetails(ctx context.Context) (*model.StorageDetails, error) { }, }, nil } + + if d.login.CompanyID == 0 { + return nil, fmt.Errorf("wps company id is empty, please check business account login") + } + url := ENDPOINT_BUSINESS + "/3rd/plussvr/compose/v1/u/companies/batch/service-space?comp_ids=" + fmt.Sprint(d.login.CompanyID) var resp serviceSpaceResp r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url) From 3dfa30c9efb91d12a61161e778e873146ca44f8c Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sat, 30 May 2026 21:09:36 +0800 Subject: [PATCH 02/20] chore(drivers/alidoc): update metadata copy Generated with OpenAI Codex --- drivers/alidoc/meta.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/alidoc/meta.go b/drivers/alidoc/meta.go index e714fa5c49..1eb0b44e19 100644 --- a/drivers/alidoc/meta.go +++ b/drivers/alidoc/meta.go @@ -7,14 +7,14 @@ import ( type Addition struct { driver.RootID - Cookie string `json:"cookie" type:"text" required:"true" help:"DingTalk AliDoc web cookie"` + Cookie string `json:"cookie" type:"text" required:"true" help:"钉钉文档网页 Cookie"` } var config = driver.Config{ Name: "AliDoc", LocalSort: true, DefaultRoot: "", - Alert: "warning|AliDoc uses web cookies captured from the DingTalk document site. Keep the cookie private. This driver is read-only.", + Alert: "info|If you use PDF preview, please enable proxy, otherwise the preview may fail because of CORS.", } func init() { From 957d341bfdd4069122975e92de4c1461209e214d Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sat, 30 May 2026 21:12:21 +0800 Subject: [PATCH 03/20] chore(drivers/alidoc): update read-only alert copy Generated with OpenAI Codex --- drivers/alidoc/meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/alidoc/meta.go b/drivers/alidoc/meta.go index 1eb0b44e19..af810a95a8 100644 --- a/drivers/alidoc/meta.go +++ b/drivers/alidoc/meta.go @@ -14,7 +14,7 @@ var config = driver.Config{ Name: "AliDoc", LocalSort: true, DefaultRoot: "", - Alert: "info|If you use PDF preview, please enable proxy, otherwise the preview may fail because of CORS.", + Alert: "info|This driver supports accessing DingDrive through DingTalk Docs and is currently read-only.", } func init() { From fc36dcae1d64590f97a0b241ee91b9f919f61ea3 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sat, 30 May 2026 21:45:21 +0800 Subject: [PATCH 04/20] chore(gitignore): remove alidoc ignore rule Generated with OpenAI Codex --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5fc683c384..1d71f0d608 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,4 @@ output/ /public/dist/* /!public/dist/README.md -.VSCodeCounter - -/alidoc/ \ No newline at end of file +.VSCodeCounter \ No newline at end of file From 640912f947c010358d6a46090085203bfa2a348c Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sun, 31 May 2026 17:27:21 +0800 Subject: [PATCH 05/20] feat(alidoc): support upload move and recycle delete Generated with OpenAI Codex --- drivers/alidoc/driver.go | 56 ++++++- drivers/alidoc/meta.go | 4 +- drivers/alidoc/types.go | 49 ++++-- drivers/alidoc/upload.go | 322 +++++++++++++++++++++++++++++++++++++++ drivers/alidoc/util.go | 21 ++- 5 files changed, 436 insertions(+), 16 deletions(-) create mode 100644 drivers/alidoc/upload.go diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index b51e557a83..708da826af 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -101,7 +101,38 @@ func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin } func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, readonlyError() + if srcObj == nil { + return nil, fmt.Errorf("source object is nil") + } + if dstDir == nil { + return nil, fmt.Errorf("destination directory is nil") + } + sourceID := strings.TrimSpace(srcObj.GetID()) + targetID := strings.TrimSpace(dstDir.GetID()) + if sourceID == "" { + return nil, fmt.Errorf("source dentry uuid is empty") + } + if targetID == "" { + return nil, fmt.Errorf("target parent dentry uuid is empty") + } + + var result apiResp + resp, err := d.request(ctx). + SetBody(map[string]interface{}{ + "targetParentDentryUuid": targetID, + "sourceDentryUuid": sourceID, + "operateFrom": 1, + }). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v2/dentry/move") + if err != nil { + return nil, err + } + if err := checkResp(resp, result); err != nil { + return nil, err + } + return srcObj, nil } func (d *AliDoc) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { @@ -113,11 +144,30 @@ func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, } func (d *AliDoc) Remove(ctx context.Context, obj model.Obj) error { - return readonlyError() + if obj == nil { + return fmt.Errorf("object is nil") + } + dentryUUID := strings.TrimSpace(obj.GetID()) + if dentryUUID == "" { + return fmt.Errorf("dentry uuid is empty") + } + + var result apiResp + resp, err := d.request(ctx). + SetBody(map[string]string{ + "dentryUuid": dentryUUID, + }). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v1/dentry/recycle") + if err != nil { + return err + } + return checkResp(resp, result) } func (d *AliDoc) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - return nil, readonlyError() + return d.put(ctx, dstDir, file, up) } var ( diff --git a/drivers/alidoc/meta.go b/drivers/alidoc/meta.go index af810a95a8..b289ebd439 100644 --- a/drivers/alidoc/meta.go +++ b/drivers/alidoc/meta.go @@ -7,14 +7,14 @@ import ( type Addition struct { driver.RootID - Cookie string `json:"cookie" type:"text" required:"true" help:"钉钉文档网页 Cookie"` + Cookie string `json:"cookie" type:"text" required:"true" help:"钉钉文档网页 Cookie"` } var config = driver.Config{ Name: "AliDoc", LocalSort: true, DefaultRoot: "", - Alert: "info|This driver supports accessing DingDrive through DingTalk Docs and is currently read-only.", + Alert: "info|This driver supports accessing DingDrive through DingTalk Docs, including listing, download, upload, move, and recycle delete.", } func init() { diff --git a/drivers/alidoc/types.go b/drivers/alidoc/types.go index 5c064fb0e7..73b48d0aef 100644 --- a/drivers/alidoc/types.go +++ b/drivers/alidoc/types.go @@ -29,15 +29,17 @@ type listData struct { } type dentry struct { - DentryType string `json:"dentryType"` - DentryUUID string `json:"dentryUuid"` - Name string `json:"name"` - FileSize int64 `json:"fileSize"` - CreatedTime int64 `json:"createdTime"` - UpdatedTime int64 `json:"updatedTime"` - ContentType string `json:"contentType"` - Extension string `json:"extension"` - DentryStatistic struct { + DentryType string `json:"dentryType"` + DentryUUID string `json:"dentryUuid"` + ParentDentryUUID string `json:"parentDentryUuid"` + Name string `json:"name"` + Path string `json:"path"` + FileSize int64 `json:"fileSize"` + CreatedTime int64 `json:"createdTime"` + UpdatedTime int64 `json:"updatedTime"` + ContentType string `json:"contentType"` + Extension string `json:"extension"` + DentryStatistic struct { ChildrenCount int `json:"childrenCount"` } `json:"dentryStatistic"` URL struct { @@ -57,6 +59,35 @@ type downloadData struct { } `json:"ossUrlPreSignatureInfo"` } +type uploadInfoResp struct { + apiResp + Data uploadInfoData `json:"data"` +} + +type uploadInfoData struct { + CurrentTimestamp int64 `json:"currentTimestamp"` + FileUploadProtocolConfig uploadProtocolConfig `json:"fileUploadProtocolConfig"` + STSSignatureInfo uploadSTSSignatureInfo `json:"stsSignatureInfo"` + UploadKey string `json:"uploadKey"` + UploadType string `json:"uploadType"` +} + +type uploadProtocolConfig struct { + MinPartSize int64 `json:"minPartSize"` +} + +type uploadSTSSignatureInfo struct { + AccelerateCname string `json:"accelerateCname"` + AccessKeyID string `json:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret"` + AccessToken string `json:"accessToken"` + AccessTokenExpiration int64 `json:"accessTokenExpiration"` + Bucket string `json:"bucket"` + Cname string `json:"cname"` + EndPoint string `json:"endPoint"` + ObjectKey string `json:"objectKey"` +} + type Object struct { model.Object DentryType string diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go new file mode 100644 index 0000000000..0630487243 --- /dev/null +++ b/drivers/alidoc/upload.go @@ -0,0 +1,322 @@ +package alidoc + +import ( + "context" + "fmt" + "io" + "math" + "strings" + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/model" + netutil "github.com/OpenListTeam/OpenList/v4/internal/net" + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +const ( + defaultAliDocMultipartThreshold = 16 * 1024 * 1024 + defaultAliDocPartSize = 100 * 1024 + maxAliDocMultipartParts = 10000 +) + +func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { + if file == nil { + return nil, fmt.Errorf("file is nil") + } + if up == nil { + up = func(float64) {} + } + + parentID := d.RootFolderID + parentPath := "/" + if dstDir != nil { + if id := strings.TrimSpace(dstDir.GetID()); id != "" { + parentID = id + } + if p := dstDir.GetPath(); p != "" { + parentPath = p + } + } + + src, size, err := prepareAliDocUploadFile(file) + if err != nil { + return nil, err + } + + useMultipart := size > defaultAliDocMultipartThreshold + info, err := d.getUploadInfo(ctx, parentID, file.GetName(), size, useMultipart) + if err != nil { + return nil, err + } + if size > 0 { + partSize := calcAliDocPartSize(size, info.Data.FileUploadProtocolConfig.MinPartSize) + if size > partSize && !useMultipart { + useMultipart = true + info, err = d.getUploadInfo(ctx, parentID, file.GetName(), size, true) + if err != nil { + return nil, err + } + } + } + + startedAt := time.Now() + if useMultipart && size > 0 { + err = d.multipartUpload(ctx, src, size, info, up) + } else { + err = d.singleUpload(ctx, src, size, info, up) + } + if err != nil { + return nil, err + } + + if obj, err := d.findUploadedObj(ctx, parentID, parentPath, file.GetName(), size, startedAt); err == nil && obj != nil { + return obj, nil + } + + return &Object{ + Object: model.Object{ + Path: joinPath(parentPath, file.GetName()), + Name: file.GetName(), + Size: size, + Modified: startedAt, + Ctime: startedAt, + }, + DentryType: "file", + }, nil +} + +func prepareAliDocUploadFile(file model.FileStreamer) (model.File, int64, error) { + size := file.GetSize() + if src := file.GetFile(); src != nil && size >= 0 { + if _, err := src.Seek(0, io.SeekStart); err != nil { + return nil, 0, err + } + return src, size, nil + } + + src, err := file.CacheFullAndWriter(nil, nil) + if err != nil { + return nil, 0, err + } + if _, err := src.Seek(0, io.SeekStart); err != nil { + return nil, 0, err + } + size = file.GetSize() + if size < 0 { + cur, err := src.Seek(0, io.SeekCurrent) + if err != nil { + return nil, 0, err + } + end, err := src.Seek(0, io.SeekEnd) + if err != nil { + return nil, 0, err + } + size = end + if _, err := src.Seek(cur, io.SeekStart); err != nil { + return nil, 0, err + } + } + return src, size, nil +} + +func (d *AliDoc) getUploadInfo(ctx context.Context, parentDentryUUID, name string, fileSize int64, multipart bool) (uploadInfoResp, error) { + var result uploadInfoResp + body := map[string]interface{}{ + "uploadType": "STS_SIGNATURE", + "supportUploadTypes": []string{"STS_SIGNATURE", "HTTP_TO_CENTER"}, + "parentDentryUuid": parentDentryUUID, + "fileSize": fileSize, + "name": name, + "multipart": multipart, + } + resp, err := d.request(ctx). + SetBody(body). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v2/file/uploadinfo") + if err != nil { + return result, err + } + if err := checkResp(resp, result.apiResp); err != nil { + return result, err + } + if strings.TrimSpace(result.Data.STSSignatureInfo.Bucket) == "" { + return result, fmt.Errorf("empty upload bucket") + } + return result, nil +} + +func calcAliDocPartSize(fileSize, minPartSize int64) int64 { + partSize := minPartSize + if partSize <= 0 { + partSize = defaultAliDocPartSize + } + if fileSize <= 0 { + return partSize + } + minRequired := int64(math.Ceil(float64(fileSize) / maxAliDocMultipartParts)) + if minRequired > partSize { + partSize = minRequired + } + return partSize +} + +func (d *AliDoc) singleUpload(ctx context.Context, src model.File, size int64, info uploadInfoResp, up driver.UpdateProgress) error { + bucket, objectKey, err := d.newOSSBucket(info) + if err != nil { + return err + } + if _, err := src.Seek(0, io.SeekStart); err != nil { + return err + } + reader := io.NewSectionReader(src, 0, size) + err = bucket.PutObject( + objectKey, + driver.NewLimitedUploadStream(ctx, io.TeeReader(reader, driver.NewProgress(size, up))), + ) + if err != nil { + return err + } + up(100) + return nil +} + +func (d *AliDoc) multipartUpload(ctx context.Context, src model.File, size int64, info uploadInfoResp, up driver.UpdateProgress) error { + bucket, objectKey, err := d.newOSSBucket(info) + if err != nil { + return err + } + + imur, err := bucket.InitiateMultipartUpload(objectKey, oss.Sequential()) + if err != nil { + return err + } + + partSize := calcAliDocPartSize(size, info.Data.FileUploadProtocolConfig.MinPartSize) + partNum := int((size + partSize - 1) / partSize) + parts := make([]oss.UploadPart, 0, partNum) + progress := driver.NewProgress(size, up) + + var offset int64 + for partNumber := 1; partNumber <= partNum; partNumber++ { + if err := ctx.Err(); err != nil { + return err + } + length := partSize + if remain := size - offset; remain < length { + length = remain + } + + var part oss.UploadPart + var uploadErr error + for attempt := 0; attempt < 3; attempt++ { + reader := io.NewSectionReader(src, offset, length) + part, uploadErr = bucket.UploadPart( + imur, + driver.NewLimitedUploadStream(ctx, io.TeeReader(reader, progress)), + length, + partNumber, + ) + if uploadErr == nil { + break + } + } + if uploadErr != nil { + return uploadErr + } + parts = append(parts, part) + offset += length + } + + _, err = bucket.CompleteMultipartUpload(imur, parts) + if err != nil { + return err + } + up(100) + return nil +} + +func (d *AliDoc) newOSSBucket(info uploadInfoResp) (*oss.Bucket, string, error) { + sts := info.Data.STSSignatureInfo + objectKey := strings.TrimSpace(sts.ObjectKey) + if objectKey == "" { + objectKey = strings.TrimSpace(info.Data.UploadKey) + } + if objectKey == "" { + return nil, "", fmt.Errorf("empty upload object key") + } + + endpoint := strings.TrimSpace(sts.AccelerateCname) + if endpoint == "" { + endpoint = strings.TrimSpace(sts.Cname) + } + if endpoint == "" { + endpoint = strings.TrimSpace(sts.EndPoint) + } + if endpoint == "" { + return nil, "", fmt.Errorf("empty upload endpoint") + } + if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { + endpoint = "https://" + endpoint + } + + client, err := netutil.NewOSSClient( + endpoint, + sts.AccessKeyID, + sts.AccessKeySecret, + oss.SecurityToken(sts.AccessToken), + ) + if err != nil { + return nil, "", err + } + bucket, err := client.Bucket(sts.Bucket) + if err != nil { + return nil, "", err + } + return bucket, objectKey, nil +} + +func (d *AliDoc) findUploadedObj(ctx context.Context, parentID, parentPath, name string, size int64, startedAt time.Time) (model.Obj, error) { + for attempt := 0; attempt < 5; attempt++ { + items, err := d.list(ctx, parentID) + if err != nil { + return nil, err + } + var ( + matched dentry + hasMatched bool + ) + for i := range items { + item := items[i] + if item.DentryType != "file" || item.Name != name { + continue + } + if size >= 0 && item.FileSize != size { + continue + } + if !hasMatched || item.UpdatedTime > matched.UpdatedTime { + matched = item + hasMatched = true + } + } + if hasMatched { + obj := toObj(parentPath, matched) + if !obj.ModTime().IsZero() && obj.ModTime().Before(startedAt.Add(-5*time.Second)) { + // Keep polling briefly if only an older homonymous file is visible. + } else { + return obj, nil + } + } + if attempt < 4 { + timer := time.NewTimer(time.Duration(attempt+1) * 300 * time.Millisecond) + select { + case <-ctx.Done(): + timer.Stop() + return nil, ctx.Err() + case <-timer.C: + } + } + } + return nil, fmt.Errorf("uploaded object not found") +} diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index 3c5cbe54b1..96538e144b 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -3,6 +3,7 @@ package alidoc import ( "context" "fmt" + "path" "strings" "time" @@ -55,11 +56,27 @@ func checkResp(resp *resty.Response, result apiResp) error { } func toObj(parentPath string, item dentry) model.Obj { + return toObjWithPath(joinPath(parentPath, item.Name), item) +} + +func toObjUsingBestPath(parentPath string, item dentry) model.Obj { + fullPath := strings.TrimSpace(item.Path) + if fullPath == "" { + fullPath = joinPath(parentPath, item.Name) + } + return toObjWithPath(fullPath, item) +} + +func toObjWithPath(fullPath string, item dentry) model.Obj { + name := item.Name + if name == "" { + name = path.Base(fullPath) + } obj := &Object{ Object: model.Object{ ID: item.DentryUUID, - Path: joinPath(parentPath, item.Name), - Name: item.Name, + Path: fullPath, + Name: name, Size: item.FileSize, Modified: msToTime(item.UpdatedTime), Ctime: msToTime(item.CreatedTime), From c1205ebf033b577ddb809a12df2217bf0421af39 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sun, 31 May 2026 17:48:35 +0800 Subject: [PATCH 06/20] fix(alidoc): commit uploaded files after oss transfer Generated with OpenAI Codex --- drivers/alidoc/upload.go | 60 ++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go index 0630487243..043b60bf87 100644 --- a/drivers/alidoc/upload.go +++ b/drivers/alidoc/upload.go @@ -12,6 +12,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/model" netutil "github.com/OpenListTeam/OpenList/v4/internal/net" "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/google/uuid" ) const ( @@ -69,6 +70,9 @@ func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStrea if err != nil { return nil, err } + if err := d.commitUpload(ctx, parentID, file.GetName(), size, info.Data.UploadKey); err != nil { + return nil, err + } if obj, err := d.findUploadedObj(ctx, parentID, parentPath, file.GetName(), size, startedAt); err == nil && obj != nil { return obj, nil @@ -147,6 +151,35 @@ func (d *AliDoc) getUploadInfo(ctx context.Context, parentDentryUUID, name strin return result, nil } +func (d *AliDoc) commitUpload(ctx context.Context, parentDentryUUID, name string, fileSize int64, uploadKey string) error { + uploadKey = strings.TrimSpace(uploadKey) + if uploadKey == "" { + return fmt.Errorf("empty upload key") + } + + var result apiResp + body := map[string]interface{}{ + "parentDentryUuid": parentDentryUUID, + "uploadKey": uploadKey, + "fileSize": fileSize, + "name": name, + "toPrevDentryUuid": nil, + "toNextDentryUuid": nil, + "batchId": uuid.NewString(), + "batchUploadType": 1, + "batchParentDentryUuid": parentDentryUUID, + } + resp, err := d.request(ctx). + SetBody(body). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v2/file/commit") + if err != nil { + return err + } + return checkResp(resp, result) +} + func calcAliDocPartSize(fileSize, minPartSize int64) int64 { partSize := minPartSize if partSize <= 0 { @@ -247,13 +280,7 @@ func (d *AliDoc) newOSSBucket(info uploadInfoResp) (*oss.Bucket, string, error) return nil, "", fmt.Errorf("empty upload object key") } - endpoint := strings.TrimSpace(sts.AccelerateCname) - if endpoint == "" { - endpoint = strings.TrimSpace(sts.Cname) - } - if endpoint == "" { - endpoint = strings.TrimSpace(sts.EndPoint) - } + endpoint, useCname := pickAliDocOSSEndpoint(sts) if endpoint == "" { return nil, "", fmt.Errorf("empty upload endpoint") } @@ -261,11 +288,15 @@ func (d *AliDoc) newOSSBucket(info uploadInfoResp) (*oss.Bucket, string, error) endpoint = "https://" + endpoint } + options := []oss.ClientOption{oss.SecurityToken(sts.AccessToken)} + if useCname { + options = append(options, oss.UseCname(true)) + } client, err := netutil.NewOSSClient( endpoint, sts.AccessKeyID, sts.AccessKeySecret, - oss.SecurityToken(sts.AccessToken), + options..., ) if err != nil { return nil, "", err @@ -277,6 +308,19 @@ func (d *AliDoc) newOSSBucket(info uploadInfoResp) (*oss.Bucket, string, error) return bucket, objectKey, nil } +func pickAliDocOSSEndpoint(sts uploadSTSSignatureInfo) (endpoint string, useCname bool) { + if endpoint = strings.TrimSpace(sts.EndPoint); endpoint != "" { + return endpoint, false + } + if endpoint = strings.TrimSpace(sts.Cname); endpoint != "" { + return endpoint, true + } + if endpoint = strings.TrimSpace(sts.AccelerateCname); endpoint != "" { + return endpoint, true + } + return "", false +} + func (d *AliDoc) findUploadedObj(ctx context.Context, parentID, parentPath, name string, size int64, startedAt time.Time) (model.Obj, error) { for attempt := 0; attempt < 5; attempt++ { items, err := d.list(ctx, parentID) From 4330be2e35623f00cba0b22784da2e2800506127 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sun, 31 May 2026 19:20:06 +0800 Subject: [PATCH 07/20] feat(alidoc): support mkdir copy and rename Generated with OpenAI Codex --- drivers/alidoc/driver.go | 135 ++++++++++++++++++++++++++++++++++++++- drivers/alidoc/util.go | 10 +++ 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index 708da826af..1754345c7f 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "path" "strings" "github.com/OpenListTeam/OpenList/v4/drivers/base" @@ -97,7 +98,47 @@ func (d *AliDoc) Link(ctx context.Context, file model.Obj, args model.LinkArgs) } func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - return nil, readonlyError() + dirName = strings.TrimSpace(dirName) + if dirName == "" { + return nil, fmt.Errorf("folder name is empty") + } + + parentID := d.RootFolderID + parentPath := "/" + if parentDir != nil { + if id := strings.TrimSpace(parentDir.GetID()); id != "" { + parentID = id + } + if p := parentDir.GetPath(); p != "" { + parentPath = p + } + } + + var result apiResp + resp, err := d.request(ctx). + SetBody(map[string]string{ + "dentryType": "folder", + "name": dirName, + "parentDentryUuid": parentID, + "conflictHandleStrategy": "auto_rename", + }). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v2/dentry/createfolder") + if err != nil { + return nil, err + } + if err := checkResp(resp, result); err != nil { + return nil, err + } + return &Object{ + Object: model.Object{ + Path: joinPath(parentPath, dirName), + Name: dirName, + IsFolder: true, + }, + DentryType: "folder", + }, nil } func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { @@ -136,11 +177,99 @@ func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, } func (d *AliDoc) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - return nil, readonlyError() + if srcObj == nil { + return nil, fmt.Errorf("source object is nil") + } + newName = strings.TrimSpace(newName) + if newName == "" { + return nil, fmt.Errorf("new name is empty") + } + dentryUUID := strings.TrimSpace(srcObj.GetID()) + if dentryUUID == "" { + return nil, fmt.Errorf("dentry uuid is empty") + } + + var result apiResp + resp, err := d.request(ctx). + SetBody(map[string]string{ + "dentryUuid": dentryUUID, + "name": newName, + }). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v2/dentry/rename") + if err != nil { + return nil, err + } + if err := checkResp(resp, result); err != nil { + return nil, err + } + + newPath := srcObj.GetPath() + if newPath != "" { + newPath = path.Join(path.Dir(newPath), newName) + } + return &Object{ + Object: model.Object{ + ID: srcObj.GetID(), + Path: newPath, + Name: newName, + Size: srcObj.GetSize(), + Modified: srcObj.ModTime(), + Ctime: srcObj.CreateTime(), + IsFolder: srcObj.IsDir(), + HashInfo: srcObj.GetHash(), + }, + DentryType: pickAliDocDentryType(srcObj), + }, nil } func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - return nil, readonlyError() + if srcObj == nil { + return nil, fmt.Errorf("source object is nil") + } + if dstDir == nil { + return nil, fmt.Errorf("destination directory is nil") + } + sourceID := strings.TrimSpace(srcObj.GetID()) + targetID := strings.TrimSpace(dstDir.GetID()) + if sourceID == "" { + return nil, fmt.Errorf("source dentry uuid is empty") + } + if targetID == "" { + return nil, fmt.Errorf("target parent dentry uuid is empty") + } + + var result apiResp + resp, err := d.request(ctx). + SetBody(map[string]interface{}{ + "sourceDentryUuid": sourceID, + "targetParentDentryUuid": targetID, + "operateFrom": 1, + "onlyCopyMeta": false, + }). + SetResult(&result). + SetError(&result). + Post(apiBase + "/box/api/v2/dentry/copy") + if err != nil { + return nil, err + } + if err := checkResp(resp, result); err != nil { + return nil, err + } + + return &Object{ + Object: model.Object{ + Path: joinPath(dstDir.GetPath(), srcObj.GetName()), + Name: srcObj.GetName(), + Size: srcObj.GetSize(), + Modified: srcObj.ModTime(), + Ctime: srcObj.CreateTime(), + IsFolder: srcObj.IsDir(), + HashInfo: srcObj.GetHash(), + }, + DentryType: pickAliDocDentryType(srcObj), + }, nil } func (d *AliDoc) Remove(ctx context.Context, obj model.Obj) error { diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index 96538e144b..b4af83cf4e 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -111,6 +111,16 @@ func newClient() *resty.Client { return client } +func pickAliDocDentryType(obj model.Obj) string { + if o, ok := obj.(*Object); ok && strings.TrimSpace(o.DentryType) != "" { + return o.DentryType + } + if obj != nil && obj.IsDir() { + return "folder" + } + return "file" +} + func (d *AliDoc) list(ctx context.Context, dentryUUID string) ([]dentry, error) { var result listResp resp, err := d.request(ctx). From 08b031e24516fc7494910cda614a19173fe5d5b4 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Sun, 31 May 2026 21:41:31 +0800 Subject: [PATCH 08/20] chore(alidoc): remove unused readonly helper --- drivers/alidoc/util.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index b4af83cf4e..be9d389e83 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -8,7 +8,6 @@ import ( "time" "github.com/OpenListTeam/OpenList/v4/drivers/base" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/go-resty/resty/v2" ) @@ -94,10 +93,6 @@ func toObjWithPath(fullPath string, item dentry) model.Obj { return obj } -func readonlyError() error { - return fmt.Errorf("alidoc driver is read-only: %w", errs.NotSupport) -} - func firstDownloadURL(resp downloadResp) (string, error) { if len(resp.Data.OSSURLPreSignatureInfo.PreSignURLs) == 0 { return "", fmt.Errorf("empty download url") From b73616e932e43b96cbd2da72e154801ae1050c1b Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Mon, 1 Jun 2026 07:57:15 +0800 Subject: [PATCH 09/20] chore(alidoc): remove driver alert copy --- drivers/alidoc/meta.go | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/alidoc/meta.go b/drivers/alidoc/meta.go index b289ebd439..3c0fa19a94 100644 --- a/drivers/alidoc/meta.go +++ b/drivers/alidoc/meta.go @@ -14,7 +14,6 @@ var config = driver.Config{ Name: "AliDoc", LocalSort: true, DefaultRoot: "", - Alert: "info|This driver supports accessing DingDrive through DingTalk Docs, including listing, download, upload, move, and recycle delete.", } func init() { From 5242f65d6367a100a0f8f239fdc01284290b0c98 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Mon, 1 Jun 2026 20:26:36 +0800 Subject: [PATCH 10/20] refactor(alidoc): simplify id-based driver behavior Generated with OpenAI Codex --- drivers/alidoc/driver.go | 19 +------ drivers/alidoc/upload.go | 116 ++++++--------------------------------- drivers/alidoc/util.go | 22 +------- 3 files changed, 21 insertions(+), 136 deletions(-) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index 1754345c7f..da35e8b358 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "path" "strings" "github.com/OpenListTeam/OpenList/v4/drivers/base" @@ -51,14 +50,10 @@ func (d *AliDoc) Drop(ctx context.Context) error { func (d *AliDoc) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { parentID := d.RootFolderID - parentPath := "/" if dir != nil { if id := strings.TrimSpace(dir.GetID()); id != "" { parentID = id } - if p := dir.GetPath(); p != "" { - parentPath = p - } } items, err := d.list(ctx, parentID) @@ -71,7 +66,7 @@ func (d *AliDoc) List(ctx context.Context, dir model.Obj, args model.ListArgs) ( if strings.TrimSpace(item.DentryUUID) == "" || strings.TrimSpace(item.Name) == "" { continue } - objs = append(objs, toObj(parentPath, item)) + objs = append(objs, toObj(item)) } return objs, nil } @@ -104,14 +99,10 @@ func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin } parentID := d.RootFolderID - parentPath := "/" if parentDir != nil { if id := strings.TrimSpace(parentDir.GetID()); id != "" { parentID = id } - if p := parentDir.GetPath(); p != "" { - parentPath = p - } } var result apiResp @@ -133,7 +124,6 @@ func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin } return &Object{ Object: model.Object{ - Path: joinPath(parentPath, dirName), Name: dirName, IsFolder: true, }, @@ -204,15 +194,9 @@ func (d *AliDoc) Rename(ctx context.Context, srcObj model.Obj, newName string) ( if err := checkResp(resp, result); err != nil { return nil, err } - - newPath := srcObj.GetPath() - if newPath != "" { - newPath = path.Join(path.Dir(newPath), newName) - } return &Object{ Object: model.Object{ ID: srcObj.GetID(), - Path: newPath, Name: newName, Size: srcObj.GetSize(), Modified: srcObj.ModTime(), @@ -260,7 +244,6 @@ func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, return &Object{ Object: model.Object{ - Path: joinPath(dstDir.GetPath(), srcObj.GetName()), Name: srcObj.GetName(), Size: srcObj.GetSize(), Modified: srcObj.ModTime(), diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go index 043b60bf87..6c2ac6a476 100644 --- a/drivers/alidoc/upload.go +++ b/drivers/alidoc/upload.go @@ -6,7 +6,6 @@ import ( "io" "math" "strings" - "time" "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" @@ -30,19 +29,15 @@ func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStrea } parentID := d.RootFolderID - parentPath := "/" if dstDir != nil { if id := strings.TrimSpace(dstDir.GetID()); id != "" { parentID = id } - if p := dstDir.GetPath(); p != "" { - parentPath = p - } } - src, size, err := prepareAliDocUploadFile(file) - if err != nil { - return nil, err + size := file.GetSize() + if size < 0 { + return nil, fmt.Errorf("unknown file size is not supported") } useMultipart := size > defaultAliDocMultipartThreshold @@ -61,11 +56,14 @@ func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStrea } } - startedAt := time.Now() if useMultipart && size > 0 { + src, err := prepareAliDocUploadFile(file) + if err != nil { + return nil, err + } err = d.multipartUpload(ctx, src, size, info, up) } else { - err = d.singleUpload(ctx, src, size, info, up) + err = d.singleUpload(ctx, file, size, info, up) } if err != nil { return nil, err @@ -73,55 +71,25 @@ func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStrea if err := d.commitUpload(ctx, parentID, file.GetName(), size, info.Data.UploadKey); err != nil { return nil, err } - - if obj, err := d.findUploadedObj(ctx, parentID, parentPath, file.GetName(), size, startedAt); err == nil && obj != nil { - return obj, nil - } - - return &Object{ - Object: model.Object{ - Path: joinPath(parentPath, file.GetName()), - Name: file.GetName(), - Size: size, - Modified: startedAt, - Ctime: startedAt, - }, - DentryType: "file", - }, nil + return nil, nil } -func prepareAliDocUploadFile(file model.FileStreamer) (model.File, int64, error) { - size := file.GetSize() - if src := file.GetFile(); src != nil && size >= 0 { +func prepareAliDocUploadFile(file model.FileStreamer) (model.File, error) { + if src := file.GetFile(); src != nil { if _, err := src.Seek(0, io.SeekStart); err != nil { - return nil, 0, err + return nil, err } - return src, size, nil + return src, nil } src, err := file.CacheFullAndWriter(nil, nil) if err != nil { - return nil, 0, err + return nil, err } if _, err := src.Seek(0, io.SeekStart); err != nil { - return nil, 0, err - } - size = file.GetSize() - if size < 0 { - cur, err := src.Seek(0, io.SeekCurrent) - if err != nil { - return nil, 0, err - } - end, err := src.Seek(0, io.SeekEnd) - if err != nil { - return nil, 0, err - } - size = end - if _, err := src.Seek(cur, io.SeekStart); err != nil { - return nil, 0, err - } + return nil, err } - return src, size, nil + return src, nil } func (d *AliDoc) getUploadInfo(ctx context.Context, parentDentryUUID, name string, fileSize int64, multipart bool) (uploadInfoResp, error) { @@ -195,18 +163,14 @@ func calcAliDocPartSize(fileSize, minPartSize int64) int64 { return partSize } -func (d *AliDoc) singleUpload(ctx context.Context, src model.File, size int64, info uploadInfoResp, up driver.UpdateProgress) error { +func (d *AliDoc) singleUpload(ctx context.Context, src model.FileStreamer, size int64, info uploadInfoResp, up driver.UpdateProgress) error { bucket, objectKey, err := d.newOSSBucket(info) if err != nil { return err } - if _, err := src.Seek(0, io.SeekStart); err != nil { - return err - } - reader := io.NewSectionReader(src, 0, size) err = bucket.PutObject( objectKey, - driver.NewLimitedUploadStream(ctx, io.TeeReader(reader, driver.NewProgress(size, up))), + driver.NewLimitedUploadStream(ctx, io.TeeReader(src, driver.NewProgress(size, up))), ) if err != nil { return err @@ -320,47 +284,3 @@ func pickAliDocOSSEndpoint(sts uploadSTSSignatureInfo) (endpoint string, useCnam } return "", false } - -func (d *AliDoc) findUploadedObj(ctx context.Context, parentID, parentPath, name string, size int64, startedAt time.Time) (model.Obj, error) { - for attempt := 0; attempt < 5; attempt++ { - items, err := d.list(ctx, parentID) - if err != nil { - return nil, err - } - var ( - matched dentry - hasMatched bool - ) - for i := range items { - item := items[i] - if item.DentryType != "file" || item.Name != name { - continue - } - if size >= 0 && item.FileSize != size { - continue - } - if !hasMatched || item.UpdatedTime > matched.UpdatedTime { - matched = item - hasMatched = true - } - } - if hasMatched { - obj := toObj(parentPath, matched) - if !obj.ModTime().IsZero() && obj.ModTime().Before(startedAt.Add(-5*time.Second)) { - // Keep polling briefly if only an older homonymous file is visible. - } else { - return obj, nil - } - } - if attempt < 4 { - timer := time.NewTimer(time.Duration(attempt+1) * 300 * time.Millisecond) - select { - case <-ctx.Done(): - timer.Stop() - return nil, ctx.Err() - case <-timer.C: - } - } - } - return nil, fmt.Errorf("uploaded object not found") -} diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index be9d389e83..9e8f19a9d3 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -3,7 +3,6 @@ package alidoc import ( "context" "fmt" - "path" "strings" "time" @@ -54,28 +53,11 @@ func checkResp(resp *resty.Response, result apiResp) error { return nil } -func toObj(parentPath string, item dentry) model.Obj { - return toObjWithPath(joinPath(parentPath, item.Name), item) -} - -func toObjUsingBestPath(parentPath string, item dentry) model.Obj { - fullPath := strings.TrimSpace(item.Path) - if fullPath == "" { - fullPath = joinPath(parentPath, item.Name) - } - return toObjWithPath(fullPath, item) -} - -func toObjWithPath(fullPath string, item dentry) model.Obj { - name := item.Name - if name == "" { - name = path.Base(fullPath) - } +func toObj(item dentry) model.Obj { obj := &Object{ Object: model.Object{ ID: item.DentryUUID, - Path: fullPath, - Name: name, + Name: item.Name, Size: item.FileSize, Modified: msToTime(item.UpdatedTime), Ctime: msToTime(item.CreatedTime), From 25475b620e333b933bd2087ddb3e924f57372a0e Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Mon, 1 Jun 2026 21:03:07 +0800 Subject: [PATCH 11/20] refactor(alidoc): stream multipart uploads directly Generated with OpenAI Codex --- drivers/alidoc/upload.go | 41 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go index 6c2ac6a476..a9927efb39 100644 --- a/drivers/alidoc/upload.go +++ b/drivers/alidoc/upload.go @@ -10,6 +10,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" netutil "github.com/OpenListTeam/OpenList/v4/internal/net" + streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/google/uuid" ) @@ -57,11 +58,7 @@ func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStrea } if useMultipart && size > 0 { - src, err := prepareAliDocUploadFile(file) - if err != nil { - return nil, err - } - err = d.multipartUpload(ctx, src, size, info, up) + err = d.multipartUpload(ctx, file, size, info, up) } else { err = d.singleUpload(ctx, file, size, info, up) } @@ -74,24 +71,6 @@ func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStrea return nil, nil } -func prepareAliDocUploadFile(file model.FileStreamer) (model.File, error) { - if src := file.GetFile(); src != nil { - if _, err := src.Seek(0, io.SeekStart); err != nil { - return nil, err - } - return src, nil - } - - src, err := file.CacheFullAndWriter(nil, nil) - if err != nil { - return nil, err - } - if _, err := src.Seek(0, io.SeekStart); err != nil { - return nil, err - } - return src, nil -} - func (d *AliDoc) getUploadInfo(ctx context.Context, parentDentryUUID, name string, fileSize int64, multipart bool) (uploadInfoResp, error) { var result uploadInfoResp body := map[string]interface{}{ @@ -179,7 +158,7 @@ func (d *AliDoc) singleUpload(ctx context.Context, src model.FileStreamer, size return nil } -func (d *AliDoc) multipartUpload(ctx context.Context, src model.File, size int64, info uploadInfoResp, up driver.UpdateProgress) error { +func (d *AliDoc) multipartUpload(ctx context.Context, src model.FileStreamer, size int64, info uploadInfoResp, up driver.UpdateProgress) error { bucket, objectKey, err := d.newOSSBucket(info) if err != nil { return err @@ -193,7 +172,10 @@ func (d *AliDoc) multipartUpload(ctx context.Context, src model.File, size int64 partSize := calcAliDocPartSize(size, info.Data.FileUploadProtocolConfig.MinPartSize) partNum := int((size + partSize - 1) / partSize) parts := make([]oss.UploadPart, 0, partNum) - progress := driver.NewProgress(size, up) + ss, err := streamPkg.NewStreamSectionReader(src, int(partSize), &up) + if err != nil { + return err + } var offset int64 for partNumber := 1; partNumber <= partNum; partNumber++ { @@ -207,14 +189,19 @@ func (d *AliDoc) multipartUpload(ctx context.Context, src model.File, size int64 var part oss.UploadPart var uploadErr error + var reader io.ReadSeeker for attempt := 0; attempt < 3; attempt++ { - reader := io.NewSectionReader(src, offset, length) + reader, uploadErr = ss.GetSectionReader(offset, length) + if uploadErr != nil { + break + } part, uploadErr = bucket.UploadPart( imur, - driver.NewLimitedUploadStream(ctx, io.TeeReader(reader, progress)), + driver.NewLimitedUploadStream(ctx, reader), length, partNumber, ) + ss.FreeSectionReader(reader) if uploadErr == nil { break } From c59fca8bf95a61c1c69cb04fb50f838cced5eec1 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Mon, 1 Jun 2026 21:18:30 +0800 Subject: [PATCH 12/20] refactor(alidoc): use retry helper for multipart uploads Generated with OpenAI Codex --- drivers/alidoc/upload.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go index a9927efb39..0c91390003 100644 --- a/drivers/alidoc/upload.go +++ b/drivers/alidoc/upload.go @@ -6,12 +6,14 @@ import ( "io" "math" "strings" + "time" "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" netutil "github.com/OpenListTeam/OpenList/v4/internal/net" streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/avast/retry-go" "github.com/google/uuid" ) @@ -187,13 +189,15 @@ func (d *AliDoc) multipartUpload(ctx context.Context, src model.FileStreamer, si length = remain } + reader, err := ss.GetSectionReader(offset, length) + if err != nil { + return err + } var part oss.UploadPart var uploadErr error - var reader io.ReadSeeker - for attempt := 0; attempt < 3; attempt++ { - reader, uploadErr = ss.GetSectionReader(offset, length) - if uploadErr != nil { - break + err = retry.Do(func() error { + if _, err := reader.Seek(0, io.SeekStart); err != nil { + return err } part, uploadErr = bucket.UploadPart( imur, @@ -201,13 +205,16 @@ func (d *AliDoc) multipartUpload(ctx context.Context, src model.FileStreamer, si length, partNumber, ) - ss.FreeSectionReader(reader) - if uploadErr == nil { - break - } - } - if uploadErr != nil { return uploadErr + }, + retry.Context(ctx), + retry.Attempts(3), + retry.DelayType(retry.BackOffDelay), + retry.Delay(time.Second), + ) + ss.FreeSectionReader(reader) + if err != nil { + return err } parts = append(parts, part) offset += length From 64bd329eca14c6f2e1f134a7144959ea4603662a Mon Sep 17 00:00:00 2001 From: Unity_exe Date: Mon, 1 Jun 2026 21:21:33 +0800 Subject: [PATCH 13/20] Update drivers/alidoc/driver.go Co-authored-by: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com> Signed-off-by: Unity_exe --- drivers/alidoc/driver.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index da35e8b358..a3b95cbead 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -49,14 +49,7 @@ func (d *AliDoc) Drop(ctx context.Context) error { } func (d *AliDoc) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - parentID := d.RootFolderID - if dir != nil { - if id := strings.TrimSpace(dir.GetID()); id != "" { - parentID = id - } - } - - items, err := d.list(ctx, parentID) + items, err := d.list(ctx, dir.GetID()) if err != nil { return nil, err } From 151c63aa7c57f86c66f6b66ecc10d7b7e0f584f1 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Mon, 1 Jun 2026 21:43:36 +0800 Subject: [PATCH 14/20] refactor(alidoc): simplify write operation responses Generated with OpenAI Codex --- drivers/alidoc/driver.go | 143 +++++++++------------------------------ drivers/alidoc/util.go | 34 ++++++++-- 2 files changed, 60 insertions(+), 117 deletions(-) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index a3b95cbead..acf63ca930 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -91,37 +91,16 @@ func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin return nil, fmt.Errorf("folder name is empty") } - parentID := d.RootFolderID - if parentDir != nil { - if id := strings.TrimSpace(parentDir.GetID()); id != "" { - parentID = id - } - } - - var result apiResp - resp, err := d.request(ctx). - SetBody(map[string]string{ - "dentryType": "folder", - "name": dirName, - "parentDentryUuid": parentID, - "conflictHandleStrategy": "auto_rename", - }). - SetResult(&result). - SetError(&result). - Post(apiBase + "/box/api/v2/dentry/createfolder") + err := d.post(ctx, "/box/api/v2/dentry/createfolder", map[string]string{ + "dentryType": "folder", + "name": dirName, + "parentDentryUuid": d.parentID(parentDir), + "conflictHandleStrategy": "auto_rename", + }) if err != nil { return nil, err } - if err := checkResp(resp, result); err != nil { - return nil, err - } - return &Object{ - Object: model.Object{ - Name: dirName, - IsFolder: true, - }, - DentryType: "folder", - }, nil + return nil, nil } func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { @@ -131,8 +110,8 @@ func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, if dstDir == nil { return nil, fmt.Errorf("destination directory is nil") } - sourceID := strings.TrimSpace(srcObj.GetID()) - targetID := strings.TrimSpace(dstDir.GetID()) + sourceID := aliDocObjID(srcObj) + targetID := aliDocObjID(dstDir) if sourceID == "" { return nil, fmt.Errorf("source dentry uuid is empty") } @@ -140,22 +119,14 @@ func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, return nil, fmt.Errorf("target parent dentry uuid is empty") } - var result apiResp - resp, err := d.request(ctx). - SetBody(map[string]interface{}{ - "targetParentDentryUuid": targetID, - "sourceDentryUuid": sourceID, - "operateFrom": 1, - }). - SetResult(&result). - SetError(&result). - Post(apiBase + "/box/api/v2/dentry/move") + err := d.post(ctx, "/box/api/v2/dentry/move", map[string]interface{}{ + "targetParentDentryUuid": targetID, + "sourceDentryUuid": sourceID, + "operateFrom": 1, + }) if err != nil { return nil, err } - if err := checkResp(resp, result); err != nil { - return nil, err - } return srcObj, nil } @@ -167,38 +138,19 @@ func (d *AliDoc) Rename(ctx context.Context, srcObj model.Obj, newName string) ( if newName == "" { return nil, fmt.Errorf("new name is empty") } - dentryUUID := strings.TrimSpace(srcObj.GetID()) + dentryUUID := aliDocObjID(srcObj) if dentryUUID == "" { return nil, fmt.Errorf("dentry uuid is empty") } - var result apiResp - resp, err := d.request(ctx). - SetBody(map[string]string{ - "dentryUuid": dentryUUID, - "name": newName, - }). - SetResult(&result). - SetError(&result). - Post(apiBase + "/box/api/v2/dentry/rename") + err := d.post(ctx, "/box/api/v2/dentry/rename", map[string]string{ + "dentryUuid": dentryUUID, + "name": newName, + }) if err != nil { return nil, err } - if err := checkResp(resp, result); err != nil { - return nil, err - } - return &Object{ - Object: model.Object{ - ID: srcObj.GetID(), - Name: newName, - Size: srcObj.GetSize(), - Modified: srcObj.ModTime(), - Ctime: srcObj.CreateTime(), - IsFolder: srcObj.IsDir(), - HashInfo: srcObj.GetHash(), - }, - DentryType: pickAliDocDentryType(srcObj), - }, nil + return nil, nil } func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { @@ -208,8 +160,8 @@ func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, if dstDir == nil { return nil, fmt.Errorf("destination directory is nil") } - sourceID := strings.TrimSpace(srcObj.GetID()) - targetID := strings.TrimSpace(dstDir.GetID()) + sourceID := aliDocObjID(srcObj) + targetID := aliDocObjID(dstDir) if sourceID == "" { return nil, fmt.Errorf("source dentry uuid is empty") } @@ -217,58 +169,29 @@ func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, return nil, fmt.Errorf("target parent dentry uuid is empty") } - var result apiResp - resp, err := d.request(ctx). - SetBody(map[string]interface{}{ - "sourceDentryUuid": sourceID, - "targetParentDentryUuid": targetID, - "operateFrom": 1, - "onlyCopyMeta": false, - }). - SetResult(&result). - SetError(&result). - Post(apiBase + "/box/api/v2/dentry/copy") + err := d.post(ctx, "/box/api/v2/dentry/copy", map[string]interface{}{ + "sourceDentryUuid": sourceID, + "targetParentDentryUuid": targetID, + "operateFrom": 1, + "onlyCopyMeta": false, + }) if err != nil { return nil, err } - if err := checkResp(resp, result); err != nil { - return nil, err - } - - return &Object{ - Object: model.Object{ - Name: srcObj.GetName(), - Size: srcObj.GetSize(), - Modified: srcObj.ModTime(), - Ctime: srcObj.CreateTime(), - IsFolder: srcObj.IsDir(), - HashInfo: srcObj.GetHash(), - }, - DentryType: pickAliDocDentryType(srcObj), - }, nil + return nil, nil } func (d *AliDoc) Remove(ctx context.Context, obj model.Obj) error { if obj == nil { return fmt.Errorf("object is nil") } - dentryUUID := strings.TrimSpace(obj.GetID()) + dentryUUID := aliDocObjID(obj) if dentryUUID == "" { return fmt.Errorf("dentry uuid is empty") } - - var result apiResp - resp, err := d.request(ctx). - SetBody(map[string]string{ - "dentryUuid": dentryUUID, - }). - SetResult(&result). - SetError(&result). - Post(apiBase + "/box/api/v1/dentry/recycle") - if err != nil { - return err - } - return checkResp(resp, result) + return d.post(ctx, "/box/api/v1/dentry/recycle", map[string]string{ + "dentryUuid": dentryUUID, + }) } func (d *AliDoc) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index 9e8f19a9d3..834d4cbfab 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -22,13 +22,6 @@ func (d *AliDoc) request(ctx context.Context) *resty.Request { SetHeader("Origin", apiBase) } -func joinPath(basePath, name string) string { - if basePath == "" || basePath == "/" { - return "/" + name - } - return strings.TrimRight(basePath, "/") + "/" + name -} - func msToTime(v int64) time.Time { if v <= 0 { return time.Time{} @@ -88,6 +81,20 @@ func newClient() *resty.Client { return client } +func aliDocObjID(obj model.Obj) string { + if obj == nil { + return "" + } + return strings.TrimSpace(obj.GetID()) +} + +func (d *AliDoc) parentID(dir model.Obj) string { + if id := aliDocObjID(dir); id != "" { + return id + } + return d.RootFolderID +} + func pickAliDocDentryType(obj model.Obj) string { if o, ok := obj.(*Object); ok && strings.TrimSpace(o.DentryType) != "" { return o.DentryType @@ -98,6 +105,19 @@ func pickAliDocDentryType(obj model.Obj) string { return "file" } +func (d *AliDoc) post(ctx context.Context, path string, body interface{}) error { + var result apiResp + resp, err := d.request(ctx). + SetBody(body). + SetResult(&result). + SetError(&result). + Post(apiBase + path) + if err != nil { + return err + } + return checkResp(resp, result) +} + func (d *AliDoc) list(ctx context.Context, dentryUUID string) ([]dentry, error) { var result listResp resp, err := d.request(ctx). From 939abb95539666c84ff4833fcfc796693e1d9443 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Mon, 1 Jun 2026 21:51:24 +0800 Subject: [PATCH 15/20] fix(alidoc): update multipart upload progress Generated with OpenAI Codex --- drivers/alidoc/upload.go | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go index 0c91390003..b6c565ed15 100644 --- a/drivers/alidoc/upload.go +++ b/drivers/alidoc/upload.go @@ -217,6 +217,7 @@ func (d *AliDoc) multipartUpload(ctx context.Context, src model.FileStreamer, si return err } parts = append(parts, part) + up(100 * float64(len(parts)) / float64(partNum+1)) offset += length } From 9d441b92cf9296cc75ac316dc3b45d1a1ba9e894 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Tue, 2 Jun 2026 17:24:04 +0800 Subject: [PATCH 16/20] docs(readme): sync DingTalk Docs support across translations --- README.md | 1 + README_cn.md | 1 + README_ja.md | 1 + README_nl.md | 1 + 4 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 4a94dbfcf6..18c41369bf 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Thank you for your support and understanding of the OpenList project. - [x] [OpenList](https://github.com/OpenListTeam/OpenList) - [x] [Teldrive](https://github.com/tgdrive/teldrive) - [x] [Weiyun](https://www.weiyun.com) + - [x] [DingTalk Docs](https://alidocs.dingtalk.com/) - [x] Easy to deploy and out-of-the-box - [x] File preview (PDF, markdown, code, plain text, ...) - [x] Image preview in gallery mode diff --git a/README_cn.md b/README_cn.md index 55fe221060..88e7beed96 100644 --- a/README_cn.md +++ b/README_cn.md @@ -95,6 +95,7 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3 - [x] [OpenList](https://github.com/OpenListTeam/OpenList) - [x] [Teldrive](https://github.com/tgdrive/teldrive) - [x] [微云](https://www.weiyun.com) + - [x] [钉钉文档](https://alidocs.dingtalk.com/) - [x] 部署方便,开箱即用 - [x] 文件预览(PDF、markdown、代码、纯文本等) - [x] 画廊模式下的图片预览 diff --git a/README_ja.md b/README_ja.md index 261223de3a..8c71782340 100644 --- a/README_ja.md +++ b/README_ja.md @@ -94,6 +94,7 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい - [x] [OpenList](https://github.com/OpenListTeam/OpenList) - [x] [Teldrive](https://github.com/tgdrive/teldrive) - [x] [Weiyun](https://www.weiyun.com) + - [x] [DingTalk ドキュメント](https://alidocs.dingtalk.com/) - [x] [MediaFire](https://www.mediafire.com) - [x] 簡単にデプロイでき、すぐに使える - [x] ファイルプレビュー(PDF、markdown、コード、テキストなど) diff --git a/README_nl.md b/README_nl.md index d3be2703f9..7767df795d 100644 --- a/README_nl.md +++ b/README_nl.md @@ -95,6 +95,7 @@ Dank u voor uw ondersteuning en begrip - [x] [OpenList](https://github.com/OpenListTeam/OpenList) - [x] [Teldrive](https://github.com/tgdrive/teldrive) - [x] [Weiyun](https://www.weiyun.com) + - [x] [DingTalk-documenten](https://alidocs.dingtalk.com/) - [x] Eenvoudig te implementeren en direct te gebruiken - [x] Bestandsvoorbeeld (PDF, markdown, code, platte tekst, ...) - [x] Afbeeldingsvoorbeeld in galerijweergave From b8e1388116a08e092a6adff2556948edb9f8ebda Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Tue, 2 Jun 2026 19:42:50 +0800 Subject: [PATCH 17/20] refactor(alidoc): simplify file operations and remove redundant checks --- drivers/alidoc/driver.go | 88 ++++++---------------------------------- drivers/alidoc/types.go | 11 ----- drivers/alidoc/upload.go | 38 +++++------------ drivers/alidoc/util.go | 50 ++++------------------- 4 files changed, 30 insertions(+), 157 deletions(-) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index acf63ca930..a9dbb2fa8c 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -65,7 +65,7 @@ func (d *AliDoc) List(ctx context.Context, dir model.Obj, args model.ListArgs) ( } func (d *AliDoc) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - if file == nil || file.IsDir() { + if file.IsDir() { return nil, fmt.Errorf("alidoc does not support directory links") } resp, err := d.download(ctx, file.GetID()) @@ -86,15 +86,10 @@ func (d *AliDoc) Link(ctx context.Context, file model.Obj, args model.LinkArgs) } func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - dirName = strings.TrimSpace(dirName) - if dirName == "" { - return nil, fmt.Errorf("folder name is empty") - } - err := d.post(ctx, "/box/api/v2/dentry/createfolder", map[string]string{ "dentryType": "folder", "name": dirName, - "parentDentryUuid": d.parentID(parentDir), + "parentDentryUuid": parentDir.GetID(), "conflictHandleStrategy": "auto_rename", }) if err != nil { @@ -104,24 +99,9 @@ func (d *AliDoc) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin } func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - if srcObj == nil { - return nil, fmt.Errorf("source object is nil") - } - if dstDir == nil { - return nil, fmt.Errorf("destination directory is nil") - } - sourceID := aliDocObjID(srcObj) - targetID := aliDocObjID(dstDir) - if sourceID == "" { - return nil, fmt.Errorf("source dentry uuid is empty") - } - if targetID == "" { - return nil, fmt.Errorf("target parent dentry uuid is empty") - } - err := d.post(ctx, "/box/api/v2/dentry/move", map[string]interface{}{ - "targetParentDentryUuid": targetID, - "sourceDentryUuid": sourceID, + "targetParentDentryUuid": dstDir.GetID(), + "sourceDentryUuid": srcObj.GetID(), "operateFrom": 1, }) if err != nil { @@ -131,79 +111,37 @@ func (d *AliDoc) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, } func (d *AliDoc) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - if srcObj == nil { - return nil, fmt.Errorf("source object is nil") - } - newName = strings.TrimSpace(newName) - if newName == "" { - return nil, fmt.Errorf("new name is empty") - } - dentryUUID := aliDocObjID(srcObj) - if dentryUUID == "" { - return nil, fmt.Errorf("dentry uuid is empty") - } - err := d.post(ctx, "/box/api/v2/dentry/rename", map[string]string{ - "dentryUuid": dentryUUID, + "dentryUuid": srcObj.GetID(), "name": newName, }) if err != nil { return nil, err } - return nil, nil + srcObj.(*model.Object).Name = newName + return srcObj, nil } -func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { - if srcObj == nil { - return nil, fmt.Errorf("source object is nil") - } - if dstDir == nil { - return nil, fmt.Errorf("destination directory is nil") - } - sourceID := aliDocObjID(srcObj) - targetID := aliDocObjID(dstDir) - if sourceID == "" { - return nil, fmt.Errorf("source dentry uuid is empty") - } - if targetID == "" { - return nil, fmt.Errorf("target parent dentry uuid is empty") - } - - err := d.post(ctx, "/box/api/v2/dentry/copy", map[string]interface{}{ - "sourceDentryUuid": sourceID, - "targetParentDentryUuid": targetID, +func (d *AliDoc) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + return d.post(ctx, "/box/api/v2/dentry/copy", map[string]interface{}{ + "sourceDentryUuid": srcObj.GetID(), + "targetParentDentryUuid": dstDir.GetID(), "operateFrom": 1, "onlyCopyMeta": false, }) - if err != nil { - return nil, err - } - return nil, nil } func (d *AliDoc) Remove(ctx context.Context, obj model.Obj) error { - if obj == nil { - return fmt.Errorf("object is nil") - } - dentryUUID := aliDocObjID(obj) - if dentryUUID == "" { - return fmt.Errorf("dentry uuid is empty") - } return d.post(ctx, "/box/api/v1/dentry/recycle", map[string]string{ - "dentryUuid": dentryUUID, + "dentryUuid": obj.GetID(), }) } -func (d *AliDoc) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - return d.put(ctx, dstDir, file, up) -} - var ( _ driver.Driver = (*AliDoc)(nil) _ driver.MkdirResult = (*AliDoc)(nil) _ driver.MoveResult = (*AliDoc)(nil) _ driver.RenameResult = (*AliDoc)(nil) - _ driver.CopyResult = (*AliDoc)(nil) + _ driver.Copy = (*AliDoc)(nil) _ driver.Remove = (*AliDoc)(nil) - _ driver.PutResult = (*AliDoc)(nil) ) diff --git a/drivers/alidoc/types.go b/drivers/alidoc/types.go index 73b48d0aef..171d401a01 100644 --- a/drivers/alidoc/types.go +++ b/drivers/alidoc/types.go @@ -1,7 +1,5 @@ package alidoc -import "github.com/OpenListTeam/OpenList/v4/internal/model" - type apiResp struct { Status int `json:"status"` IsSuccess bool `json:"isSuccess"` @@ -87,12 +85,3 @@ type uploadSTSSignatureInfo struct { EndPoint string `json:"endPoint"` ObjectKey string `json:"objectKey"` } - -type Object struct { - model.Object - DentryType string - ContentType string - Extension string - PreviewURL string - EditURL string -} diff --git a/drivers/alidoc/upload.go b/drivers/alidoc/upload.go index b6c565ed15..a8c2eacb7d 100644 --- a/drivers/alidoc/upload.go +++ b/drivers/alidoc/upload.go @@ -23,54 +23,36 @@ const ( maxAliDocMultipartParts = 10000 ) -func (d *AliDoc) put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - if file == nil { - return nil, fmt.Errorf("file is nil") - } - if up == nil { - up = func(float64) {} - } - - parentID := d.RootFolderID - if dstDir != nil { - if id := strings.TrimSpace(dstDir.GetID()); id != "" { - parentID = id - } - } - +func (d *AliDoc) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error { size := file.GetSize() - if size < 0 { - return nil, fmt.Errorf("unknown file size is not supported") - } - useMultipart := size > defaultAliDocMultipartThreshold - info, err := d.getUploadInfo(ctx, parentID, file.GetName(), size, useMultipart) + info, err := d.getUploadInfo(ctx, dstDir.GetID(), file.GetName(), size, useMultipart) if err != nil { - return nil, err + return err } if size > 0 { partSize := calcAliDocPartSize(size, info.Data.FileUploadProtocolConfig.MinPartSize) if size > partSize && !useMultipart { useMultipart = true - info, err = d.getUploadInfo(ctx, parentID, file.GetName(), size, true) + info, err = d.getUploadInfo(ctx, dstDir.GetID(), file.GetName(), size, true) if err != nil { - return nil, err + return err } } } - if useMultipart && size > 0 { + if useMultipart { err = d.multipartUpload(ctx, file, size, info, up) } else { err = d.singleUpload(ctx, file, size, info, up) } if err != nil { - return nil, err + return err } - if err := d.commitUpload(ctx, parentID, file.GetName(), size, info.Data.UploadKey); err != nil { - return nil, err + if err := d.commitUpload(ctx, dstDir.GetID(), file.GetName(), size, info.Data.UploadKey); err != nil { + return err } - return nil, nil + return nil } func (d *AliDoc) getUploadInfo(ctx context.Context, parentDentryUUID, name string, fileSize int64, multipart bool) (uploadInfoResp, error) { diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index 834d4cbfab..628b2c472f 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -3,7 +3,6 @@ package alidoc import ( "context" "fmt" - "strings" "time" "github.com/OpenListTeam/OpenList/v4/drivers/base" @@ -47,25 +46,14 @@ func checkResp(resp *resty.Response, result apiResp) error { } func toObj(item dentry) model.Obj { - obj := &Object{ - Object: model.Object{ - ID: item.DentryUUID, - Name: item.Name, - Size: item.FileSize, - Modified: msToTime(item.UpdatedTime), - Ctime: msToTime(item.CreatedTime), - IsFolder: item.DentryType == "folder", - }, - DentryType: item.DentryType, - ContentType: item.ContentType, - Extension: item.Extension, - PreviewURL: item.URL.PCChildAppPreviewURL, - EditURL: item.URL.PCChildAppURL, + return &model.Object{ + ID: item.DentryUUID, + Name: item.Name, + Size: item.FileSize, + Modified: msToTime(item.UpdatedTime), + Ctime: msToTime(item.CreatedTime), + IsFolder: item.DentryType == "folder", } - if obj.IsDir() && item.DentryStatistic.ChildrenCount > 0 && obj.Size == 0 { - // Keep size as 0 for folders; childrenCount is intentionally ignored. - } - return obj } func firstDownloadURL(resp downloadResp) (string, error) { @@ -81,30 +69,6 @@ func newClient() *resty.Client { return client } -func aliDocObjID(obj model.Obj) string { - if obj == nil { - return "" - } - return strings.TrimSpace(obj.GetID()) -} - -func (d *AliDoc) parentID(dir model.Obj) string { - if id := aliDocObjID(dir); id != "" { - return id - } - return d.RootFolderID -} - -func pickAliDocDentryType(obj model.Obj) string { - if o, ok := obj.(*Object); ok && strings.TrimSpace(o.DentryType) != "" { - return o.DentryType - } - if obj != nil && obj.IsDir() { - return "folder" - } - return "file" -} - func (d *AliDoc) post(ctx context.Context, path string, body interface{}) error { var result apiResp resp, err := d.request(ctx). From 79f5d74f6146a5fff08bcbd90557119383a87d1a Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Tue, 2 Jun 2026 19:47:12 +0800 Subject: [PATCH 18/20] fix(wps): update login state handling and improve error messages --- drivers/wps/driver.go | 26 ++++++-------------------- drivers/wps/meta.go | 2 +- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/drivers/wps/driver.go b/drivers/wps/driver.go index 6e18d2110f..810048a7dc 100644 --- a/drivers/wps/driver.go +++ b/drivers/wps/driver.go @@ -38,28 +38,23 @@ func (d *Wps) Init(ctx context.Context) error { d.client = base.NewRestyClient() - resp, err := d.request(ctx).SetResult(&d.login).Get("https://account.kdocs.cn/api/v3/islogin") + d.login = &loginState{} + resp, err := d.request(ctx).SetResult(d.login).Get("https://account.kdocs.cn/api/v3/islogin") if err != nil { return err } if !resp.IsSuccess() { return fmt.Errorf("failed to check login status, status code: %d, body: %s", resp.StatusCode(), resp.String()) } - if d.login == nil { - return fmt.Errorf("wps login failed: empty login state, please check cookie") + if d.login.CompanyID == 0 { + return fmt.Errorf("wps company id is empty, please check business account login") } - return nil } func (d *Wps) Drop(ctx context.Context) error { - - if d.client != nil { - d.client = nil - } - if d.login != nil { - d.login = nil - } + d.client = nil + d.login = nil return nil } @@ -385,10 +380,6 @@ func (d *Wps) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer } func (d *Wps) GetDetails(ctx context.Context) (*model.StorageDetails, error) { - if d.login == nil { - return nil, fmt.Errorf("wps login state is nil, please reinitialize or check cookie") - } - if d.isPersonal() { url := ENDPOINT_PERSONAL + "/api/v3/spaces" var resp spacesResp @@ -406,11 +397,6 @@ func (d *Wps) GetDetails(ctx context.Context) (*model.StorageDetails, error) { }, }, nil } - - if d.login.CompanyID == 0 { - return nil, fmt.Errorf("wps company id is empty, please check business account login") - } - url := ENDPOINT_BUSINESS + "/3rd/plussvr/compose/v1/u/companies/batch/service-space?comp_ids=" + fmt.Sprint(d.login.CompanyID) var resp serviceSpaceResp r, err := d.request(ctx).SetResult(&resp).SetError(&resp).Get(url) diff --git a/drivers/wps/meta.go b/drivers/wps/meta.go index a520bb4331..1befb7cd0f 100644 --- a/drivers/wps/meta.go +++ b/drivers/wps/meta.go @@ -16,7 +16,7 @@ var config = driver.Config{ Name: "WPS", LocalSort: true, DefaultRoot: "/", - Alert: "", + CheckStatus: true, } func init() { From 6190dab7e7ed419bccdf81f11e311176541fd7b5 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Tue, 2 Jun 2026 20:13:47 +0800 Subject: [PATCH 19/20] fix(alidoc): validate cookie with mine info endpoint Generated with OpenAI Codex --- drivers/alidoc/driver.go | 3 +++ drivers/alidoc/util.go | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index a9dbb2fa8c..c38901e68f 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -37,6 +37,9 @@ func (d *AliDoc) Init(ctx context.Context) error { return fmt.Errorf("root folder id is empty") } d.client = newClient() + if err := d.checkCookie(ctx); err != nil { + return err + } if _, err := d.list(ctx, d.RootFolderID); err != nil { return err } diff --git a/drivers/alidoc/util.go b/drivers/alidoc/util.go index 628b2c472f..5e7d8cfa66 100644 --- a/drivers/alidoc/util.go +++ b/drivers/alidoc/util.go @@ -82,6 +82,18 @@ func (d *AliDoc) post(ctx context.Context, path string, body interface{}) error return checkResp(resp, result) } +func (d *AliDoc) checkCookie(ctx context.Context) error { + var result apiResp + resp, err := d.request(ctx). + SetResult(&result). + SetError(&result). + Get(apiBase + "/portal/api/v1/mine/info") + if err != nil { + return err + } + return checkResp(resp, result) +} + func (d *AliDoc) list(ctx context.Context, dentryUUID string) ([]dentry, error) { var result listResp resp, err := d.request(ctx). From 6ea7d886ef8ecc63d0b4d8e5b065b85bcb3b3eb3 Mon Sep 17 00:00:00 2001 From: zhangjiahuichenxi Date: Tue, 2 Jun 2026 20:18:11 +0800 Subject: [PATCH 20/20] fix(alidoc): skip root folder listing during init --- drivers/alidoc/driver.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/drivers/alidoc/driver.go b/drivers/alidoc/driver.go index c38901e68f..3d1a351125 100644 --- a/drivers/alidoc/driver.go +++ b/drivers/alidoc/driver.go @@ -40,9 +40,6 @@ func (d *AliDoc) Init(ctx context.Context) error { if err := d.checkCookie(ctx); err != nil { return err } - if _, err := d.list(ctx, d.RootFolderID); err != nil { - return err - } return nil }