Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/doubao_share/util.go
1986 views
1
package doubao_share
2
3
import (
4
"context"
5
"encoding/json"
6
"fmt"
7
"github.com/alist-org/alist/v3/drivers/base"
8
"github.com/alist-org/alist/v3/internal/model"
9
"github.com/go-resty/resty/v2"
10
log "github.com/sirupsen/logrus"
11
"net/http"
12
"net/url"
13
"path"
14
"regexp"
15
"strings"
16
"time"
17
)
18
19
const (
20
DirectoryType = 1
21
FileType = 2
22
LinkType = 3
23
ImageType = 4
24
PagesType = 5
25
VideoType = 6
26
AudioType = 7
27
MeetingMinutesType = 8
28
)
29
30
var FileNodeType = map[int]string{
31
1: "directory",
32
2: "file",
33
3: "link",
34
4: "image",
35
5: "pages",
36
6: "video",
37
7: "audio",
38
8: "meeting_minutes",
39
}
40
41
const (
42
BaseURL = "https://www.doubao.com"
43
FileDataType = "file"
44
ImgDataType = "image"
45
VideoDataType = "video"
46
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
47
)
48
49
func (d *DoubaoShare) request(path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
50
reqUrl := BaseURL + path
51
req := base.RestyClient.R()
52
53
req.SetHeaders(map[string]string{
54
"Cookie": d.Cookie,
55
"User-Agent": UserAgent,
56
})
57
58
req.SetQueryParams(map[string]string{
59
"version_code": "20800",
60
"device_platform": "web",
61
})
62
63
if callback != nil {
64
callback(req)
65
}
66
67
var commonResp CommonResp
68
69
res, err := req.Execute(method, reqUrl)
70
log.Debugln(res.String())
71
if err != nil {
72
return nil, err
73
}
74
75
body := res.Body()
76
// 先解析为通用响应
77
if err = json.Unmarshal(body, &commonResp); err != nil {
78
return nil, err
79
}
80
// 检查响应是否成功
81
if !commonResp.IsSuccess() {
82
return body, commonResp.GetError()
83
}
84
85
if resp != nil {
86
if err = json.Unmarshal(body, resp); err != nil {
87
return body, err
88
}
89
}
90
91
return body, nil
92
}
93
94
func (d *DoubaoShare) getFiles(dirId, nodeId, cursor string) (resp []File, err error) {
95
var r NodeInfoResp
96
97
var body = base.Json{
98
"share_id": dirId,
99
"node_id": nodeId,
100
}
101
// 如果有游标,则设置游标和大小
102
if cursor != "" {
103
body["cursor"] = cursor
104
body["size"] = 50
105
} else {
106
body["need_full_path"] = false
107
}
108
109
_, err = d.request("/samantha/aispace/share/node_info", http.MethodPost, func(req *resty.Request) {
110
req.SetBody(body)
111
}, &r)
112
if err != nil {
113
return nil, err
114
}
115
116
if r.NodeInfoData.Children != nil {
117
resp = r.NodeInfoData.Children
118
}
119
120
if r.NodeInfoData.NextCursor != "-1" {
121
// 递归获取下一页
122
nextFiles, err := d.getFiles(dirId, nodeId, r.NodeInfoData.NextCursor)
123
if err != nil {
124
return nil, err
125
}
126
127
resp = append(r.NodeInfoData.Children, nextFiles...)
128
}
129
130
return resp, err
131
}
132
133
func (d *DoubaoShare) getShareOverview(shareId, cursor string) (resp []File, err error) {
134
return d.getShareOverviewWithHistory(shareId, cursor, make(map[string]bool))
135
}
136
137
func (d *DoubaoShare) getShareOverviewWithHistory(shareId, cursor string, cursorHistory map[string]bool) (resp []File, err error) {
138
var r NodeInfoResp
139
140
var body = base.Json{
141
"share_id": shareId,
142
}
143
// 如果有游标,则设置游标和大小
144
if cursor != "" {
145
body["cursor"] = cursor
146
body["size"] = 50
147
} else {
148
body["need_full_path"] = false
149
}
150
151
_, err = d.request("/samantha/aispace/share/overview", http.MethodPost, func(req *resty.Request) {
152
req.SetBody(body)
153
}, &r)
154
if err != nil {
155
return nil, err
156
}
157
158
if r.NodeInfoData.NodeList != nil {
159
resp = r.NodeInfoData.NodeList
160
}
161
162
if r.NodeInfoData.NextCursor != "-1" {
163
// 检查游标是否重复出现,防止无限循环
164
if cursorHistory[r.NodeInfoData.NextCursor] {
165
return resp, nil
166
}
167
168
// 记录当前游标
169
cursorHistory[r.NodeInfoData.NextCursor] = true
170
171
// 递归获取下一页
172
nextFiles, err := d.getShareOverviewWithHistory(shareId, r.NodeInfoData.NextCursor, cursorHistory)
173
if err != nil {
174
return nil, err
175
}
176
177
resp = append(resp, nextFiles...)
178
}
179
180
return resp, nil
181
}
182
183
func (d *DoubaoShare) initShareList() error {
184
if d.Addition.ShareIds == "" {
185
return fmt.Errorf("share_ids is empty")
186
}
187
188
// 解析分享配置
189
shareConfigs, rootShares, err := d._parseShareConfigs()
190
if err != nil {
191
return err
192
}
193
194
// 检查路径冲突
195
if err := d._detectPathConflicts(shareConfigs); err != nil {
196
return err
197
}
198
199
// 构建树形结构
200
rootMap := d._buildTreeStructure(shareConfigs, rootShares)
201
202
// 提取顶级节点
203
topLevelNodes := d._extractTopLevelNodes(rootMap, rootShares)
204
if len(topLevelNodes) == 0 {
205
return fmt.Errorf("no valid share_ids found")
206
}
207
208
// 存储结果
209
d.RootFiles = topLevelNodes
210
211
return nil
212
}
213
214
// 从配置中解析分享ID和路径
215
func (d *DoubaoShare) _parseShareConfigs() (map[string]string, []string, error) {
216
shareConfigs := make(map[string]string) // 路径 -> 分享ID
217
rootShares := make([]string, 0) // 根目录显示的分享ID
218
219
lines := strings.Split(strings.TrimSpace(d.Addition.ShareIds), "\n")
220
if len(lines) == 0 {
221
return nil, nil, fmt.Errorf("no share_ids found")
222
}
223
224
for _, line := range lines {
225
line = strings.TrimSpace(line)
226
if line == "" {
227
continue
228
}
229
230
// 解析分享ID和路径
231
parts := strings.Split(line, "|")
232
var shareId, sharePath string
233
234
if len(parts) == 1 {
235
// 无路径分享,直接在根目录显示
236
shareId = _extractShareId(parts[0])
237
if shareId != "" {
238
rootShares = append(rootShares, shareId)
239
}
240
continue
241
} else if len(parts) >= 2 {
242
shareId = _extractShareId(parts[0])
243
sharePath = strings.Trim(parts[1], "/")
244
}
245
246
if shareId == "" {
247
log.Warnf("[doubao_share] Invalid Share_id Format: %s", line)
248
continue
249
}
250
251
// 空路径也加入根目录显示
252
if sharePath == "" {
253
rootShares = append(rootShares, shareId)
254
continue
255
}
256
257
// 添加到路径映射
258
shareConfigs[sharePath] = shareId
259
}
260
261
return shareConfigs, rootShares, nil
262
}
263
264
// 检测路径冲突
265
func (d *DoubaoShare) _detectPathConflicts(shareConfigs map[string]string) error {
266
// 检查直接路径冲突
267
pathToShareIds := make(map[string][]string)
268
for sharePath, id := range shareConfigs {
269
pathToShareIds[sharePath] = append(pathToShareIds[sharePath], id)
270
}
271
272
for sharePath, ids := range pathToShareIds {
273
if len(ids) > 1 {
274
return fmt.Errorf("路径冲突: 路径 '%s' 被多个不同的分享ID使用: %s",
275
sharePath, strings.Join(ids, ", "))
276
}
277
}
278
279
// 检查层次冲突
280
for path1, id1 := range shareConfigs {
281
for path2, id2 := range shareConfigs {
282
if path1 == path2 || id1 == id2 {
283
continue
284
}
285
286
// 检查前缀冲突
287
if strings.HasPrefix(path2, path1+"/") || strings.HasPrefix(path1, path2+"/") {
288
return fmt.Errorf("路径冲突: 路径 '%s' (ID: %s) 与路径 '%s' (ID: %s) 存在层次冲突",
289
path1, id1, path2, id2)
290
}
291
}
292
}
293
294
return nil
295
}
296
297
// 构建树形结构
298
func (d *DoubaoShare) _buildTreeStructure(shareConfigs map[string]string, rootShares []string) map[string]*RootFileList {
299
rootMap := make(map[string]*RootFileList)
300
301
// 添加所有分享节点
302
for sharePath, shareId := range shareConfigs {
303
children := make([]RootFileList, 0)
304
rootMap[sharePath] = &RootFileList{
305
ShareID: shareId,
306
VirtualPath: sharePath,
307
NodeInfo: NodeInfoData{},
308
Child: &children,
309
}
310
}
311
312
// 构建父子关系
313
for sharePath, node := range rootMap {
314
if sharePath == "" {
315
continue
316
}
317
318
pathParts := strings.Split(sharePath, "/")
319
if len(pathParts) > 1 {
320
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
321
322
// 确保所有父级路径都已创建
323
_ensurePathExists(rootMap, parentPath)
324
325
// 添加当前节点到父节点
326
if parent, exists := rootMap[parentPath]; exists {
327
*parent.Child = append(*parent.Child, *node)
328
}
329
}
330
}
331
332
return rootMap
333
}
334
335
// 提取顶级节点
336
func (d *DoubaoShare) _extractTopLevelNodes(rootMap map[string]*RootFileList, rootShares []string) []RootFileList {
337
var topLevelNodes []RootFileList
338
339
// 添加根目录分享
340
for _, shareId := range rootShares {
341
children := make([]RootFileList, 0)
342
topLevelNodes = append(topLevelNodes, RootFileList{
343
ShareID: shareId,
344
VirtualPath: "",
345
NodeInfo: NodeInfoData{},
346
Child: &children,
347
})
348
}
349
350
// 添加顶级目录
351
for rootPath, node := range rootMap {
352
if rootPath == "" {
353
continue
354
}
355
356
isTopLevel := true
357
pathParts := strings.Split(rootPath, "/")
358
359
if len(pathParts) > 1 {
360
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
361
if _, exists := rootMap[parentPath]; exists {
362
isTopLevel = false
363
}
364
}
365
366
if isTopLevel {
367
topLevelNodes = append(topLevelNodes, *node)
368
}
369
}
370
371
return topLevelNodes
372
}
373
374
// 确保路径存在,创建所有必要的中间节点
375
func _ensurePathExists(rootMap map[string]*RootFileList, path string) {
376
if path == "" {
377
return
378
}
379
380
// 如果路径已存在,不需要再处理
381
if _, exists := rootMap[path]; exists {
382
return
383
}
384
385
// 创建当前路径节点
386
children := make([]RootFileList, 0)
387
rootMap[path] = &RootFileList{
388
ShareID: "",
389
VirtualPath: path,
390
NodeInfo: NodeInfoData{},
391
Child: &children,
392
}
393
394
// 处理父路径
395
pathParts := strings.Split(path, "/")
396
if len(pathParts) > 1 {
397
parentPath := strings.Join(pathParts[:len(pathParts)-1], "/")
398
399
// 确保父路径存在
400
_ensurePathExists(rootMap, parentPath)
401
402
// 将当前节点添加为父节点的子节点
403
if parent, exists := rootMap[parentPath]; exists {
404
*parent.Child = append(*parent.Child, *rootMap[path])
405
}
406
}
407
}
408
409
// _extractShareId 从URL或直接ID中提取分享ID
410
func _extractShareId(input string) string {
411
input = strings.TrimSpace(input)
412
if strings.HasPrefix(input, "http") {
413
regex := regexp.MustCompile(`/drive/s/([a-zA-Z0-9]+)`)
414
if matches := regex.FindStringSubmatch(input); len(matches) > 1 {
415
return matches[1]
416
}
417
return ""
418
}
419
return input // 直接返回ID
420
}
421
422
// _findRootFileByShareID 查找指定ShareID的配置
423
func _findRootFileByShareID(rootFiles []RootFileList, shareID string) *RootFileList {
424
for i, rf := range rootFiles {
425
if rf.ShareID == shareID {
426
return &rootFiles[i]
427
}
428
if rf.Child != nil && len(*rf.Child) > 0 {
429
if found := _findRootFileByShareID(*rf.Child, shareID); found != nil {
430
return found
431
}
432
}
433
}
434
return nil
435
}
436
437
// _findNodeByPath 查找指定路径的节点
438
func _findNodeByPath(rootFiles []RootFileList, path string) *RootFileList {
439
for i, rf := range rootFiles {
440
if rf.VirtualPath == path {
441
return &rootFiles[i]
442
}
443
if rf.Child != nil && len(*rf.Child) > 0 {
444
if found := _findNodeByPath(*rf.Child, path); found != nil {
445
return found
446
}
447
}
448
}
449
return nil
450
}
451
452
// _findShareByPath 根据路径查找分享和相对路径
453
func _findShareByPath(rootFiles []RootFileList, path string) (*RootFileList, string) {
454
// 完全匹配或子路径匹配
455
for i, rf := range rootFiles {
456
if rf.VirtualPath == path {
457
return &rootFiles[i], ""
458
}
459
460
if rf.VirtualPath != "" && strings.HasPrefix(path, rf.VirtualPath+"/") {
461
relPath := strings.TrimPrefix(path, rf.VirtualPath+"/")
462
463
// 先检查子节点
464
if rf.Child != nil && len(*rf.Child) > 0 {
465
if child, childPath := _findShareByPath(*rf.Child, path); child != nil {
466
return child, childPath
467
}
468
}
469
470
return &rootFiles[i], relPath
471
}
472
473
// 递归检查子节点
474
if rf.Child != nil && len(*rf.Child) > 0 {
475
if child, childPath := _findShareByPath(*rf.Child, path); child != nil {
476
return child, childPath
477
}
478
}
479
}
480
481
// 检查根目录分享
482
for i, rf := range rootFiles {
483
if rf.VirtualPath == "" && rf.ShareID != "" {
484
parts := strings.SplitN(path, "/", 2)
485
if len(parts) > 0 && parts[0] == rf.ShareID {
486
if len(parts) > 1 {
487
return &rootFiles[i], parts[1]
488
}
489
return &rootFiles[i], ""
490
}
491
}
492
}
493
494
return nil, ""
495
}
496
497
// _findShareAndPath 根据给定路径查找对应的ShareID和相对路径
498
func (d *DoubaoShare) _findShareAndPath(dir model.Obj) (string, string, error) {
499
dirPath := dir.GetPath()
500
501
// 如果是根目录,返回空值表示需要列出所有分享
502
if dirPath == "/" || dirPath == "" {
503
return "", "", nil
504
}
505
506
// 检查是否是 FileObject 类型,并获取 ShareID
507
if fo, ok := dir.(*FileObject); ok && fo.ShareID != "" {
508
// 直接使用对象中存储的 ShareID
509
// 计算相对路径(移除前导斜杠)
510
relativePath := strings.TrimPrefix(dirPath, "/")
511
512
// 递归查找对应的 RootFile
513
found := _findRootFileByShareID(d.RootFiles, fo.ShareID)
514
if found != nil {
515
if found.VirtualPath != "" {
516
// 如果此分享配置了路径前缀,需要考虑相对路径的计算
517
if strings.HasPrefix(relativePath, found.VirtualPath) {
518
return fo.ShareID, strings.TrimPrefix(relativePath, found.VirtualPath+"/"), nil
519
}
520
}
521
return fo.ShareID, relativePath, nil
522
}
523
524
// 如果找不到对应的 RootFile 配置,仍然使用对象中的 ShareID
525
return fo.ShareID, relativePath, nil
526
}
527
528
// 移除开头的斜杠
529
cleanPath := strings.TrimPrefix(dirPath, "/")
530
531
// 先检查是否有直接匹配的根目录分享
532
for _, rootFile := range d.RootFiles {
533
if rootFile.VirtualPath == "" && rootFile.ShareID != "" {
534
// 检查是否匹配当前路径的第一部分
535
parts := strings.SplitN(cleanPath, "/", 2)
536
if len(parts) > 0 && parts[0] == rootFile.ShareID {
537
if len(parts) > 1 {
538
return rootFile.ShareID, parts[1], nil
539
}
540
return rootFile.ShareID, "", nil
541
}
542
}
543
}
544
545
// 查找匹配此路径的分享或虚拟目录
546
share, relPath := _findShareByPath(d.RootFiles, cleanPath)
547
if share != nil {
548
return share.ShareID, relPath, nil
549
}
550
551
log.Warnf("[doubao_share] No matching share path found: %s", dirPath)
552
return "", "", fmt.Errorf("no matching share path found: %s", dirPath)
553
}
554
555
// convertToFileObject 将File转换为FileObject
556
func (d *DoubaoShare) convertToFileObject(file File, shareId string, relativePath string) *FileObject {
557
// 构建文件对象
558
obj := &FileObject{
559
Object: model.Object{
560
ID: file.ID,
561
Name: file.Name,
562
Size: file.Size,
563
Modified: time.Unix(file.UpdateTime, 0),
564
Ctime: time.Unix(file.CreateTime, 0),
565
IsFolder: file.NodeType == DirectoryType,
566
Path: path.Join(relativePath, file.Name),
567
},
568
ShareID: shareId,
569
Key: file.Key,
570
NodeID: file.ID,
571
NodeType: file.NodeType,
572
}
573
574
return obj
575
}
576
577
// getFilesInPath 获取指定分享和路径下的文件
578
func (d *DoubaoShare) getFilesInPath(ctx context.Context, shareId, nodeId, relativePath string) ([]model.Obj, error) {
579
var (
580
files []File
581
err error
582
)
583
584
// 调用overview接口获取分享链接信息 nodeId
585
if nodeId == "" {
586
files, err = d.getShareOverview(shareId, "")
587
if err != nil {
588
return nil, fmt.Errorf("failed to get share link information: %w", err)
589
}
590
591
result := make([]model.Obj, 0, len(files))
592
for _, file := range files {
593
result = append(result, d.convertToFileObject(file, shareId, "/"))
594
}
595
596
return result, nil
597
598
} else {
599
files, err = d.getFiles(shareId, nodeId, "")
600
if err != nil {
601
return nil, fmt.Errorf("failed to get share file: %w", err)
602
}
603
604
result := make([]model.Obj, 0, len(files))
605
for _, file := range files {
606
result = append(result, d.convertToFileObject(file, shareId, path.Join("/", relativePath)))
607
}
608
609
return result, nil
610
}
611
}
612
613
// listRootDirectory 处理根目录的内容展示
614
func (d *DoubaoShare) listRootDirectory(ctx context.Context) ([]model.Obj, error) {
615
objects := make([]model.Obj, 0)
616
617
// 分组处理:直接显示的分享内容 vs 虚拟目录
618
var directShareIDs []string
619
addedDirs := make(map[string]bool)
620
621
// 处理所有根节点
622
for _, rootFile := range d.RootFiles {
623
if rootFile.VirtualPath == "" && rootFile.ShareID != "" {
624
// 无路径分享,记录ShareID以便后续获取内容
625
directShareIDs = append(directShareIDs, rootFile.ShareID)
626
} else {
627
// 有路径的分享,显示第一级目录
628
parts := strings.SplitN(rootFile.VirtualPath, "/", 2)
629
firstLevel := parts[0]
630
631
// 避免重复添加同名目录
632
if _, exists := addedDirs[firstLevel]; exists {
633
continue
634
}
635
636
// 创建虚拟目录对象
637
obj := &FileObject{
638
Object: model.Object{
639
ID: "",
640
Name: firstLevel,
641
Modified: time.Now(),
642
Ctime: time.Now(),
643
IsFolder: true,
644
Path: path.Join("/", firstLevel),
645
},
646
ShareID: rootFile.ShareID,
647
Key: "",
648
NodeID: "",
649
NodeType: DirectoryType,
650
}
651
objects = append(objects, obj)
652
addedDirs[firstLevel] = true
653
}
654
}
655
656
// 处理直接显示的分享内容
657
for _, shareID := range directShareIDs {
658
shareFiles, err := d.getFilesInPath(ctx, shareID, "", "")
659
if err != nil {
660
log.Warnf("[doubao_share] Failed to get list of files in share %s: %s", shareID, err)
661
continue
662
}
663
objects = append(objects, shareFiles...)
664
}
665
666
return objects, nil
667
}
668
669
// listVirtualDirectoryContent 列出虚拟目录的内容
670
func (d *DoubaoShare) listVirtualDirectoryContent(dir model.Obj) ([]model.Obj, error) {
671
dirPath := strings.TrimPrefix(dir.GetPath(), "/")
672
objects := make([]model.Obj, 0)
673
674
// 递归查找此路径的节点
675
node := _findNodeByPath(d.RootFiles, dirPath)
676
677
if node != nil && node.Child != nil {
678
// 显示此节点的所有子节点
679
for _, child := range *node.Child {
680
// 计算显示名称(取路径的最后一部分)
681
displayName := child.VirtualPath
682
if child.VirtualPath != "" {
683
parts := strings.Split(child.VirtualPath, "/")
684
displayName = parts[len(parts)-1]
685
} else if child.ShareID != "" {
686
displayName = child.ShareID
687
}
688
689
obj := &FileObject{
690
Object: model.Object{
691
ID: "",
692
Name: displayName,
693
Modified: time.Now(),
694
Ctime: time.Now(),
695
IsFolder: true,
696
Path: path.Join("/", child.VirtualPath),
697
},
698
ShareID: child.ShareID,
699
Key: "",
700
NodeID: "",
701
NodeType: DirectoryType,
702
}
703
objects = append(objects, obj)
704
}
705
}
706
707
return objects, nil
708
}
709
710
// generateContentDisposition 生成符合RFC 5987标准的Content-Disposition头部
711
func generateContentDisposition(filename string) string {
712
// 按照RFC 2047进行编码,用于filename部分
713
encodedName := urlEncode(filename)
714
715
// 按照RFC 5987进行编码,用于filename*部分
716
encodedNameRFC5987 := encodeRFC5987(filename)
717
718
return fmt.Sprintf("attachment; filename=\"%s\"; filename*=utf-8''%s",
719
encodedName, encodedNameRFC5987)
720
}
721
722
// encodeRFC5987 按照RFC 5987规范编码字符串,适用于HTTP头部参数中的非ASCII字符
723
func encodeRFC5987(s string) string {
724
var buf strings.Builder
725
for _, r := range []byte(s) {
726
// 根据RFC 5987,只有字母、数字和部分特殊符号可以不编码
727
if (r >= 'a' && r <= 'z') ||
728
(r >= 'A' && r <= 'Z') ||
729
(r >= '0' && r <= '9') ||
730
r == '-' || r == '.' || r == '_' || r == '~' {
731
buf.WriteByte(r)
732
} else {
733
// 其他字符都需要百分号编码
734
fmt.Fprintf(&buf, "%%%02X", r)
735
}
736
}
737
return buf.String()
738
}
739
740
func urlEncode(s string) string {
741
s = url.QueryEscape(s)
742
s = strings.ReplaceAll(s, "+", "%20")
743
return s
744
}
745
746