Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/doubao/util.go
1987 views
1
package doubao
2
3
import (
4
"context"
5
"crypto/hmac"
6
"crypto/sha256"
7
"encoding/hex"
8
"encoding/json"
9
"errors"
10
"fmt"
11
"github.com/alist-org/alist/v3/drivers/base"
12
"github.com/alist-org/alist/v3/internal/driver"
13
"github.com/alist-org/alist/v3/internal/model"
14
"github.com/alist-org/alist/v3/pkg/errgroup"
15
"github.com/alist-org/alist/v3/pkg/utils"
16
"github.com/avast/retry-go"
17
"github.com/go-resty/resty/v2"
18
"github.com/google/uuid"
19
log "github.com/sirupsen/logrus"
20
"hash/crc32"
21
"io"
22
"math"
23
"math/rand"
24
"net/http"
25
"net/url"
26
"path/filepath"
27
"sort"
28
"strconv"
29
"strings"
30
"sync"
31
"time"
32
)
33
34
const (
35
DirectoryType = 1
36
FileType = 2
37
LinkType = 3
38
ImageType = 4
39
PagesType = 5
40
VideoType = 6
41
AudioType = 7
42
MeetingMinutesType = 8
43
)
44
45
var FileNodeType = map[int]string{
46
1: "directory",
47
2: "file",
48
3: "link",
49
4: "image",
50
5: "pages",
51
6: "video",
52
7: "audio",
53
8: "meeting_minutes",
54
}
55
56
const (
57
BaseURL = "https://www.doubao.com"
58
FileDataType = "file"
59
ImgDataType = "image"
60
VideoDataType = "video"
61
DefaultChunkSize = int64(5 * 1024 * 1024) // 5MB
62
MaxRetryAttempts = 3 // 最大重试次数
63
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
64
Region = "cn-north-1"
65
UploadTimeout = 3 * time.Minute
66
)
67
68
// do others that not defined in Driver interface
69
func (d *Doubao) request(path string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
70
reqUrl := BaseURL + path
71
req := base.RestyClient.R()
72
req.SetHeader("Cookie", d.Cookie)
73
if callback != nil {
74
callback(req)
75
}
76
77
var commonResp CommonResp
78
79
res, err := req.Execute(method, reqUrl)
80
log.Debugln(res.String())
81
if err != nil {
82
return nil, err
83
}
84
85
body := res.Body()
86
// 先解析为通用响应
87
if err = json.Unmarshal(body, &commonResp); err != nil {
88
return nil, err
89
}
90
// 检查响应是否成功
91
if !commonResp.IsSuccess() {
92
return body, commonResp.GetError()
93
}
94
95
if resp != nil {
96
if err = json.Unmarshal(body, resp); err != nil {
97
return body, err
98
}
99
}
100
101
return body, nil
102
}
103
104
func (d *Doubao) getFiles(dirId, cursor string) (resp []File, err error) {
105
var r NodeInfoResp
106
107
var body = base.Json{
108
"node_id": dirId,
109
}
110
// 如果有游标,则设置游标和大小
111
if cursor != "" {
112
body["cursor"] = cursor
113
body["size"] = 50
114
} else {
115
body["need_full_path"] = false
116
}
117
118
_, err = d.request("/samantha/aispace/node_info", http.MethodPost, func(req *resty.Request) {
119
req.SetBody(body)
120
}, &r)
121
if err != nil {
122
return nil, err
123
}
124
125
if r.Data.Children != nil {
126
resp = r.Data.Children
127
}
128
129
if r.Data.NextCursor != "-1" {
130
// 递归获取下一页
131
nextFiles, err := d.getFiles(dirId, r.Data.NextCursor)
132
if err != nil {
133
return nil, err
134
}
135
136
resp = append(r.Data.Children, nextFiles...)
137
}
138
139
return resp, err
140
}
141
142
func (d *Doubao) getUserInfo() (UserInfo, error) {
143
var r UserInfoResp
144
145
_, err := d.request("/passport/account/info/v2/", http.MethodGet, nil, &r)
146
if err != nil {
147
return UserInfo{}, err
148
}
149
150
return r.Data, err
151
}
152
153
// 签名请求
154
func (d *Doubao) signRequest(req *resty.Request, method, tokenType, uploadUrl string) error {
155
parsedUrl, err := url.Parse(uploadUrl)
156
if err != nil {
157
return fmt.Errorf("invalid URL format: %w", err)
158
}
159
160
var accessKeyId, secretAccessKey, sessionToken string
161
var serviceName string
162
163
if tokenType == VideoDataType {
164
accessKeyId = d.UploadToken.Samantha.StsToken.AccessKeyID
165
secretAccessKey = d.UploadToken.Samantha.StsToken.SecretAccessKey
166
sessionToken = d.UploadToken.Samantha.StsToken.SessionToken
167
serviceName = "vod"
168
} else {
169
accessKeyId = d.UploadToken.Alice[tokenType].Auth.AccessKeyID
170
secretAccessKey = d.UploadToken.Alice[tokenType].Auth.SecretAccessKey
171
sessionToken = d.UploadToken.Alice[tokenType].Auth.SessionToken
172
serviceName = "imagex"
173
}
174
175
// 当前时间,格式为 ISO8601
176
now := time.Now().UTC()
177
amzDate := now.Format("20060102T150405Z")
178
dateStamp := now.Format("20060102")
179
180
req.SetHeader("X-Amz-Date", amzDate)
181
182
if sessionToken != "" {
183
req.SetHeader("X-Amz-Security-Token", sessionToken)
184
}
185
186
// 计算请求体的SHA256哈希
187
var bodyHash string
188
if req.Body != nil {
189
bodyBytes, ok := req.Body.([]byte)
190
if !ok {
191
return fmt.Errorf("request body must be []byte")
192
}
193
194
bodyHash = hashSHA256(string(bodyBytes))
195
req.SetHeader("X-Amz-Content-Sha256", bodyHash)
196
} else {
197
bodyHash = hashSHA256("")
198
}
199
200
// 创建规范请求
201
canonicalURI := parsedUrl.Path
202
if canonicalURI == "" {
203
canonicalURI = "/"
204
}
205
206
// 查询参数按照字母顺序排序
207
canonicalQueryString := getCanonicalQueryString(req.QueryParam)
208
// 规范请求头
209
canonicalHeaders, signedHeaders := getCanonicalHeadersFromMap(req.Header)
210
canonicalRequest := method + "\n" +
211
canonicalURI + "\n" +
212
canonicalQueryString + "\n" +
213
canonicalHeaders + "\n" +
214
signedHeaders + "\n" +
215
bodyHash
216
217
algorithm := "AWS4-HMAC-SHA256"
218
credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, Region, serviceName)
219
220
stringToSign := algorithm + "\n" +
221
amzDate + "\n" +
222
credentialScope + "\n" +
223
hashSHA256(canonicalRequest)
224
// 计算签名密钥
225
signingKey := getSigningKey(secretAccessKey, dateStamp, Region, serviceName)
226
// 计算签名
227
signature := hmacSHA256Hex(signingKey, stringToSign)
228
// 构建授权头
229
authorizationHeader := fmt.Sprintf(
230
"%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
231
algorithm,
232
accessKeyId,
233
credentialScope,
234
signedHeaders,
235
signature,
236
)
237
238
req.SetHeader("Authorization", authorizationHeader)
239
240
return nil
241
}
242
243
func (d *Doubao) requestApi(url, method, tokenType string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
244
req := base.RestyClient.R()
245
req.SetHeaders(map[string]string{
246
"user-agent": UserAgent,
247
})
248
249
if method == http.MethodPost {
250
req.SetHeader("Content-Type", "text/plain;charset=UTF-8")
251
}
252
253
if callback != nil {
254
callback(req)
255
}
256
257
if resp != nil {
258
req.SetResult(resp)
259
}
260
261
// 使用自定义AWS SigV4签名
262
err := d.signRequest(req, method, tokenType, url)
263
if err != nil {
264
return nil, err
265
}
266
267
res, err := req.Execute(method, url)
268
if err != nil {
269
return nil, err
270
}
271
272
return res.Body(), nil
273
}
274
275
func (d *Doubao) initUploadToken() (*UploadToken, error) {
276
uploadToken := &UploadToken{
277
Alice: make(map[string]UploadAuthToken),
278
Samantha: MediaUploadAuthToken{},
279
}
280
281
fileAuthToken, err := d.getUploadAuthToken(FileDataType)
282
if err != nil {
283
return nil, err
284
}
285
286
imgAuthToken, err := d.getUploadAuthToken(ImgDataType)
287
if err != nil {
288
return nil, err
289
}
290
291
mediaAuthToken, err := d.getSamantaUploadAuthToken()
292
if err != nil {
293
return nil, err
294
}
295
296
uploadToken.Alice[FileDataType] = fileAuthToken
297
uploadToken.Alice[ImgDataType] = imgAuthToken
298
uploadToken.Samantha = mediaAuthToken
299
300
return uploadToken, nil
301
}
302
303
func (d *Doubao) getUploadAuthToken(dataType string) (ut UploadAuthToken, err error) {
304
var r UploadAuthTokenResp
305
_, err = d.request("/alice/upload/auth_token", http.MethodPost, func(req *resty.Request) {
306
req.SetBody(base.Json{
307
"scene": "bot_chat",
308
"data_type": dataType,
309
})
310
}, &r)
311
312
return r.Data, err
313
}
314
315
func (d *Doubao) getSamantaUploadAuthToken() (mt MediaUploadAuthToken, err error) {
316
var r MediaUploadAuthTokenResp
317
_, err = d.request("/samantha/media/get_upload_token", http.MethodPost, func(req *resty.Request) {
318
req.SetBody(base.Json{})
319
}, &r)
320
321
return r.Data, err
322
}
323
324
// getUploadConfig 获取上传配置信息
325
func (d *Doubao) getUploadConfig(upConfig *UploadConfig, dataType string, file model.FileStreamer) error {
326
tokenType := dataType
327
// 配置参数函数
328
configureParams := func() (string, map[string]string) {
329
var uploadUrl string
330
var params map[string]string
331
// 根据数据类型设置不同的上传参数
332
switch dataType {
333
case VideoDataType:
334
// 音频/视频类型 - 使用uploadToken.Samantha的配置
335
uploadUrl = d.UploadToken.Samantha.UploadInfo.VideoHost
336
params = map[string]string{
337
"Action": "ApplyUploadInner",
338
"Version": "2020-11-19",
339
"SpaceName": d.UploadToken.Samantha.UploadInfo.SpaceName,
340
"FileType": "video",
341
"IsInner": "1",
342
"NeedFallback": "true",
343
"FileSize": strconv.FormatInt(file.GetSize(), 10),
344
"s": randomString(),
345
}
346
case ImgDataType, FileDataType:
347
// 图片或其他文件类型 - 使用uploadToken.Alice对应配置
348
uploadUrl = "https://" + d.UploadToken.Alice[dataType].UploadHost
349
params = map[string]string{
350
"Action": "ApplyImageUpload",
351
"Version": "2018-08-01",
352
"ServiceId": d.UploadToken.Alice[dataType].ServiceID,
353
"NeedFallback": "true",
354
"FileSize": strconv.FormatInt(file.GetSize(), 10),
355
"FileExtension": filepath.Ext(file.GetName()),
356
"s": randomString(),
357
}
358
}
359
return uploadUrl, params
360
}
361
362
// 获取初始参数
363
uploadUrl, params := configureParams()
364
365
tokenRefreshed := false
366
var configResp UploadConfigResp
367
368
err := d._retryOperation("get upload_config", func() error {
369
configResp = UploadConfigResp{}
370
371
_, err := d.requestApi(uploadUrl, http.MethodGet, tokenType, func(req *resty.Request) {
372
req.SetQueryParams(params)
373
}, &configResp)
374
if err != nil {
375
return err
376
}
377
378
if configResp.ResponseMetadata.Error.Code == "" {
379
*upConfig = configResp.Result
380
return nil
381
}
382
383
// 100028 凭证过期
384
if configResp.ResponseMetadata.Error.CodeN == 100028 && !tokenRefreshed {
385
log.Debugln("[doubao] Upload token expired, re-fetching...")
386
newToken, err := d.initUploadToken()
387
if err != nil {
388
return fmt.Errorf("failed to refresh token: %w", err)
389
}
390
391
d.UploadToken = newToken
392
tokenRefreshed = true
393
uploadUrl, params = configureParams()
394
395
return retry.Error{errors.New("token refreshed, retry needed")}
396
}
397
398
return fmt.Errorf("get upload_config failed: %s", configResp.ResponseMetadata.Error.Message)
399
})
400
401
return err
402
}
403
404
// uploadNode 上传 文件信息
405
func (d *Doubao) uploadNode(uploadConfig *UploadConfig, dir model.Obj, file model.FileStreamer, dataType string) (UploadNodeResp, error) {
406
reqUuid := uuid.New().String()
407
var key string
408
var nodeType int
409
410
mimetype := file.GetMimetype()
411
switch dataType {
412
case VideoDataType:
413
key = uploadConfig.InnerUploadAddress.UploadNodes[0].Vid
414
if strings.HasPrefix(mimetype, "audio/") {
415
nodeType = AudioType // 音频类型
416
} else {
417
nodeType = VideoType // 视频类型
418
}
419
case ImgDataType:
420
key = uploadConfig.InnerUploadAddress.UploadNodes[0].StoreInfos[0].StoreURI
421
nodeType = ImageType // 图片类型
422
default: // FileDataType
423
key = uploadConfig.InnerUploadAddress.UploadNodes[0].StoreInfos[0].StoreURI
424
nodeType = FileType // 文件类型
425
}
426
427
var r UploadNodeResp
428
_, err := d.request("/samantha/aispace/upload_node", http.MethodPost, func(req *resty.Request) {
429
req.SetBody(base.Json{
430
"node_list": []base.Json{
431
{
432
"local_id": reqUuid,
433
"parent_id": dir.GetID(),
434
"name": file.GetName(),
435
"key": key,
436
"node_content": base.Json{},
437
"node_type": nodeType,
438
"size": file.GetSize(),
439
},
440
},
441
"request_id": reqUuid,
442
})
443
}, &r)
444
445
return r, err
446
}
447
448
// Upload 普通上传实现
449
func (d *Doubao) Upload(config *UploadConfig, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, dataType string) (model.Obj, error) {
450
data, err := io.ReadAll(file)
451
if err != nil {
452
return nil, err
453
}
454
455
// 计算CRC32
456
crc32Hash := crc32.NewIEEE()
457
crc32Hash.Write(data)
458
crc32Value := hex.EncodeToString(crc32Hash.Sum(nil))
459
460
// 构建请求路径
461
uploadNode := config.InnerUploadAddress.UploadNodes[0]
462
storeInfo := uploadNode.StoreInfos[0]
463
uploadUrl := fmt.Sprintf("https://%s/upload/v1/%s", uploadNode.UploadHost, storeInfo.StoreURI)
464
465
uploadResp := UploadResp{}
466
467
if _, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {
468
req.SetHeaders(map[string]string{
469
"Content-Type": "application/octet-stream",
470
"Content-Crc32": crc32Value,
471
"Content-Length": fmt.Sprintf("%d", len(data)),
472
"Content-Disposition": fmt.Sprintf("attachment; filename=%s", url.QueryEscape(storeInfo.StoreURI)),
473
})
474
475
req.SetBody(data)
476
}, &uploadResp); err != nil {
477
return nil, err
478
}
479
480
if uploadResp.Code != 2000 {
481
return nil, fmt.Errorf("upload failed: %s", uploadResp.Message)
482
}
483
484
uploadNodeResp, err := d.uploadNode(config, dstDir, file, dataType)
485
if err != nil {
486
return nil, err
487
}
488
489
return &model.Object{
490
ID: uploadNodeResp.Data.NodeList[0].ID,
491
Name: uploadNodeResp.Data.NodeList[0].Name,
492
Size: file.GetSize(),
493
IsFolder: false,
494
}, nil
495
}
496
497
// UploadByMultipart 分片上传
498
func (d *Doubao) UploadByMultipart(ctx context.Context, config *UploadConfig, fileSize int64, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, dataType string) (model.Obj, error) {
499
// 构建请求路径
500
uploadNode := config.InnerUploadAddress.UploadNodes[0]
501
storeInfo := uploadNode.StoreInfos[0]
502
uploadUrl := fmt.Sprintf("https://%s/upload/v1/%s", uploadNode.UploadHost, storeInfo.StoreURI)
503
// 初始化分片上传
504
var uploadID string
505
err := d._retryOperation("Initialize multipart upload", func() error {
506
var err error
507
uploadID, err = d.initMultipartUpload(config, uploadUrl, storeInfo)
508
return err
509
})
510
if err != nil {
511
return nil, fmt.Errorf("failed to initialize multipart upload: %w", err)
512
}
513
// 准备分片参数
514
chunkSize := DefaultChunkSize
515
if config.InnerUploadAddress.AdvanceOption.SliceSize > 0 {
516
chunkSize = int64(config.InnerUploadAddress.AdvanceOption.SliceSize)
517
}
518
totalParts := (fileSize + chunkSize - 1) / chunkSize
519
// 创建分片信息组
520
parts := make([]UploadPart, totalParts)
521
// 缓存文件
522
tempFile, err := file.CacheFullInTempFile()
523
if err != nil {
524
return nil, fmt.Errorf("failed to cache file: %w", err)
525
}
526
defer tempFile.Close()
527
up(10.0) // 更新进度
528
// 设置并行上传
529
threadG, uploadCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
530
retry.Attempts(1),
531
retry.Delay(time.Second),
532
retry.DelayType(retry.BackOffDelay))
533
534
var partsMutex sync.Mutex
535
// 并行上传所有分片
536
for partIndex := int64(0); partIndex < totalParts; partIndex++ {
537
if utils.IsCanceled(uploadCtx) {
538
break
539
}
540
partIndex := partIndex
541
partNumber := partIndex + 1 // 分片编号从1开始
542
543
threadG.Go(func(ctx context.Context) error {
544
// 计算此分片的大小和偏移
545
offset := partIndex * chunkSize
546
size := chunkSize
547
if partIndex == totalParts-1 {
548
size = fileSize - offset
549
}
550
551
limitedReader := driver.NewLimitedUploadStream(ctx, io.NewSectionReader(tempFile, offset, size))
552
// 读取数据到内存
553
data, err := io.ReadAll(limitedReader)
554
if err != nil {
555
return fmt.Errorf("failed to read part %d: %w", partNumber, err)
556
}
557
// 计算CRC32
558
crc32Value := calculateCRC32(data)
559
// 使用_retryOperation上传分片
560
var uploadPart UploadPart
561
if err = d._retryOperation(fmt.Sprintf("Upload part %d", partNumber), func() error {
562
var err error
563
uploadPart, err = d.uploadPart(config, uploadUrl, uploadID, partNumber, data, crc32Value)
564
return err
565
}); err != nil {
566
return fmt.Errorf("part %d upload failed: %w", partNumber, err)
567
}
568
// 记录成功上传的分片
569
partsMutex.Lock()
570
parts[partIndex] = UploadPart{
571
PartNumber: strconv.FormatInt(partNumber, 10),
572
Etag: uploadPart.Etag,
573
Crc32: crc32Value,
574
}
575
partsMutex.Unlock()
576
// 更新进度
577
progress := 10.0 + 90.0*float64(threadG.Success()+1)/float64(totalParts)
578
up(math.Min(progress, 95.0))
579
580
return nil
581
})
582
}
583
584
if err = threadG.Wait(); err != nil {
585
return nil, err
586
}
587
// 完成上传-分片合并
588
if err = d._retryOperation("Complete multipart upload", func() error {
589
return d.completeMultipartUpload(config, uploadUrl, uploadID, parts)
590
}); err != nil {
591
return nil, fmt.Errorf("failed to complete multipart upload: %w", err)
592
}
593
// 提交上传
594
if err = d._retryOperation("Commit upload", func() error {
595
return d.commitMultipartUpload(config)
596
}); err != nil {
597
return nil, fmt.Errorf("failed to commit upload: %w", err)
598
}
599
600
up(98.0) // 更新到98%
601
// 上传节点信息
602
var uploadNodeResp UploadNodeResp
603
604
if err = d._retryOperation("Upload node", func() error {
605
var err error
606
uploadNodeResp, err = d.uploadNode(config, dstDir, file, dataType)
607
return err
608
}); err != nil {
609
return nil, fmt.Errorf("failed to upload node: %w", err)
610
}
611
612
up(100.0) // 完成上传
613
614
return &model.Object{
615
ID: uploadNodeResp.Data.NodeList[0].ID,
616
Name: uploadNodeResp.Data.NodeList[0].Name,
617
Size: file.GetSize(),
618
IsFolder: false,
619
}, nil
620
}
621
622
// 统一上传请求方法
623
func (d *Doubao) uploadRequest(uploadUrl string, method string, storeInfo StoreInfo, callback base.ReqCallback, resp interface{}) ([]byte, error) {
624
client := resty.New()
625
client.SetTransport(&http.Transport{
626
DisableKeepAlives: true, // 禁用连接复用
627
ForceAttemptHTTP2: false, // 强制使用HTTP/1.1
628
})
629
client.SetTimeout(UploadTimeout)
630
631
req := client.R()
632
req.SetHeaders(map[string]string{
633
"Host": strings.Split(uploadUrl, "/")[2],
634
"Referer": BaseURL + "/",
635
"Origin": BaseURL,
636
"User-Agent": UserAgent,
637
"X-Storage-U": d.UserId,
638
"Authorization": storeInfo.Auth,
639
})
640
641
if method == http.MethodPost {
642
req.SetHeader("Content-Type", "text/plain;charset=UTF-8")
643
}
644
645
if callback != nil {
646
callback(req)
647
}
648
649
if resp != nil {
650
req.SetResult(resp)
651
}
652
653
res, err := req.Execute(method, uploadUrl)
654
if err != nil && err != io.EOF {
655
return nil, fmt.Errorf("upload request failed: %w", err)
656
}
657
658
return res.Body(), nil
659
}
660
661
// 初始化分片上传
662
func (d *Doubao) initMultipartUpload(config *UploadConfig, uploadUrl string, storeInfo StoreInfo) (uploadId string, err error) {
663
uploadResp := UploadResp{}
664
665
_, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {
666
req.SetQueryParams(map[string]string{
667
"uploadmode": "part",
668
"phase": "init",
669
})
670
}, &uploadResp)
671
672
if err != nil {
673
return uploadId, err
674
}
675
676
if uploadResp.Code != 2000 {
677
return uploadId, fmt.Errorf("init upload failed: %s", uploadResp.Message)
678
}
679
680
return uploadResp.Data.UploadId, nil
681
}
682
683
// 分片上传实现
684
func (d *Doubao) uploadPart(config *UploadConfig, uploadUrl, uploadID string, partNumber int64, data []byte, crc32Value string) (resp UploadPart, err error) {
685
uploadResp := UploadResp{}
686
storeInfo := config.InnerUploadAddress.UploadNodes[0].StoreInfos[0]
687
688
_, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {
689
req.SetHeaders(map[string]string{
690
"Content-Type": "application/octet-stream",
691
"Content-Crc32": crc32Value,
692
"Content-Length": fmt.Sprintf("%d", len(data)),
693
"Content-Disposition": fmt.Sprintf("attachment; filename=%s", url.QueryEscape(storeInfo.StoreURI)),
694
})
695
696
req.SetQueryParams(map[string]string{
697
"uploadid": uploadID,
698
"part_number": strconv.FormatInt(partNumber, 10),
699
"phase": "transfer",
700
})
701
702
req.SetBody(data)
703
req.SetContentLength(true)
704
}, &uploadResp)
705
706
if err != nil {
707
return resp, err
708
}
709
710
if uploadResp.Code != 2000 {
711
return resp, fmt.Errorf("upload part failed: %s", uploadResp.Message)
712
} else if uploadResp.Data.Crc32 != crc32Value {
713
return resp, fmt.Errorf("upload part failed: crc32 mismatch, expected %s, got %s", crc32Value, uploadResp.Data.Crc32)
714
}
715
716
return uploadResp.Data, nil
717
}
718
719
// 完成分片上传
720
func (d *Doubao) completeMultipartUpload(config *UploadConfig, uploadUrl, uploadID string, parts []UploadPart) error {
721
uploadResp := UploadResp{}
722
723
storeInfo := config.InnerUploadAddress.UploadNodes[0].StoreInfos[0]
724
725
body := _convertUploadParts(parts)
726
727
err := utils.Retry(MaxRetryAttempts, time.Second, func() (err error) {
728
_, err = d.uploadRequest(uploadUrl, http.MethodPost, storeInfo, func(req *resty.Request) {
729
req.SetQueryParams(map[string]string{
730
"uploadid": uploadID,
731
"phase": "finish",
732
"uploadmode": "part",
733
})
734
req.SetBody(body)
735
}, &uploadResp)
736
737
if err != nil {
738
return err
739
}
740
// 检查响应状态码 2000 成功 4024 分片合并中
741
if uploadResp.Code != 2000 && uploadResp.Code != 4024 {
742
return fmt.Errorf("finish upload failed: %s", uploadResp.Message)
743
}
744
745
return err
746
})
747
748
if err != nil {
749
return fmt.Errorf("failed to complete multipart upload: %w", err)
750
}
751
752
return nil
753
}
754
755
func (d *Doubao) commitMultipartUpload(uploadConfig *UploadConfig) error {
756
uploadUrl := d.UploadToken.Samantha.UploadInfo.VideoHost
757
params := map[string]string{
758
"Action": "CommitUploadInner",
759
"Version": "2020-11-19",
760
"SpaceName": d.UploadToken.Samantha.UploadInfo.SpaceName,
761
}
762
tokenType := VideoDataType
763
764
videoCommitUploadResp := VideoCommitUploadResp{}
765
766
jsonBytes, err := json.Marshal(base.Json{
767
"SessionKey": uploadConfig.InnerUploadAddress.UploadNodes[0].SessionKey,
768
"Functions": []base.Json{},
769
})
770
if err != nil {
771
return fmt.Errorf("failed to marshal request data: %w", err)
772
}
773
774
_, err = d.requestApi(uploadUrl, http.MethodPost, tokenType, func(req *resty.Request) {
775
req.SetHeader("Content-Type", "application/json")
776
req.SetQueryParams(params)
777
req.SetBody(jsonBytes)
778
779
}, &videoCommitUploadResp)
780
if err != nil {
781
return err
782
}
783
784
return nil
785
}
786
787
// 计算CRC32
788
func calculateCRC32(data []byte) string {
789
hash := crc32.NewIEEE()
790
hash.Write(data)
791
return hex.EncodeToString(hash.Sum(nil))
792
}
793
794
// _retryOperation 操作重试
795
func (d *Doubao) _retryOperation(operation string, fn func() error) error {
796
return retry.Do(
797
fn,
798
retry.Attempts(MaxRetryAttempts),
799
retry.Delay(500*time.Millisecond),
800
retry.DelayType(retry.BackOffDelay),
801
retry.MaxJitter(200*time.Millisecond),
802
retry.OnRetry(func(n uint, err error) {
803
log.Debugf("[doubao] %s retry #%d: %v", operation, n+1, err)
804
}),
805
)
806
}
807
808
// _convertUploadParts 将分片信息转换为字符串
809
func _convertUploadParts(parts []UploadPart) string {
810
if len(parts) == 0 {
811
return ""
812
}
813
814
var result strings.Builder
815
816
for i, part := range parts {
817
if i > 0 {
818
result.WriteString(",")
819
}
820
result.WriteString(fmt.Sprintf("%s:%s", part.PartNumber, part.Crc32))
821
}
822
823
return result.String()
824
}
825
826
// 获取规范查询字符串
827
func getCanonicalQueryString(query url.Values) string {
828
if len(query) == 0 {
829
return ""
830
}
831
832
keys := make([]string, 0, len(query))
833
for k := range query {
834
keys = append(keys, k)
835
}
836
sort.Strings(keys)
837
838
parts := make([]string, 0, len(keys))
839
for _, k := range keys {
840
values := query[k]
841
for _, v := range values {
842
parts = append(parts, urlEncode(k)+"="+urlEncode(v))
843
}
844
}
845
846
return strings.Join(parts, "&")
847
}
848
849
func urlEncode(s string) string {
850
s = url.QueryEscape(s)
851
s = strings.ReplaceAll(s, "+", "%20")
852
return s
853
}
854
855
// 获取规范头信息和已签名头列表
856
func getCanonicalHeadersFromMap(headers map[string][]string) (string, string) {
857
// 不可签名的头部列表
858
unsignableHeaders := map[string]bool{
859
"authorization": true,
860
"content-type": true,
861
"content-length": true,
862
"user-agent": true,
863
"presigned-expires": true,
864
"expect": true,
865
"x-amzn-trace-id": true,
866
}
867
headerValues := make(map[string]string)
868
var signedHeadersList []string
869
870
for k, v := range headers {
871
if len(v) == 0 {
872
continue
873
}
874
875
lowerKey := strings.ToLower(k)
876
// 检查是否可签名
877
if strings.HasPrefix(lowerKey, "x-amz-") || !unsignableHeaders[lowerKey] {
878
value := strings.TrimSpace(v[0])
879
value = strings.Join(strings.Fields(value), " ")
880
headerValues[lowerKey] = value
881
signedHeadersList = append(signedHeadersList, lowerKey)
882
}
883
}
884
885
sort.Strings(signedHeadersList)
886
887
var canonicalHeadersStr strings.Builder
888
for _, key := range signedHeadersList {
889
canonicalHeadersStr.WriteString(key)
890
canonicalHeadersStr.WriteString(":")
891
canonicalHeadersStr.WriteString(headerValues[key])
892
canonicalHeadersStr.WriteString("\n")
893
}
894
895
signedHeaders := strings.Join(signedHeadersList, ";")
896
897
return canonicalHeadersStr.String(), signedHeaders
898
}
899
900
// 计算HMAC-SHA256
901
func hmacSHA256(key []byte, data string) []byte {
902
h := hmac.New(sha256.New, key)
903
h.Write([]byte(data))
904
return h.Sum(nil)
905
}
906
907
// 计算HMAC-SHA256并返回十六进制字符串
908
func hmacSHA256Hex(key []byte, data string) string {
909
return hex.EncodeToString(hmacSHA256(key, data))
910
}
911
912
// 计算SHA256哈希并返回十六进制字符串
913
func hashSHA256(data string) string {
914
h := sha256.New()
915
h.Write([]byte(data))
916
return hex.EncodeToString(h.Sum(nil))
917
}
918
919
// 获取签名密钥
920
func getSigningKey(secretKey, dateStamp, region, service string) []byte {
921
kDate := hmacSHA256([]byte("AWS4"+secretKey), dateStamp)
922
kRegion := hmacSHA256(kDate, region)
923
kService := hmacSHA256(kRegion, service)
924
kSigning := hmacSHA256(kService, "aws4_request")
925
return kSigning
926
}
927
928
// generateContentDisposition 生成符合RFC 5987标准的Content-Disposition头部
929
func generateContentDisposition(filename string) string {
930
// 按照RFC 2047进行编码,用于filename部分
931
encodedName := urlEncode(filename)
932
933
// 按照RFC 5987进行编码,用于filename*部分
934
encodedNameRFC5987 := encodeRFC5987(filename)
935
936
return fmt.Sprintf("attachment; filename=\"%s\"; filename*=utf-8''%s",
937
encodedName, encodedNameRFC5987)
938
}
939
940
// encodeRFC5987 按照RFC 5987规范编码字符串,适用于HTTP头部参数中的非ASCII字符
941
func encodeRFC5987(s string) string {
942
var buf strings.Builder
943
for _, r := range []byte(s) {
944
// 根据RFC 5987,只有字母、数字和部分特殊符号可以不编码
945
if (r >= 'a' && r <= 'z') ||
946
(r >= 'A' && r <= 'Z') ||
947
(r >= '0' && r <= '9') ||
948
r == '-' || r == '.' || r == '_' || r == '~' {
949
buf.WriteByte(r)
950
} else {
951
// 其他字符都需要百分号编码
952
fmt.Fprintf(&buf, "%%%02X", r)
953
}
954
}
955
return buf.String()
956
}
957
958
func randomString() string {
959
const charset = "0123456789abcdefghijklmnopqrstuvwxyz"
960
const length = 11 // 11位随机字符串
961
962
var sb strings.Builder
963
sb.Grow(length)
964
965
for i := 0; i < length; i++ {
966
sb.WriteByte(charset[rand.Intn(len(charset))])
967
}
968
969
return sb.String()
970
}
971
972