Skip to content

feat(drivers/alidoc): add DingTalk Docs driver#2543

Open
zjhcx wants to merge 21 commits into
OpenListTeam:mainfrom
zjhcx:main
Open

feat(drivers/alidoc): add DingTalk Docs driver#2543
zjhcx wants to merge 21 commits into
OpenListTeam:mainfrom
zjhcx:main

Conversation

@zjhcx
Copy link
Copy Markdown

@zjhcx zjhcx commented May 30, 2026

Summary / 摘要

Add a new read-only AliDoc driver for accessing DingDrive through DingTalk Docs, and harden the WPS driver against empty login-state responses so initialization and details queries fail with clear errors instead of unstable behavior.

新增 AliDoc 驱动,用于通过钉钉文档访问钉盘,支持列出文件、查看文件、删除文件、移动文件、上传文件、创建文件夹、复制文件和重命名文件;同时增强 WPS 驱动在登录状态为空时的错误处理,使初始化和空间信息查询返回明确错误,而不是产生不稳定行为。

  • Add drivers/alidoc with Cookie-based initialization, folder listing, and file download link resolution.

  • Register the new driver in drivers/all.go and ignore local alidoc/ reverse-engineering materials.

  • Return user-friendly errors in WPS when login state or business company id is missing.

  • Related frontend changes are needed to add AliDoc i18n entries and alert copy.

  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。

  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。

  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR:

Testing / 测试

  • go test ./drivers/alidoc
  • go test ./drivers
  • Manual test / 手动测试:
    可正常列出、查看、删除、移动、上传、复制和重命名文本、图片、视频和PDF。可正常创建和移动文件夹

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明):

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Refactoring / 重构

  • Documentation / 文档

  • Tests / 测试

  • Translation / 翻译

  • Review assistance / 审查辅助

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

Comment thread .gitignore Outdated
Generated with OpenAI Codex
Copy link
Copy Markdown
Member

@jyxjjj jyxjjj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请不要随意篡改PR模板
请撤回gitignore的改动
请仔细阅读Git官方文档有关如何仅在本地忽略内容而无需将忽略行为推送至仓库的部分

@zjhcx
Copy link
Copy Markdown
Author

zjhcx commented May 30, 2026

请不要随意篡改PR模板 请撤回gitignore的改动 请仔细阅读Git官方文档有关如何仅在本地忽略内容而无需将忽略行为推送至仓库的部分

已撤回gitignore的改动

@xrgzs xrgzs changed the title [feat] Add DingTalk documentation and update text feat(drivers/alidoc): add DingTalk Docs driver May 31, 2026
@zjhcx zjhcx requested a review from jyxjjj May 31, 2026 08:37
@zjhcx zjhcx requested a review from xrgzs May 31, 2026 11:23
Comment thread drivers/alidoc/meta.go Outdated
Comment on lines +17 to +18
Alert: "info|This driver supports accessing DingDrive through DingTalk Docs, including listing, download, upload, move, and recycle delete.",
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以删了

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已删除

@xrgzs
Copy link
Copy Markdown
Member

xrgzs commented May 31, 2026

@zjhcx 麻烦增加文档到OpenList-Docs,前端的翻译可以不用管,会自动生成

@zjhcx
Copy link
Copy Markdown
Author

zjhcx commented May 31, 2026

@zjhcx 麻烦增加文档到OpenList-Docs,前端的翻译可以不用管,会自动生成

我在写文档

@zjhcx
Copy link
Copy Markdown
Author

zjhcx commented Jun 1, 2026

@zjhcx 麻烦增加文档到OpenList-Docs,前端的翻译可以不用管,会自动生成

已添加文档

@zjhcx zjhcx requested a review from xrgzs June 1, 2026 08:42
Comment thread drivers/alidoc/driver.go Outdated
Comment on lines +54 to +62
parentPath := "/"
if dir != nil {
if id := strings.TrimSpace(dir.GetID()); id != "" {
parentID = id
}
if p := dir.GetPath(); p != "" {
parentPath = p
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个也可以删了,整个驱动都是用ID的,Path就不要设置了

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好,我改改

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已删除

Comment thread drivers/alidoc/upload.go Outdated
Comment on lines +324 to +366
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")
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个也建议删掉,如果文件上传api没返回 文件信息,可以直接返回nil,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我看看

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已删除

Comment thread drivers/alidoc/upload.go Outdated
return src, size, nil
}

src, err := file.CacheFullAndWriter(nil, nil)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

流上传不需要计算校验值就不要缓存,可以节省资源

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修改

@zjhcx zjhcx requested a review from j2rong4cn June 1, 2026 12:43
Comment thread drivers/alidoc/upload.go Outdated
Comment on lines +77 to +93
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
}
Copy link
Copy Markdown
Member

@j2rong4cn j2rong4cn Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个删掉,传file给multipartUpload,在multipartUpload使用stream.NewStreamSectionReader顺序读取分片,参考
单线程分片上传 stream.NewStreamSectionReader + retry.Do

  • func (d *GoogleDrive) chunkUpload(ctx context.Context, file model.FileStreamer, url string, up driver.UpdateProgress) error {
    defaultChunkSize := d.ChunkSize * 1024 * 1024
    ss, err := stream.NewStreamSectionReader(file, int(defaultChunkSize), &up)
    if err != nil {
    return err
    }
    var offset int64 = 0
    url += "?includeItemsFromAllDrives=true&supportsAllDrives=true"
    for offset < file.GetSize() {
    if utils.IsCanceled(ctx) {
    return ctx.Err()
    }
    chunkSize := min(file.GetSize()-offset, defaultChunkSize)
    reader, err := ss.GetSectionReader(offset, chunkSize)
    if err != nil {
    return err
    }
    limitedReader := driver.NewLimitedUploadStream(ctx, reader)
    err = retry.Do(func() error {
    reader.Seek(0, io.SeekStart)
    req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, limitedReader)
    if err != nil {
    return err
    }
    req.Header = map[string][]string{
    "Authorization": {"Bearer " + d.AccessToken},
    "Content-Length": {strconv.FormatInt(chunkSize, 10)},
    "Content-Range": {fmt.Sprintf("bytes %d-%d/%d", offset, offset+chunkSize-1, file.GetSize())},
    }
    res, err := base.HttpClient.Do(req)
    if err != nil {
    return err
    }
    defer res.Body.Close()
    bytes, _ := io.ReadAll(res.Body)
    var e Error
    utils.Json.Unmarshal(bytes, &e)
    if e.Error.Code != 0 {
    if e.Error.Code == 401 {
    err = d.refreshToken()
    if err != nil {
    return err
    }
    }
    return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
    }
    up(float64(offset+chunkSize) / float64(file.GetSize()) * 100)
    return nil
    },
    retry.Context(ctx),
    retry.Attempts(3),
    retry.DelayType(retry.BackOffDelay),
    retry.Delay(time.Second))
    ss.FreeSectionReader(reader)
    if err != nil {
    return err
    }
    offset += chunkSize
    }
    return nil
    }

多线程分片上传 stream.NewStreamSectionReader + errgroup.NewOrderedGroupWithContext

  • func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, up driver.UpdateProgress) error {
    // fetch s3 pre signed urls
    size := file.GetSize()
    chunkSize := int64(16 * utils.MB)
    chunkCount := 1
    if size > chunkSize {
    chunkCount = int((size + chunkSize - 1) / chunkSize)
    }
    ss, err := stream.NewStreamSectionReader(file, int(chunkSize), &up)
    if err != nil {
    return err
    }
    lastChunkSize := size % chunkSize
    if lastChunkSize == 0 {
    lastChunkSize = chunkSize
    }
    // only 1 batch is allowed
    batchSize := 1
    getS3UploadUrl := d.getS3Auth
    if chunkCount > 1 {
    batchSize = 10
    getS3UploadUrl = d.getS3PreSignedUrls
    }
    thread := min(int(chunkCount), d.UploadThread)
    threadG, uploadCtx := errgroup.NewOrderedGroupWithContext(ctx, thread,
    retry.Attempts(3),
    retry.Delay(time.Second),
    retry.DelayType(retry.BackOffDelay))
    for i := 1; i <= chunkCount; i += batchSize {
    if utils.IsCanceled(uploadCtx) {
    break
    }
    start := i
    end := min(i+batchSize, chunkCount+1)
    s3PreSignedUrls, err := getS3UploadUrl(uploadCtx, upReq, start, end)
    if err != nil {
    return err
    }
    // upload each chunk
    for cur := start; cur < end; cur++ {
    if utils.IsCanceled(uploadCtx) {
    break
    }
    offset := int64(cur-1) * chunkSize
    curSize := chunkSize
    if cur == chunkCount {
    curSize = lastChunkSize
    }
    var reader io.ReadSeeker
    threadG.GoWithLifecycle(errgroup.Lifecycle{
    Before: func(ctx context.Context) (err error) {
    reader, err = ss.GetSectionReader(offset, curSize)
    return
    },
    Do: func(ctx context.Context) (err error) {
    reader.Seek(0, io.SeekStart)
    uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
    if uploadUrl == "" {
    return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
    }
    req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, reader))
    if err != nil {
    return err
    }
    req.ContentLength = curSize
    //req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
    res, err := base.HttpClient.Do(req)
    if err != nil {
    return err
    }
    defer res.Body.Close()
    if res.StatusCode == http.StatusForbidden {
    _, err, _ = singleflight.AnyGroup.Do(fmt.Sprintf("Pan123.newUpload_%p", threadG), func() (any, error) {
    newS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)
    if err != nil {
    return nil, err
    }
    s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
    return nil, nil
    })
    if err != nil {
    return err
    }
    return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
    }
    if res.StatusCode != http.StatusOK {
    body, err := io.ReadAll(res.Body)
    if err != nil {
    return err
    }
    return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
    }
    progress := 100 * float64(threadG.Success()+1) / float64(chunkCount+1)
    up(progress)
    return nil
    },
    After: func(err error) {
    ss.FreeSectionReader(reader)
    },
    })
    }
    }
    if err := threadG.Wait(); err != nil {
    return err
    }
    defer up(100)
    // complete s3 upload
    return d.completeS3(ctx, upReq, file, chunkCount > 1)
    }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修改

Comment thread drivers/alidoc/upload.go Outdated
var uploadErr error
var reader io.ReadSeeker
for attempt := 0; attempt < 3; attempt++ {
reader, uploadErr = ss.GetSectionReader(offset, length)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个往上移一行,这一行改为reader.Seek

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修改

Comment thread drivers/alidoc/driver.go Outdated
Co-authored-by: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com>
Signed-off-by: Unity_exe <zjhcx12@gmail.com>
@zjhcx zjhcx requested a review from j2rong4cn June 1, 2026 13:29
@j2rong4cn
Copy link
Copy Markdown
Member

其他的代码也简化一下,只要确保List返回的Obj正确即可,其他的方法就不要写过多的判断逻辑了

Comment thread drivers/alidoc/driver.go Outdated
Comment on lines +238 to +248
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里获取不到新文件ID直接返回nil即可

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

改了

Comment thread drivers/alidoc/driver.go Outdated
Comment on lines +118 to +124
return &Object{
Object: model.Object{
Name: dirName,
IsFolder: true,
},
DentryType: "folder",
}, nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

还有这里

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修改

Comment thread drivers/alidoc/upload.go
return err
}
parts = append(parts, part)
offset += length
Copy link
Copy Markdown
Member

@j2rong4cn j2rong4cn Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里少了更新进度UpdateProgress,可以参考我上面发的GoogleDrive的例子

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

等我改完返回值改这个

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

改了

@zjhcx zjhcx requested a review from j2rong4cn June 1, 2026 13:54
Comment thread drivers/alidoc/driver.go Outdated
return fmt.Errorf("root folder id is empty")
}
d.client = newClient()
if _, err := d.list(ctx, d.RootFolderID); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zjhcx 还剩一个槽点就是使用list检测cookie是否有效,你试一下能不能抓到获取用户信息之类的,不需要返回太多数据的api,用来检测cookie的有效性

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好,我找找

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zjhcx 还剩一个槽点就是使用list检测cookie是否有效,你试一下能不能抓到获取用户信息之类的,不需要返回太多数据的api,用来检测cookie的有效性

已改成获取用户的个人信息(https://alidocs.dingtalk.com/portal/api/v1/mine/info)

@zjhcx zjhcx requested a review from j2rong4cn June 2, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants