Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d06f8b1
feat(drivers/alidoc): add DingTalk Docs driver
zjhcx May 30, 2026
3dfa30c
chore(drivers/alidoc): update metadata copy
zjhcx May 30, 2026
957d341
chore(drivers/alidoc): update read-only alert copy
zjhcx May 30, 2026
fc36dca
chore(gitignore): remove alidoc ignore rule
zjhcx May 30, 2026
640912f
feat(alidoc): support upload move and recycle delete
zjhcx May 31, 2026
c1205eb
fix(alidoc): commit uploaded files after oss transfer
zjhcx May 31, 2026
4330be2
feat(alidoc): support mkdir copy and rename
zjhcx May 31, 2026
08b031e
chore(alidoc): remove unused readonly helper
zjhcx May 31, 2026
b73616e
chore(alidoc): remove driver alert copy
zjhcx May 31, 2026
a4b78d0
Merge branch 'OpenListTeam:main' into main
zjhcx Jun 1, 2026
5242f65
refactor(alidoc): simplify id-based driver behavior
zjhcx Jun 1, 2026
25475b6
refactor(alidoc): stream multipart uploads directly
zjhcx Jun 1, 2026
c59fca8
refactor(alidoc): use retry helper for multipart uploads
zjhcx Jun 1, 2026
64bd329
Update drivers/alidoc/driver.go
zjhcx Jun 1, 2026
151c63a
refactor(alidoc): simplify write operation responses
zjhcx Jun 1, 2026
939abb9
fix(alidoc): update multipart upload progress
zjhcx Jun 1, 2026
9d441b9
docs(readme): sync DingTalk Docs support across translations
zjhcx Jun 2, 2026
b8e1388
refactor(alidoc): simplify file operations and remove redundant checks
j2rong4cn Jun 2, 2026
79f5d74
fix(wps): update login state handling and improve error messages
j2rong4cn Jun 2, 2026
6190dab
fix(alidoc): validate cookie with mine info endpoint
zjhcx Jun 2, 2026
6ea7d88
fix(alidoc): skip root folder listing during init
zjhcx Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ output/
/public/dist/*
/!public/dist/README.md

.VSCodeCounter
.VSCodeCounter
Comment thread
xrgzs marked this conversation as resolved.
Outdated

/alidoc/
131 changes: 131 additions & 0 deletions drivers/alidoc/driver.go
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
xrgzs marked this conversation as resolved.
Outdated
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
}
}
Comment thread
xrgzs marked this conversation as resolved.
Outdated

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)
)
24 changes: 24 additions & 0 deletions drivers/alidoc/meta.go
Original file line number Diff line number Diff line change
@@ -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:"钉钉文档网页 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.",
}

func init() {
op.RegisterDriver(func() driver.Driver {
return &AliDoc{}
})
}
67 changes: 67 additions & 0 deletions drivers/alidoc/types.go
Original file line number Diff line number Diff line change
@@ -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
}
135 changes: 135 additions & 0 deletions drivers/alidoc/util.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading