Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/doubao_new/util.go
2327 views
1
package doubao_new
2
3
import (
4
"bytes"
5
"context"
6
"crypto/rand"
7
"encoding/hex"
8
"encoding/json"
9
"fmt"
10
"hash/adler32"
11
"net/http"
12
"net/url"
13
"strconv"
14
"strings"
15
"time"
16
17
"github.com/alist-org/alist/v3/drivers/base"
18
"github.com/go-resty/resty/v2"
19
)
20
21
const (
22
BaseURL = "https://my.feishu.cn"
23
DownloadBaseURL = "https://internal-api-drive-stream.feishu.cn"
24
)
25
26
var defaultObjTypes = []string{"124", "0", "12", "30", "123", "22"}
27
28
func (d *DoubaoNew) request(ctx context.Context, path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
29
req := base.RestyClient.R()
30
req.SetContext(ctx)
31
req.SetHeader("accept", "*/*")
32
req.SetHeader("origin", "https://www.doubao.com")
33
req.SetHeader("referer", "https://www.doubao.com/")
34
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
35
if auth := d.resolveAuthorization(); auth != "" {
36
req.SetHeader("authorization", auth)
37
}
38
if dpop := d.resolveDpop(); dpop != "" {
39
req.SetHeader("dpop", dpop)
40
}
41
42
if callback != nil {
43
callback(req)
44
}
45
46
res, err := req.Execute(method, BaseURL+path)
47
if err != nil {
48
return nil, err
49
}
50
if res != nil {
51
if v := res.Header().Get("X-Tt-Logid"); v != "" {
52
d.TtLogid = v
53
} else if v := res.Header().Get("x-tt-logid"); v != "" {
54
d.TtLogid = v
55
}
56
}
57
58
body := res.Body()
59
var common BaseResp
60
if err = json.Unmarshal(body, &common); err != nil {
61
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
62
res.Status(),
63
res.Header().Get("Content-Type"),
64
string(body),
65
err,
66
)
67
return body, fmt.Errorf(msg)
68
}
69
if common.Code != 0 {
70
errMsg := common.Msg
71
if errMsg == "" {
72
errMsg = common.Message
73
}
74
return body, fmt.Errorf("[doubao_new] API error (code: %d): %s", common.Code, errMsg)
75
}
76
if resp != nil {
77
if err = json.Unmarshal(body, resp); err != nil {
78
return body, err
79
}
80
}
81
82
return body, nil
83
}
84
85
func getCookieValue(cookie, name string) string {
86
parts := strings.Split(cookie, ";")
87
prefix := name + "="
88
for _, part := range parts {
89
part = strings.TrimSpace(part)
90
if strings.HasPrefix(part, prefix) {
91
return strings.TrimPrefix(part, prefix)
92
}
93
}
94
return ""
95
}
96
97
func adler32String(data []byte) string {
98
sum := adler32.Checksum(data)
99
return strconv.FormatUint(uint64(sum), 10)
100
}
101
102
func buildCommaHeader(items []string) string {
103
return strings.Join(items, ",")
104
}
105
106
func joinIntComma(items []int) string {
107
if len(items) == 0 {
108
return ""
109
}
110
var sb strings.Builder
111
for i, v := range items {
112
if i > 0 {
113
sb.WriteByte(',')
114
}
115
sb.WriteString(strconv.Itoa(v))
116
}
117
return sb.String()
118
}
119
120
func previewList(items []string, n int) string {
121
if n <= 0 || len(items) == 0 {
122
return ""
123
}
124
if len(items) < n {
125
n = len(items)
126
}
127
return strings.Join(items[:n], ",")
128
}
129
130
func (d *DoubaoNew) resolveAuthorization() string {
131
auth := strings.TrimSpace(d.Authorization)
132
if auth == "" && d.Cookie != "" {
133
if token := getCookieValue(d.Cookie, "LARK_SUITE_ACCESS_TOKEN"); token != "" {
134
auth = token
135
}
136
}
137
if auth == "" {
138
return ""
139
}
140
if !strings.HasPrefix(auth, "DPoP ") && !strings.HasPrefix(auth, "dpop ") {
141
auth = "DPoP " + auth
142
}
143
return auth
144
}
145
146
func (d *DoubaoNew) resolveDpop() string {
147
dpop := strings.TrimSpace(d.Dpop)
148
if dpop == "" && d.Cookie != "" {
149
dpop = getCookieValue(d.Cookie, "LARK_SUITE_DPOP")
150
}
151
return dpop
152
}
153
154
func (d *DoubaoNew) listChildren(ctx context.Context, parentToken string, lastLabel string) (ListData, error) {
155
var resp ListResp
156
_, err := d.request(ctx, "/space/api/explorer/doubao/children/list/", http.MethodGet, func(req *resty.Request) {
157
values := url.Values{}
158
for _, t := range defaultObjTypes {
159
values.Add("obj_type", t)
160
}
161
values.Set("length", "50")
162
values.Set("rank", "0")
163
values.Set("asc", "0")
164
values.Set("min_length", "40")
165
values.Set("thumbnail_width", "1028")
166
values.Set("thumbnail_height", "1028")
167
values.Set("thumbnail_policy", "4")
168
if parentToken != "" {
169
values.Set("token", parentToken)
170
}
171
if lastLabel != "" {
172
values.Set("last_label", lastLabel)
173
}
174
req.SetQueryParamsFromValues(values)
175
}, &resp)
176
if err != nil {
177
return ListData{}, err
178
}
179
180
return resp.Data, nil
181
}
182
183
func (d *DoubaoNew) getFileInfo(ctx context.Context, fileToken string) (FileInfo, error) {
184
var resp FileInfoResp
185
_, err := d.request(ctx, "/space/api/box/file/info/", http.MethodPost, func(req *resty.Request) {
186
req.SetHeader("Content-Type", "application/json")
187
req.SetBody(base.Json{
188
"caller": "explorer",
189
"file_token": fileToken,
190
"mount_point": "explorer",
191
"option_params": []string{"preview_meta", "check_cipher"},
192
})
193
}, &resp)
194
if err != nil {
195
return FileInfo{}, err
196
}
197
198
return resp.Data, nil
199
}
200
201
func (d *DoubaoNew) createFolder(ctx context.Context, parentToken, name string) (Node, error) {
202
data := url.Values{}
203
data.Set("name", name)
204
data.Set("source", "0")
205
if parentToken != "" {
206
data.Set("parent_token", parentToken)
207
}
208
209
doRequest := func(csrfToken string) (*resty.Response, []byte, error) {
210
req := base.RestyClient.R()
211
req.SetContext(ctx)
212
req.SetHeader("accept", "*/*")
213
req.SetHeader("origin", "https://www.doubao.com")
214
req.SetHeader("referer", "https://www.doubao.com/")
215
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
216
if auth := d.resolveAuthorization(); auth != "" {
217
req.SetHeader("authorization", auth)
218
}
219
if dpop := d.resolveDpop(); dpop != "" {
220
req.SetHeader("dpop", dpop)
221
}
222
if csrfToken != "" {
223
req.SetHeader("x-csrftoken", csrfToken)
224
}
225
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
226
req.SetBody(data.Encode())
227
res, err := req.Execute(http.MethodPost, BaseURL+"/space/api/explorer/v2/create/folder/")
228
if err != nil {
229
return nil, nil, err
230
}
231
return res, res.Body(), nil
232
}
233
234
res, body, err := doRequestWithCsrf(doRequest)
235
if err != nil {
236
return Node{}, err
237
}
238
if err := decodeBaseResp(body, res); err != nil {
239
return Node{}, err
240
}
241
242
var resp CreateFolderResp
243
if err := json.Unmarshal(body, &resp); err != nil {
244
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
245
res.Status(),
246
res.Header().Get("Content-Type"),
247
string(body),
248
err,
249
)
250
return Node{}, fmt.Errorf(msg)
251
}
252
253
var node Node
254
if len(resp.Data.NodeList) > 0 {
255
if n, ok := resp.Data.Entities.Nodes[resp.Data.NodeList[0]]; ok {
256
node = n
257
}
258
}
259
if node.Token == "" {
260
for _, n := range resp.Data.Entities.Nodes {
261
node = n
262
break
263
}
264
}
265
if node.Token == "" && node.ObjToken == "" && node.NodeToken == "" {
266
return Node{}, fmt.Errorf("[doubao_new] create folder failed: empty response")
267
}
268
if node.NodeToken == "" {
269
if node.Token != "" {
270
node.NodeToken = node.Token
271
} else if node.ObjToken != "" {
272
node.NodeToken = node.ObjToken
273
}
274
}
275
if node.ObjToken == "" && node.Token != "" {
276
node.ObjToken = node.Token
277
}
278
return node, nil
279
}
280
281
func (d *DoubaoNew) renameFolder(ctx context.Context, token, name string) error {
282
if token == "" {
283
return fmt.Errorf("[doubao_new] rename folder missing token")
284
}
285
data := url.Values{}
286
data.Set("token", token)
287
data.Set("name", name)
288
289
doRequest := func(csrfToken string) (*resty.Response, []byte, error) {
290
req := base.RestyClient.R()
291
req.SetContext(ctx)
292
req.SetHeader("accept", "*/*")
293
req.SetHeader("origin", "https://www.doubao.com")
294
req.SetHeader("referer", "https://www.doubao.com/")
295
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
296
if auth := d.resolveAuthorization(); auth != "" {
297
req.SetHeader("authorization", auth)
298
}
299
if dpop := d.resolveDpop(); dpop != "" {
300
req.SetHeader("dpop", dpop)
301
}
302
if csrfToken != "" {
303
req.SetHeader("x-csrftoken", csrfToken)
304
}
305
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
306
req.SetBody(data.Encode())
307
res, err := req.Execute(http.MethodPost, BaseURL+"/space/api/explorer/v2/rename/")
308
if err != nil {
309
return nil, nil, err
310
}
311
return res, res.Body(), nil
312
}
313
314
res, body, err := doRequestWithCsrf(doRequest)
315
if err != nil {
316
return err
317
}
318
return decodeBaseResp(body, res)
319
}
320
321
func isCsrfTokenError(body []byte, res *resty.Response) bool {
322
if len(body) == 0 {
323
return false
324
}
325
if strings.Contains(strings.ToLower(string(body)), "csrf token error") {
326
return true
327
}
328
if res != nil && res.StatusCode() == http.StatusForbidden {
329
return true
330
}
331
return false
332
}
333
334
func doRequestWithCsrf(doRequest func(csrfToken string) (*resty.Response, []byte, error)) (*resty.Response, []byte, error) {
335
res, body, err := doRequest("")
336
if err != nil {
337
return res, body, err
338
}
339
if isCsrfTokenError(body, res) {
340
csrfToken := extractCsrfTokenFromResponse(res)
341
if csrfToken != "" {
342
return doRequest(csrfToken)
343
}
344
}
345
return res, body, err
346
}
347
348
func extractCsrfTokenFromResponse(res *resty.Response) string {
349
if res == nil || res.Request == nil {
350
return ""
351
}
352
if res.Request.RawRequest != nil {
353
if csrf := getCookieValue(res.Request.RawRequest.Header.Get("Cookie"), "_csrf_token"); csrf != "" {
354
return csrf
355
}
356
}
357
if csrf := getCookieValue(res.Request.Header.Get("Cookie"), "_csrf_token"); csrf != "" {
358
return csrf
359
}
360
for _, c := range res.Cookies() {
361
if c.Name == "_csrf_token" {
362
return c.Value
363
}
364
}
365
return ""
366
}
367
368
func decodeBaseResp(body []byte, res *resty.Response) error {
369
var common BaseResp
370
if err := json.Unmarshal(body, &common); err != nil {
371
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
372
res.Status(),
373
res.Header().Get("Content-Type"),
374
string(body),
375
err,
376
)
377
return fmt.Errorf(msg)
378
}
379
if common.Code != 0 {
380
errMsg := common.Msg
381
if errMsg == "" {
382
errMsg = common.Message
383
}
384
return fmt.Errorf("[doubao_new] API error (code: %d): %s", common.Code, errMsg)
385
}
386
return nil
387
}
388
389
func (d *DoubaoNew) renameFile(ctx context.Context, fileToken, name string) error {
390
if fileToken == "" {
391
return fmt.Errorf("[doubao_new] rename file missing file token")
392
}
393
_, err := d.request(ctx, "/space/api/box/file/update_info/", http.MethodPost, func(req *resty.Request) {
394
req.SetHeader("Content-Type", "application/json")
395
req.SetBody(base.Json{
396
"file_token": fileToken,
397
"name": name,
398
})
399
}, nil)
400
return err
401
}
402
403
func (d *DoubaoNew) moveObj(ctx context.Context, srcToken, destToken string) error {
404
if srcToken == "" {
405
return fmt.Errorf("[doubao_new] move missing src token")
406
}
407
data := url.Values{}
408
data.Set("src_token", srcToken)
409
if destToken != "" {
410
data.Set("dest_token", destToken)
411
}
412
doRequest := func(csrfToken string) (*resty.Response, []byte, error) {
413
req := base.RestyClient.R()
414
req.SetContext(ctx)
415
req.SetHeader("accept", "*/*")
416
req.SetHeader("origin", "https://www.doubao.com")
417
req.SetHeader("referer", "https://www.doubao.com/")
418
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
419
if auth := d.resolveAuthorization(); auth != "" {
420
req.SetHeader("authorization", auth)
421
}
422
if dpop := d.resolveDpop(); dpop != "" {
423
req.SetHeader("dpop", dpop)
424
}
425
if csrfToken != "" {
426
req.SetHeader("x-csrftoken", csrfToken)
427
}
428
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
429
req.SetBody(data.Encode())
430
res, err := req.Execute(http.MethodPost, BaseURL+"/space/api/explorer/v2/move/")
431
if err != nil {
432
return nil, nil, err
433
}
434
return res, res.Body(), nil
435
}
436
437
res, body, err := doRequestWithCsrf(doRequest)
438
if err != nil {
439
return err
440
}
441
return decodeBaseResp(body, res)
442
}
443
444
func (d *DoubaoNew) removeObj(ctx context.Context, tokens []string) error {
445
if len(tokens) == 0 {
446
return fmt.Errorf("[doubao_new] remove missing tokens")
447
}
448
doRequest := func(csrfToken string) (*resty.Response, []byte, error) {
449
req := base.RestyClient.R()
450
req.SetContext(ctx)
451
req.SetHeader("accept", "application/json, text/plain, */*")
452
req.SetHeader("origin", "https://www.doubao.com")
453
req.SetHeader("referer", "https://www.doubao.com/")
454
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
455
if auth := d.resolveAuthorization(); auth != "" {
456
req.SetHeader("authorization", auth)
457
}
458
if dpop := d.resolveDpop(); dpop != "" {
459
req.SetHeader("dpop", dpop)
460
}
461
if csrfToken != "" {
462
req.SetHeader("x-csrftoken", csrfToken)
463
}
464
req.SetHeader("Content-Type", "application/json")
465
req.SetBody(base.Json{
466
"tokens": tokens,
467
"apply": 1,
468
})
469
res, err := req.Execute(http.MethodPost, BaseURL+"/space/api/explorer/v3/remove/")
470
if err != nil {
471
return nil, nil, err
472
}
473
return res, res.Body(), nil
474
}
475
476
res, body, err := doRequestWithCsrf(doRequest)
477
if err != nil {
478
return err
479
}
480
var resp RemoveResp
481
if err := json.Unmarshal(body, &resp); err != nil {
482
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
483
res.Status(),
484
res.Header().Get("Content-Type"),
485
string(body),
486
err,
487
)
488
return fmt.Errorf(msg)
489
}
490
if resp.Code != 0 {
491
errMsg := resp.Msg
492
if errMsg == "" {
493
errMsg = resp.Message
494
}
495
return fmt.Errorf("[doubao_new] API error (code: %d): %s", resp.Code, errMsg)
496
}
497
if resp.Data.TaskID == "" {
498
return nil
499
}
500
return d.waitTask(ctx, resp.Data.TaskID)
501
}
502
503
func (d *DoubaoNew) getUserStorage(ctx context.Context) (UserStorageData, error) {
504
req := base.RestyClient.R()
505
req.SetContext(ctx)
506
req.SetHeader("accept", "*/*")
507
req.SetHeader("origin", "https://www.doubao.com")
508
req.SetHeader("referer", "https://www.doubao.com/")
509
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
510
req.SetHeader("agw-js-conv", "str")
511
req.SetHeader("content-type", "application/json")
512
if auth := d.resolveAuthorization(); auth != "" {
513
req.SetHeader("authorization", auth)
514
}
515
if dpop := d.resolveDpop(); dpop != "" {
516
req.SetHeader("dpop", dpop)
517
}
518
if d.Cookie != "" {
519
req.SetHeader("cookie", d.Cookie)
520
}
521
req.SetBody(base.Json{})
522
523
res, err := req.Execute(http.MethodPost, "https://www.doubao.com/alice/aispace/facade/get_user_storage")
524
if err != nil {
525
return UserStorageData{}, err
526
}
527
528
body := res.Body()
529
var resp UserStorageResp
530
if err := json.Unmarshal(body, &resp); err != nil {
531
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
532
res.Status(),
533
res.Header().Get("Content-Type"),
534
string(body),
535
err,
536
)
537
return UserStorageData{}, fmt.Errorf(msg)
538
}
539
if resp.Code != 0 {
540
errMsg := resp.Msg
541
if errMsg == "" {
542
errMsg = resp.Message
543
}
544
return UserStorageData{}, fmt.Errorf("[doubao_new] API error (code: %d): %s", resp.Code, errMsg)
545
}
546
547
return resp.Data, nil
548
}
549
550
func (d *DoubaoNew) waitTask(ctx context.Context, taskID string) error {
551
const (
552
taskPollInterval = time.Second
553
taskPollMaxAttempts = 120
554
)
555
var lastErr error
556
for attempt := 0; attempt < taskPollMaxAttempts; attempt++ {
557
if attempt > 0 {
558
if err := waitWithContext(ctx, taskPollInterval); err != nil {
559
return err
560
}
561
}
562
status, err := d.getTaskStatus(ctx, taskID)
563
if err != nil {
564
lastErr = err
565
continue
566
}
567
if status.IsFail {
568
return fmt.Errorf("[doubao_new] remove task failed: %s", taskID)
569
}
570
if status.IsFinish {
571
return nil
572
}
573
}
574
if lastErr != nil {
575
return lastErr
576
}
577
return fmt.Errorf("[doubao_new] remove task timed out: %s", taskID)
578
}
579
580
func (d *DoubaoNew) getTaskStatus(ctx context.Context, taskID string) (TaskStatusData, error) {
581
if taskID == "" {
582
return TaskStatusData{}, fmt.Errorf("[doubao_new] task status missing task_id")
583
}
584
req := base.RestyClient.R()
585
req.SetContext(ctx)
586
req.SetHeader("accept", "application/json, text/plain, */*")
587
req.SetHeader("origin", "https://www.doubao.com")
588
req.SetHeader("referer", "https://www.doubao.com/")
589
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
590
if auth := d.resolveAuthorization(); auth != "" {
591
req.SetHeader("authorization", auth)
592
}
593
if dpop := d.resolveDpop(); dpop != "" {
594
req.SetHeader("dpop", dpop)
595
}
596
req.SetQueryParam("task_id", taskID)
597
res, err := req.Execute(http.MethodGet, BaseURL+"/space/api/explorer/v2/task/")
598
if err != nil {
599
return TaskStatusData{}, err
600
}
601
body := res.Body()
602
var resp TaskStatusResp
603
if err := json.Unmarshal(body, &resp); err != nil {
604
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
605
res.Status(),
606
res.Header().Get("Content-Type"),
607
string(body),
608
err,
609
)
610
return TaskStatusData{}, fmt.Errorf(msg)
611
}
612
if resp.Code != 0 {
613
errMsg := resp.Msg
614
if errMsg == "" {
615
errMsg = resp.Message
616
}
617
return TaskStatusData{}, fmt.Errorf("[doubao_new] API error (code: %d): %s", resp.Code, errMsg)
618
}
619
return resp.Data, nil
620
}
621
622
func waitWithContext(ctx context.Context, d time.Duration) error {
623
timer := time.NewTimer(d)
624
defer timer.Stop()
625
select {
626
case <-ctx.Done():
627
return ctx.Err()
628
case <-timer.C:
629
return nil
630
}
631
}
632
633
func (d *DoubaoNew) prepareUpload(ctx context.Context, name string, size int64, mountNodeToken string) (UploadPrepareData, error) {
634
var resp UploadPrepareResp
635
_, err := d.request(ctx, "/space/api/box/upload/prepare/", http.MethodPost, func(req *resty.Request) {
636
values := url.Values{}
637
values.Set("shouldBypassScsDialog", "true")
638
values.Set("doubao_storage", "imagex_other")
639
values.Set("doubao_app_id", "497858")
640
req.SetQueryParamsFromValues(values)
641
req.SetHeader("Content-Type", "application/json")
642
req.SetHeader("x-command", "space.api.box.upload.prepare")
643
req.SetHeader("rpc-persist-doubao-pan", "true")
644
req.SetHeader("cache-control", "no-cache")
645
req.SetHeader("pragma", "no-cache")
646
body := base.Json{
647
"mount_point": "explorer",
648
"mount_node_token": "",
649
"name": name,
650
"size": size,
651
"size_checker": true,
652
}
653
if mountNodeToken != "" {
654
body["mount_node_token"] = mountNodeToken
655
}
656
req.SetBody(body)
657
}, &resp)
658
if err != nil {
659
return UploadPrepareData{}, err
660
}
661
return resp.Data, nil
662
}
663
664
func (d *DoubaoNew) uploadBlocks(ctx context.Context, uploadID string, blocks []UploadBlock, mountPoint string) (UploadBlocksData, error) {
665
if uploadID == "" {
666
return UploadBlocksData{}, fmt.Errorf("[doubao_new] upload blocks missing upload_id")
667
}
668
if mountPoint == "" {
669
mountPoint = "explorer"
670
}
671
var resp UploadBlocksResp
672
_, err := d.request(ctx, "/space/api/box/upload/blocks/", http.MethodPost, func(req *resty.Request) {
673
values := url.Values{}
674
values.Set("shouldBypassScsDialog", "true")
675
values.Set("doubao_storage", "imagex_other")
676
values.Set("doubao_app_id", "497858")
677
req.SetQueryParamsFromValues(values)
678
req.SetHeader("Content-Type", "application/json")
679
req.SetHeader("x-command", "space.api.box.upload.blocks")
680
req.SetHeader("rpc-persist-doubao-pan", "true")
681
req.SetHeader("cache-control", "no-cache")
682
req.SetHeader("pragma", "no-cache")
683
req.SetBody(base.Json{
684
"blocks": blocks,
685
"upload_id": uploadID,
686
"mount_point": mountPoint,
687
})
688
}, &resp)
689
if err != nil {
690
return UploadBlocksData{}, err
691
}
692
return resp.Data, nil
693
}
694
695
func (d *DoubaoNew) mergeUploadBlocks(ctx context.Context, uploadID string, seqList []int, checksumList []string, sizeList []int64, blockOriginSize int64, data []byte) (UploadMergeData, error) {
696
if uploadID == "" {
697
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks missing upload_id")
698
}
699
if len(seqList) == 0 {
700
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks empty seq list")
701
}
702
if len(checksumList) == 0 {
703
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks empty checksum list")
704
}
705
if len(sizeList) != len(seqList) {
706
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks size list mismatch")
707
}
708
if blockOriginSize <= 0 {
709
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks invalid block origin size")
710
}
711
if len(data) == 0 {
712
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks empty data")
713
}
714
715
seqHeader := joinIntComma(seqList)
716
checksumHeader := buildCommaHeader(checksumList)
717
718
client := base.NewRestyClient()
719
client.SetCookieJar(nil)
720
req := client.R()
721
req.SetContext(ctx)
722
req.SetHeader("accept", "application/json, text/plain, */*")
723
req.SetHeader("origin", "https://www.doubao.com")
724
req.SetHeader("referer", "https://www.doubao.com/")
725
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
726
req.SetHeader("rpc-persist-doubao-pan", "true")
727
req.SetHeader("content-type", "application/octet-stream")
728
req.Header.Set("x-block-list-checksum", checksumHeader)
729
req.Header.Set("x-seq-list", seqHeader)
730
req.SetHeader("x-block-origin-size", strconv.FormatInt(blockOriginSize, 10))
731
req.SetHeader("x-command", "space.api.box.stream.upload.merge_block")
732
req.SetHeader("x-csrftoken", "")
733
reqID := ""
734
if buf := make([]byte, 16); true {
735
if _, err := rand.Read(buf); err == nil {
736
reqID = hex.EncodeToString(buf)
737
}
738
}
739
if reqID != "" {
740
req.SetHeader("x-request-id", reqID)
741
}
742
if auth := d.resolveAuthorization(); auth != "" {
743
req.SetHeader("authorization", auth)
744
}
745
if dpop := d.resolveDpop(); dpop != "" {
746
req.SetHeader("dpop", dpop)
747
}
748
req.Header.Del("Cookie")
749
req.Header.Del("cookie")
750
if req.Header.Get("x-command") == "" {
751
return UploadMergeData{}, fmt.Errorf("[doubao_new] merge blocks missing x-command header")
752
}
753
req.SetBody(data)
754
755
values := url.Values{}
756
values.Set("shouldBypassScsDialog", "true")
757
values.Set("upload_id", uploadID)
758
values.Set("mount_point", "explorer")
759
values.Set("doubao_storage", "imagex_other")
760
values.Set("doubao_app_id", "497858")
761
urlStr := "https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/merge_block/?" + values.Encode()
762
763
res, err := req.Execute(http.MethodPost, urlStr)
764
if err != nil {
765
return UploadMergeData{}, err
766
}
767
if v := res.Header().Get("X-Tt-Logid"); v != "" {
768
d.TtLogid = v
769
} else if v := res.Header().Get("x-tt-logid"); v != "" {
770
d.TtLogid = v
771
}
772
body := res.Body()
773
var resp UploadMergeResp
774
if err := json.Unmarshal(body, &resp); err != nil {
775
msg := fmt.Sprintf("[doubao_new] decode response failed (status: %s, content-type: %s, body: %s): %v",
776
res.Status(),
777
res.Header().Get("Content-Type"),
778
string(body),
779
err,
780
)
781
return UploadMergeData{}, fmt.Errorf(msg)
782
}
783
if resp.Code != 0 {
784
if res != nil && res.StatusCode() == http.StatusBadRequest && resp.Code == 2 {
785
success := make([]int, 0, len(seqList))
786
offset := 0
787
for i, seq := range seqList {
788
size := sizeList[i]
789
if size <= 0 {
790
return UploadMergeData{SuccessSeqList: success}, fmt.Errorf("[doubao_new] v3 fallback invalid size: seq=%d size=%d", seq, size)
791
}
792
if offset+int(size) > len(data) {
793
return UploadMergeData{SuccessSeqList: success}, fmt.Errorf("[doubao_new] v3 fallback payload out of range: seq=%d offset=%d size=%d total=%d", seq, offset, size, len(data))
794
}
795
payload := data[offset : offset+int(size)]
796
block := UploadBlockNeed{
797
Seq: seq,
798
Size: size,
799
Checksum: checksumList[i],
800
}
801
if err := d.uploadBlockV3(ctx, uploadID, block, payload); err != nil {
802
return UploadMergeData{SuccessSeqList: success}, err
803
}
804
success = append(success, seq)
805
offset += int(size)
806
}
807
return UploadMergeData{SuccessSeqList: success}, nil
808
}
809
errMsg := resp.Msg
810
if errMsg == "" {
811
errMsg = resp.Message
812
}
813
return UploadMergeData{}, fmt.Errorf("[doubao_new] API error (code: %d): %s", resp.Code, errMsg)
814
}
815
816
return resp.Data, nil
817
}
818
819
func (d *DoubaoNew) uploadBlockV3(ctx context.Context, uploadID string, block UploadBlockNeed, data []byte) error {
820
if uploadID == "" {
821
return fmt.Errorf("[doubao_new] upload v3 block missing upload_id")
822
}
823
if block.Seq < 0 {
824
return fmt.Errorf("[doubao_new] upload v3 block invalid seq")
825
}
826
if len(data) == 0 {
827
return fmt.Errorf("[doubao_new] upload v3 block empty data")
828
}
829
830
req := base.RestyClient.R()
831
req.SetContext(ctx)
832
req.SetHeader("accept", "*/*")
833
req.SetHeader("origin", "https://www.doubao.com")
834
req.SetHeader("referer", "https://www.doubao.com/")
835
req.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
836
req.SetHeader("rpc-persist-doubao-pan", "true")
837
req.SetHeader("x-block-seq", strconv.Itoa(block.Seq))
838
req.SetHeader("x-block-checksum", block.Checksum)
839
if auth := d.resolveAuthorization(); auth != "" {
840
req.SetHeader("authorization", auth)
841
}
842
if dpop := d.resolveDpop(); dpop != "" {
843
req.SetHeader("dpop", dpop)
844
}
845
846
req.SetMultipartFormData(map[string]string{
847
"upload_id": uploadID,
848
"size": strconv.FormatInt(int64(len(data)), 10),
849
})
850
req.SetMultipartField("file", "blob", "application/octet-stream", bytes.NewReader(data))
851
852
values := url.Values{}
853
values.Set("shouldBypassScsDialog", "true")
854
values.Set("upload_id", uploadID)
855
values.Set("seq", strconv.Itoa(block.Seq))
856
values.Set("size", strconv.FormatInt(int64(len(data)), 10))
857
values.Set("checksum", block.Checksum)
858
values.Set("mount_point", "explorer")
859
values.Set("doubao_storage", "imagex_other")
860
values.Set("doubao_app_id", "497858")
861
urlStr := "https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/v3/block/?" + values.Encode()
862
863
res, err := req.Execute(http.MethodPost, urlStr)
864
if err != nil {
865
return err
866
}
867
body := res.Body()
868
if err := decodeBaseResp(body, res); err != nil {
869
return err
870
}
871
return nil
872
}
873
874
func (d *DoubaoNew) finishUpload(ctx context.Context, uploadID string, numBlocks int, mountPoint string) (UploadFinishData, error) {
875
if uploadID == "" {
876
return UploadFinishData{}, fmt.Errorf("[doubao_new] finish upload missing upload_id")
877
}
878
if numBlocks <= 0 {
879
return UploadFinishData{}, fmt.Errorf("[doubao_new] finish upload invalid num_blocks")
880
}
881
if mountPoint == "" {
882
mountPoint = "explorer"
883
}
884
var resp UploadFinishResp
885
_, err := d.request(ctx, "/space/api/box/upload/finish/", http.MethodPost, func(req *resty.Request) {
886
values := url.Values{}
887
values.Set("shouldBypassScsDialog", "true")
888
values.Set("doubao_storage", "imagex_other")
889
values.Set("doubao_app_id", "497858")
890
req.SetQueryParamsFromValues(values)
891
req.SetHeader("Content-Type", "application/json")
892
req.SetHeader("x-command", "space.api.box.upload.finish")
893
req.SetHeader("rpc-persist-doubao-pan", "true")
894
req.SetHeader("cache-control", "no-cache")
895
req.SetHeader("pragma", "no-cache")
896
req.SetHeader("biz-scene", "file_upload")
897
req.SetHeader("biz-ua-type", "Web")
898
req.SetBody(base.Json{
899
"upload_id": uploadID,
900
"num_blocks": numBlocks,
901
"mount_point": mountPoint,
902
"push_open_history_record": 1,
903
})
904
}, &resp)
905
if err != nil {
906
return UploadFinishData{}, err
907
}
908
return resp.Data, nil
909
}
910
911