Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/bitqiu/driver.go
1986 views
1
package bitqiu
2
3
import (
4
"context"
5
"encoding/json"
6
"fmt"
7
"io"
8
"net/http/cookiejar"
9
"path"
10
"strconv"
11
"strings"
12
"time"
13
14
"github.com/alist-org/alist/v3/drivers/base"
15
"github.com/alist-org/alist/v3/internal/driver"
16
"github.com/alist-org/alist/v3/internal/errs"
17
"github.com/alist-org/alist/v3/internal/model"
18
"github.com/alist-org/alist/v3/internal/op"
19
streamPkg "github.com/alist-org/alist/v3/internal/stream"
20
"github.com/alist-org/alist/v3/pkg/utils"
21
"github.com/go-resty/resty/v2"
22
"github.com/google/uuid"
23
)
24
25
const (
26
baseURL = "https://pan.bitqiu.com"
27
loginURL = baseURL + "/loginServer/login"
28
userInfoURL = baseURL + "/user/getInfo"
29
listURL = baseURL + "/apiToken/cfi/fs/resources/pages"
30
uploadInitializeURL = baseURL + "/apiToken/cfi/fs/upload/v2/initialize"
31
uploadCompleteURL = baseURL + "/apiToken/cfi/fs/upload/v2/complete"
32
downloadURL = baseURL + "/download/getUrl"
33
createDirURL = baseURL + "/resource/create"
34
moveResourceURL = baseURL + "/resource/remove"
35
renameResourceURL = baseURL + "/resource/rename"
36
copyResourceURL = baseURL + "/apiToken/cfi/fs/async/copy"
37
copyManagerURL = baseURL + "/apiToken/cfi/fs/async/manager"
38
deleteResourceURL = baseURL + "/resource/delete"
39
40
successCode = "10200"
41
uploadSuccessCode = "30010"
42
copySubmittedCode = "10300"
43
orgChannel = "default|default|default"
44
)
45
46
const (
47
copyPollInterval = time.Second
48
copyPollMaxAttempts = 60
49
chunkSize = int64(1 << 20)
50
)
51
52
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
53
54
type BitQiu struct {
55
model.Storage
56
Addition
57
58
client *resty.Client
59
userID string
60
}
61
62
func (d *BitQiu) Config() driver.Config {
63
return config
64
}
65
66
func (d *BitQiu) GetAddition() driver.Additional {
67
return &d.Addition
68
}
69
70
func (d *BitQiu) Init(ctx context.Context) error {
71
if d.Addition.UserPlatform == "" {
72
d.Addition.UserPlatform = uuid.NewString()
73
op.MustSaveDriverStorage(d)
74
}
75
76
if d.client == nil {
77
jar, err := cookiejar.New(nil)
78
if err != nil {
79
return err
80
}
81
d.client = base.NewRestyClient()
82
d.client.SetBaseURL(baseURL)
83
d.client.SetCookieJar(jar)
84
}
85
d.client.SetHeader("user-agent", d.userAgent())
86
87
return d.login(ctx)
88
}
89
90
func (d *BitQiu) Drop(ctx context.Context) error {
91
d.client = nil
92
d.userID = ""
93
return nil
94
}
95
96
func (d *BitQiu) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
97
if d.userID == "" {
98
if err := d.login(ctx); err != nil {
99
return nil, err
100
}
101
}
102
103
parentID := d.resolveParentID(dir)
104
dirPath := ""
105
if dir != nil {
106
dirPath = dir.GetPath()
107
}
108
pageSize := d.pageSize()
109
orderType := d.orderType()
110
desc := d.orderDesc()
111
112
var results []model.Obj
113
page := 1
114
for {
115
form := map[string]string{
116
"parentId": parentID,
117
"limit": strconv.Itoa(pageSize),
118
"orderType": orderType,
119
"desc": desc,
120
"model": "1",
121
"userId": d.userID,
122
"currentPage": strconv.Itoa(page),
123
"page": strconv.Itoa(page),
124
"org_channel": orgChannel,
125
}
126
var resp Response[ResourcePage]
127
if err := d.postForm(ctx, listURL, form, &resp); err != nil {
128
return nil, err
129
}
130
if resp.Code != successCode {
131
if resp.Code == "10401" || resp.Code == "10404" {
132
if err := d.login(ctx); err != nil {
133
return nil, err
134
}
135
continue
136
}
137
return nil, fmt.Errorf("list failed: %s", resp.Message)
138
}
139
140
objs, err := utils.SliceConvert(resp.Data.Data, func(item Resource) (model.Obj, error) {
141
return item.toObject(parentID, dirPath)
142
})
143
if err != nil {
144
return nil, err
145
}
146
results = append(results, objs...)
147
148
if !resp.Data.HasNext || len(resp.Data.Data) == 0 {
149
break
150
}
151
page++
152
}
153
154
return results, nil
155
}
156
157
func (d *BitQiu) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
158
if file.IsDir() {
159
return nil, errs.NotFile
160
}
161
if d.userID == "" {
162
if err := d.login(ctx); err != nil {
163
return nil, err
164
}
165
}
166
167
form := map[string]string{
168
"fileIds": file.GetID(),
169
"org_channel": orgChannel,
170
}
171
for attempt := 0; attempt < 2; attempt++ {
172
var resp Response[DownloadData]
173
if err := d.postForm(ctx, downloadURL, form, &resp); err != nil {
174
return nil, err
175
}
176
switch resp.Code {
177
case successCode:
178
if resp.Data.URL == "" {
179
return nil, fmt.Errorf("empty download url returned")
180
}
181
return &model.Link{URL: resp.Data.URL}, nil
182
case "10401", "10404":
183
if err := d.login(ctx); err != nil {
184
return nil, err
185
}
186
default:
187
return nil, fmt.Errorf("get link failed: %s", resp.Message)
188
}
189
}
190
return nil, fmt.Errorf("get link failed: retry limit reached")
191
}
192
193
func (d *BitQiu) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
194
if d.userID == "" {
195
if err := d.login(ctx); err != nil {
196
return nil, err
197
}
198
}
199
200
parentID := d.resolveParentID(parentDir)
201
parentPath := ""
202
if parentDir != nil {
203
parentPath = parentDir.GetPath()
204
}
205
form := map[string]string{
206
"parentId": parentID,
207
"name": dirName,
208
"org_channel": orgChannel,
209
}
210
for attempt := 0; attempt < 2; attempt++ {
211
var resp Response[CreateDirData]
212
if err := d.postForm(ctx, createDirURL, form, &resp); err != nil {
213
return nil, err
214
}
215
switch resp.Code {
216
case successCode:
217
newParentID := parentID
218
if resp.Data.ParentID != "" {
219
newParentID = resp.Data.ParentID
220
}
221
name := resp.Data.Name
222
if name == "" {
223
name = dirName
224
}
225
resource := Resource{
226
ResourceID: resp.Data.DirID,
227
ResourceType: 1,
228
Name: name,
229
ParentID: newParentID,
230
}
231
obj, err := resource.toObject(newParentID, parentPath)
232
if err != nil {
233
return nil, err
234
}
235
if o, ok := obj.(*Object); ok {
236
o.ParentID = newParentID
237
}
238
return obj, nil
239
case "10401", "10404":
240
if err := d.login(ctx); err != nil {
241
return nil, err
242
}
243
default:
244
return nil, fmt.Errorf("create folder failed: %s", resp.Message)
245
}
246
}
247
return nil, fmt.Errorf("create folder failed: retry limit reached")
248
}
249
250
func (d *BitQiu) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
251
if d.userID == "" {
252
if err := d.login(ctx); err != nil {
253
return nil, err
254
}
255
}
256
257
targetParentID := d.resolveParentID(dstDir)
258
form := map[string]string{
259
"dirIds": "",
260
"fileIds": "",
261
"parentId": targetParentID,
262
"org_channel": orgChannel,
263
}
264
if srcObj.IsDir() {
265
form["dirIds"] = srcObj.GetID()
266
} else {
267
form["fileIds"] = srcObj.GetID()
268
}
269
270
for attempt := 0; attempt < 2; attempt++ {
271
var resp Response[any]
272
if err := d.postForm(ctx, moveResourceURL, form, &resp); err != nil {
273
return nil, err
274
}
275
switch resp.Code {
276
case successCode:
277
dstPath := ""
278
if dstDir != nil {
279
dstPath = dstDir.GetPath()
280
}
281
if setter, ok := srcObj.(model.SetPath); ok {
282
setter.SetPath(path.Join(dstPath, srcObj.GetName()))
283
}
284
if o, ok := srcObj.(*Object); ok {
285
o.ParentID = targetParentID
286
}
287
return srcObj, nil
288
case "10401", "10404":
289
if err := d.login(ctx); err != nil {
290
return nil, err
291
}
292
default:
293
return nil, fmt.Errorf("move failed: %s", resp.Message)
294
}
295
}
296
return nil, fmt.Errorf("move failed: retry limit reached")
297
}
298
299
func (d *BitQiu) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
300
if d.userID == "" {
301
if err := d.login(ctx); err != nil {
302
return nil, err
303
}
304
}
305
306
form := map[string]string{
307
"resourceId": srcObj.GetID(),
308
"name": newName,
309
"type": "0",
310
"org_channel": orgChannel,
311
}
312
if srcObj.IsDir() {
313
form["type"] = "1"
314
}
315
316
for attempt := 0; attempt < 2; attempt++ {
317
var resp Response[any]
318
if err := d.postForm(ctx, renameResourceURL, form, &resp); err != nil {
319
return nil, err
320
}
321
switch resp.Code {
322
case successCode:
323
return updateObjectName(srcObj, newName), nil
324
case "10401", "10404":
325
if err := d.login(ctx); err != nil {
326
return nil, err
327
}
328
default:
329
return nil, fmt.Errorf("rename failed: %s", resp.Message)
330
}
331
}
332
return nil, fmt.Errorf("rename failed: retry limit reached")
333
}
334
335
func (d *BitQiu) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
336
if d.userID == "" {
337
if err := d.login(ctx); err != nil {
338
return nil, err
339
}
340
}
341
342
targetParentID := d.resolveParentID(dstDir)
343
form := map[string]string{
344
"dirIds": "",
345
"fileIds": "",
346
"parentId": targetParentID,
347
"org_channel": orgChannel,
348
}
349
if srcObj.IsDir() {
350
form["dirIds"] = srcObj.GetID()
351
} else {
352
form["fileIds"] = srcObj.GetID()
353
}
354
355
for attempt := 0; attempt < 2; attempt++ {
356
var resp Response[any]
357
if err := d.postForm(ctx, copyResourceURL, form, &resp); err != nil {
358
return nil, err
359
}
360
switch resp.Code {
361
case successCode, copySubmittedCode:
362
return d.waitForCopiedObject(ctx, srcObj, dstDir)
363
case "10401", "10404":
364
if err := d.login(ctx); err != nil {
365
return nil, err
366
}
367
default:
368
return nil, fmt.Errorf("copy failed: %s", resp.Message)
369
}
370
}
371
372
return nil, fmt.Errorf("copy failed: retry limit reached")
373
}
374
375
func (d *BitQiu) Remove(ctx context.Context, obj model.Obj) error {
376
if d.userID == "" {
377
if err := d.login(ctx); err != nil {
378
return err
379
}
380
}
381
382
form := map[string]string{
383
"dirIds": "",
384
"fileIds": "",
385
"org_channel": orgChannel,
386
}
387
if obj.IsDir() {
388
form["dirIds"] = obj.GetID()
389
} else {
390
form["fileIds"] = obj.GetID()
391
}
392
393
for attempt := 0; attempt < 2; attempt++ {
394
var resp Response[any]
395
if err := d.postForm(ctx, deleteResourceURL, form, &resp); err != nil {
396
return err
397
}
398
switch resp.Code {
399
case successCode:
400
return nil
401
case "10401", "10404":
402
if err := d.login(ctx); err != nil {
403
return err
404
}
405
default:
406
return fmt.Errorf("remove failed: %s", resp.Message)
407
}
408
}
409
return fmt.Errorf("remove failed: retry limit reached")
410
}
411
412
func (d *BitQiu) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
413
if d.userID == "" {
414
if err := d.login(ctx); err != nil {
415
return nil, err
416
}
417
}
418
419
up(0)
420
tmpFile, md5sum, err := streamPkg.CacheFullInTempFileAndHash(file, utils.MD5)
421
if err != nil {
422
return nil, err
423
}
424
defer tmpFile.Close()
425
426
parentID := d.resolveParentID(dstDir)
427
parentPath := ""
428
if dstDir != nil {
429
parentPath = dstDir.GetPath()
430
}
431
form := map[string]string{
432
"parentId": parentID,
433
"name": file.GetName(),
434
"size": strconv.FormatInt(file.GetSize(), 10),
435
"hash": md5sum,
436
"sampleMd5": md5sum,
437
"org_channel": orgChannel,
438
}
439
var resp Response[json.RawMessage]
440
if err = d.postForm(ctx, uploadInitializeURL, form, &resp); err != nil {
441
return nil, err
442
}
443
if resp.Code != uploadSuccessCode {
444
switch resp.Code {
445
case successCode:
446
var initData UploadInitData
447
if err := json.Unmarshal(resp.Data, &initData); err != nil {
448
return nil, fmt.Errorf("parse upload init response failed: %w", err)
449
}
450
serverCode, err := d.uploadFileInChunks(ctx, tmpFile, file.GetSize(), md5sum, initData, up)
451
if err != nil {
452
return nil, err
453
}
454
obj, err := d.completeChunkUpload(ctx, initData, parentID, parentPath, file.GetName(), file.GetSize(), md5sum, serverCode)
455
if err != nil {
456
return nil, err
457
}
458
up(100)
459
return obj, nil
460
default:
461
return nil, fmt.Errorf("upload failed: %s", resp.Message)
462
}
463
}
464
465
var resource Resource
466
if err := json.Unmarshal(resp.Data, &resource); err != nil {
467
return nil, fmt.Errorf("parse upload response failed: %w", err)
468
}
469
obj, err := resource.toObject(parentID, parentPath)
470
if err != nil {
471
return nil, err
472
}
473
up(100)
474
return obj, nil
475
}
476
477
func (d *BitQiu) uploadFileInChunks(ctx context.Context, tmpFile model.File, size int64, md5sum string, initData UploadInitData, up driver.UpdateProgress) (string, error) {
478
if d.client == nil {
479
return "", fmt.Errorf("client not initialized")
480
}
481
if size <= 0 {
482
return "", fmt.Errorf("invalid file size")
483
}
484
buf := make([]byte, chunkSize)
485
offset := int64(0)
486
var finishedFlag string
487
488
for offset < size {
489
chunkLen := chunkSize
490
remaining := size - offset
491
if remaining < chunkLen {
492
chunkLen = remaining
493
}
494
495
reader := io.NewSectionReader(tmpFile, offset, chunkLen)
496
chunkBuf := buf[:chunkLen]
497
if _, err := io.ReadFull(reader, chunkBuf); err != nil {
498
return "", fmt.Errorf("read chunk failed: %w", err)
499
}
500
501
headers := map[string]string{
502
"accept": "*/*",
503
"content-type": "application/octet-stream",
504
"appid": initData.AppID,
505
"token": initData.Token,
506
"userid": strconv.FormatInt(initData.UserID, 10),
507
"serialnumber": initData.SerialNumber,
508
"hash": md5sum,
509
"len": strconv.FormatInt(chunkLen, 10),
510
"offset": strconv.FormatInt(offset, 10),
511
"user-agent": d.userAgent(),
512
}
513
514
var chunkResp ChunkUploadResponse
515
req := d.client.R().
516
SetContext(ctx).
517
SetHeaders(headers).
518
SetBody(chunkBuf).
519
SetResult(&chunkResp)
520
521
if _, err := req.Post(initData.UploadURL); err != nil {
522
return "", err
523
}
524
if chunkResp.ErrCode != 0 {
525
return "", fmt.Errorf("chunk upload failed with code %d", chunkResp.ErrCode)
526
}
527
finishedFlag = chunkResp.FinishedFlag
528
offset += chunkLen
529
up(float64(offset) * 100 / float64(size))
530
}
531
532
if finishedFlag == "" {
533
return "", fmt.Errorf("upload finished without server code")
534
}
535
return finishedFlag, nil
536
}
537
538
func (d *BitQiu) completeChunkUpload(ctx context.Context, initData UploadInitData, parentID, parentPath, name string, size int64, md5sum, serverCode string) (model.Obj, error) {
539
form := map[string]string{
540
"currentPage": "1",
541
"limit": "1",
542
"userId": strconv.FormatInt(initData.UserID, 10),
543
"status": "0",
544
"parentId": parentID,
545
"name": name,
546
"fileUid": initData.FileUID,
547
"fileSid": initData.FileSID,
548
"size": strconv.FormatInt(size, 10),
549
"serverCode": serverCode,
550
"snapTime": "",
551
"hash": md5sum,
552
"sampleMd5": md5sum,
553
"org_channel": orgChannel,
554
}
555
556
var resp Response[Resource]
557
if err := d.postForm(ctx, uploadCompleteURL, form, &resp); err != nil {
558
return nil, err
559
}
560
if resp.Code != successCode {
561
return nil, fmt.Errorf("complete upload failed: %s", resp.Message)
562
}
563
564
return resp.Data.toObject(parentID, parentPath)
565
}
566
567
func (d *BitQiu) login(ctx context.Context) error {
568
if d.client == nil {
569
return fmt.Errorf("client not initialized")
570
}
571
572
form := map[string]string{
573
"passport": d.Username,
574
"password": utils.GetMD5EncodeStr(d.Password),
575
"remember": "0",
576
"captcha": "",
577
"org_channel": orgChannel,
578
}
579
var resp Response[LoginData]
580
if err := d.postForm(ctx, loginURL, form, &resp); err != nil {
581
return err
582
}
583
if resp.Code != successCode {
584
return fmt.Errorf("login failed: %s", resp.Message)
585
}
586
d.userID = strconv.FormatInt(resp.Data.UserID, 10)
587
return d.ensureRootFolderID(ctx)
588
}
589
590
func (d *BitQiu) ensureRootFolderID(ctx context.Context) error {
591
rootID := d.Addition.GetRootId()
592
if rootID != "" && rootID != "0" {
593
return nil
594
}
595
596
form := map[string]string{
597
"org_channel": orgChannel,
598
}
599
var resp Response[UserInfoData]
600
if err := d.postForm(ctx, userInfoURL, form, &resp); err != nil {
601
return err
602
}
603
if resp.Code != successCode {
604
return fmt.Errorf("get user info failed: %s", resp.Message)
605
}
606
if resp.Data.RootDirID == "" {
607
return fmt.Errorf("get user info failed: empty root dir id")
608
}
609
if d.Addition.RootFolderID != resp.Data.RootDirID {
610
d.Addition.RootFolderID = resp.Data.RootDirID
611
op.MustSaveDriverStorage(d)
612
}
613
return nil
614
}
615
616
func (d *BitQiu) postForm(ctx context.Context, url string, form map[string]string, result interface{}) error {
617
if d.client == nil {
618
return fmt.Errorf("client not initialized")
619
}
620
req := d.client.R().
621
SetContext(ctx).
622
SetHeaders(d.commonHeaders()).
623
SetFormData(form)
624
if result != nil {
625
req = req.SetResult(result)
626
}
627
_, err := req.Post(url)
628
return err
629
}
630
631
func (d *BitQiu) waitForCopiedObject(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
632
expectedName := srcObj.GetName()
633
expectedIsDir := srcObj.IsDir()
634
var lastListErr error
635
636
for attempt := 0; attempt < copyPollMaxAttempts; attempt++ {
637
if attempt > 0 {
638
if err := waitWithContext(ctx, copyPollInterval); err != nil {
639
return nil, err
640
}
641
}
642
643
if err := d.checkCopyFailure(ctx); err != nil {
644
return nil, err
645
}
646
647
obj, err := d.findObjectInDir(ctx, dstDir, expectedName, expectedIsDir)
648
if err != nil {
649
lastListErr = err
650
continue
651
}
652
if obj != nil {
653
return obj, nil
654
}
655
}
656
if lastListErr != nil {
657
return nil, lastListErr
658
}
659
return nil, fmt.Errorf("copy task timed out waiting for completion")
660
}
661
662
func (d *BitQiu) checkCopyFailure(ctx context.Context) error {
663
form := map[string]string{
664
"org_channel": orgChannel,
665
}
666
for attempt := 0; attempt < 2; attempt++ {
667
var resp Response[AsyncManagerData]
668
if err := d.postForm(ctx, copyManagerURL, form, &resp); err != nil {
669
return err
670
}
671
switch resp.Code {
672
case successCode:
673
if len(resp.Data.FailTasks) > 0 {
674
return fmt.Errorf("copy failed: %s", resp.Data.FailTasks[0].ErrorMessage())
675
}
676
return nil
677
case "10401", "10404":
678
if err := d.login(ctx); err != nil {
679
return err
680
}
681
default:
682
return fmt.Errorf("query copy status failed: %s", resp.Message)
683
}
684
}
685
return fmt.Errorf("query copy status failed: retry limit reached")
686
}
687
688
func (d *BitQiu) findObjectInDir(ctx context.Context, dir model.Obj, name string, isDir bool) (model.Obj, error) {
689
objs, err := d.List(ctx, dir, model.ListArgs{})
690
if err != nil {
691
return nil, err
692
}
693
for _, obj := range objs {
694
if obj.GetName() == name && obj.IsDir() == isDir {
695
return obj, nil
696
}
697
}
698
return nil, nil
699
}
700
701
func waitWithContext(ctx context.Context, d time.Duration) error {
702
timer := time.NewTimer(d)
703
defer timer.Stop()
704
select {
705
case <-ctx.Done():
706
return ctx.Err()
707
case <-timer.C:
708
return nil
709
}
710
}
711
712
func (d *BitQiu) commonHeaders() map[string]string {
713
headers := map[string]string{
714
"accept": "application/json, text/plain, */*",
715
"accept-language": "en-US,en;q=0.9",
716
"cache-control": "no-cache",
717
"pragma": "no-cache",
718
"user-platform": d.Addition.UserPlatform,
719
"x-kl-saas-ajax-request": "Ajax_Request",
720
"x-requested-with": "XMLHttpRequest",
721
"referer": baseURL + "/",
722
"origin": baseURL,
723
"user-agent": d.userAgent(),
724
}
725
return headers
726
}
727
728
func (d *BitQiu) userAgent() string {
729
if ua := strings.TrimSpace(d.Addition.UserAgent); ua != "" {
730
return ua
731
}
732
return defaultUserAgent
733
}
734
735
func (d *BitQiu) resolveParentID(dir model.Obj) string {
736
if dir != nil && dir.GetID() != "" {
737
return dir.GetID()
738
}
739
if root := d.Addition.GetRootId(); root != "" {
740
return root
741
}
742
return config.DefaultRoot
743
}
744
745
func (d *BitQiu) pageSize() int {
746
if size, err := strconv.Atoi(d.Addition.PageSize); err == nil && size > 0 {
747
return size
748
}
749
return 24
750
}
751
752
func (d *BitQiu) orderType() string {
753
if d.Addition.OrderType != "" {
754
return d.Addition.OrderType
755
}
756
return "updateTime"
757
}
758
759
func (d *BitQiu) orderDesc() string {
760
if d.Addition.OrderDesc {
761
return "1"
762
}
763
return "0"
764
}
765
766
var _ driver.Driver = (*BitQiu)(nil)
767
var _ driver.PutResult = (*BitQiu)(nil)
768
769