Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/cloudreve_v4/util.go
1987 views
1
package cloudreve_v4
2
3
import (
4
"bytes"
5
"context"
6
"encoding/base64"
7
"encoding/json"
8
"errors"
9
"fmt"
10
"io"
11
"net/http"
12
"strconv"
13
"strings"
14
"time"
15
16
"github.com/alist-org/alist/v3/drivers/base"
17
"github.com/alist-org/alist/v3/internal/conf"
18
"github.com/alist-org/alist/v3/internal/driver"
19
"github.com/alist-org/alist/v3/internal/model"
20
"github.com/alist-org/alist/v3/internal/op"
21
"github.com/alist-org/alist/v3/internal/setting"
22
"github.com/alist-org/alist/v3/pkg/utils"
23
"github.com/go-resty/resty/v2"
24
jsoniter "github.com/json-iterator/go"
25
)
26
27
// do others that not defined in Driver interface
28
29
func (d *CloudreveV4) getUA() string {
30
if d.CustomUA != "" {
31
return d.CustomUA
32
}
33
return base.UserAgent
34
}
35
36
func (d *CloudreveV4) request(method string, path string, callback base.ReqCallback, out any) error {
37
if d.ref != nil {
38
return d.ref.request(method, path, callback, out)
39
}
40
u := d.Address + "/api/v4" + path
41
req := base.RestyClient.R()
42
req.SetHeaders(map[string]string{
43
"Accept": "application/json, text/plain, */*",
44
"User-Agent": d.getUA(),
45
})
46
if d.AccessToken != "" {
47
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
48
}
49
50
var r Resp
51
req.SetResult(&r)
52
53
if callback != nil {
54
callback(req)
55
}
56
57
resp, err := req.Execute(method, u)
58
if err != nil {
59
return err
60
}
61
if !resp.IsSuccess() {
62
return errors.New(resp.String())
63
}
64
65
if r.Code != 0 {
66
if r.Code == 401 && d.RefreshToken != "" && path != "/session/token/refresh" {
67
// try to refresh token
68
err = d.refreshToken()
69
if err != nil {
70
return err
71
}
72
return d.request(method, path, callback, out)
73
}
74
return errors.New(r.Msg)
75
}
76
77
if out != nil && r.Data != nil {
78
var marshal []byte
79
marshal, err = json.Marshal(r.Data)
80
if err != nil {
81
return err
82
}
83
err = json.Unmarshal(marshal, out)
84
if err != nil {
85
return err
86
}
87
}
88
89
return nil
90
}
91
92
func (d *CloudreveV4) login() error {
93
var siteConfig SiteLoginConfigResp
94
err := d.request(http.MethodGet, "/site/config/login", nil, &siteConfig)
95
if err != nil {
96
return err
97
}
98
if !siteConfig.Authn {
99
return errors.New("authn not support")
100
}
101
var prepareLogin PrepareLoginResp
102
err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
103
if err != nil {
104
return err
105
}
106
if !prepareLogin.PasswordEnabled {
107
return errors.New("password not enabled")
108
}
109
if prepareLogin.WebauthnEnabled {
110
return errors.New("webauthn not support")
111
}
112
for range 5 {
113
err = d.doLogin(siteConfig.LoginCaptcha)
114
if err == nil {
115
break
116
}
117
if err.Error() != "CAPTCHA not match." {
118
break
119
}
120
}
121
return err
122
}
123
124
func (d *CloudreveV4) doLogin(needCaptcha bool) error {
125
var err error
126
loginBody := base.Json{
127
"email": d.Username,
128
"password": d.Password,
129
}
130
if needCaptcha {
131
var config BasicConfigResp
132
err = d.request(http.MethodGet, "/site/config/basic", nil, &config)
133
if err != nil {
134
return err
135
}
136
if config.CaptchaType != "normal" {
137
return fmt.Errorf("captcha type %s not support", config.CaptchaType)
138
}
139
var captcha CaptchaResp
140
err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
141
if err != nil {
142
return err
143
}
144
if !strings.HasPrefix(captcha.Image, "data:image/png;base64,") {
145
return errors.New("can not get captcha")
146
}
147
loginBody["ticket"] = captcha.Ticket
148
i := strings.Index(captcha.Image, ",")
149
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha.Image[i+1:]))
150
vRes, err := base.RestyClient.R().SetMultipartField(
151
"image", "validateCode.png", "image/png", dec).
152
Post(setting.GetStr(conf.OcrApi))
153
if err != nil {
154
return err
155
}
156
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
157
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
158
}
159
captchaCode := jsoniter.Get(vRes.Body(), "result").ToString()
160
if captchaCode == "" {
161
return errors.New("ocr error: empty result")
162
}
163
loginBody["captcha"] = captchaCode
164
}
165
var token TokenResponse
166
err = d.request(http.MethodPost, "/session/token", func(req *resty.Request) {
167
req.SetBody(loginBody)
168
}, &token)
169
if err != nil {
170
return err
171
}
172
d.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken
173
op.MustSaveDriverStorage(d)
174
return nil
175
}
176
177
func (d *CloudreveV4) refreshToken() error {
178
var token Token
179
if token.RefreshToken == "" {
180
if d.Username != "" {
181
err := d.login()
182
if err != nil {
183
return fmt.Errorf("cannot login to get refresh token, error: %s", err)
184
}
185
}
186
return nil
187
}
188
err := d.request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
189
req.SetBody(base.Json{
190
"refresh_token": d.RefreshToken,
191
})
192
}, &token)
193
if err != nil {
194
return err
195
}
196
d.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken
197
op.MustSaveDriverStorage(d)
198
return nil
199
}
200
201
func (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
202
var finish int64 = 0
203
var chunk int = 0
204
DEFAULT := int64(u.ChunkSize)
205
if DEFAULT == 0 {
206
// support relay
207
DEFAULT = file.GetSize()
208
}
209
for finish < file.GetSize() {
210
if utils.IsCanceled(ctx) {
211
return ctx.Err()
212
}
213
left := file.GetSize() - finish
214
byteSize := min(left, DEFAULT)
215
utils.Log.Debugf("[CloudreveV4-Local] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
216
byteData := make([]byte, byteSize)
217
n, err := io.ReadFull(file, byteData)
218
utils.Log.Debug(err, n)
219
if err != nil {
220
return err
221
}
222
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
223
req.SetHeader("Content-Type", "application/octet-stream")
224
req.SetContentLength(true)
225
req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10))
226
req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
227
req.AddRetryCondition(func(r *resty.Response, err error) bool {
228
if err != nil {
229
return true
230
}
231
if r.IsError() {
232
return true
233
}
234
var retryResp Resp
235
jErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)
236
if jErr != nil {
237
return true
238
}
239
if retryResp.Code != 0 {
240
return true
241
}
242
return false
243
})
244
}, nil)
245
if err != nil {
246
return err
247
}
248
finish += byteSize
249
up(float64(finish) * 100 / float64(file.GetSize()))
250
chunk++
251
}
252
return nil
253
}
254
255
func (d *CloudreveV4) upRemote(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
256
uploadUrl := u.UploadUrls[0]
257
credential := u.Credential
258
var finish int64 = 0
259
var chunk int = 0
260
DEFAULT := int64(u.ChunkSize)
261
retryCount := 0
262
maxRetries := 3
263
for finish < file.GetSize() {
264
if utils.IsCanceled(ctx) {
265
return ctx.Err()
266
}
267
left := file.GetSize() - finish
268
byteSize := min(left, DEFAULT)
269
utils.Log.Debugf("[CloudreveV4-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
270
byteData := make([]byte, byteSize)
271
n, err := io.ReadFull(file, byteData)
272
utils.Log.Debug(err, n)
273
if err != nil {
274
return err
275
}
276
req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk),
277
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
278
if err != nil {
279
return err
280
}
281
req = req.WithContext(ctx)
282
req.ContentLength = byteSize
283
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
284
req.Header.Set("Authorization", fmt.Sprint(credential))
285
req.Header.Set("User-Agent", d.getUA())
286
err = func() error {
287
res, err := base.HttpClient.Do(req)
288
if err != nil {
289
return err
290
}
291
defer res.Body.Close()
292
if res.StatusCode != 200 {
293
return errors.New(res.Status)
294
}
295
body, err := io.ReadAll(res.Body)
296
if err != nil {
297
return err
298
}
299
var up Resp
300
err = json.Unmarshal(body, &up)
301
if err != nil {
302
return err
303
}
304
if up.Code != 0 {
305
return errors.New(up.Msg)
306
}
307
return nil
308
}()
309
if err == nil {
310
retryCount = 0
311
finish += byteSize
312
up(float64(finish) * 100 / float64(file.GetSize()))
313
chunk++
314
} else {
315
retryCount++
316
if retryCount > maxRetries {
317
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
318
}
319
backoff := time.Duration(1<<retryCount) * time.Second
320
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
321
time.Sleep(backoff)
322
}
323
}
324
return nil
325
}
326
327
func (d *CloudreveV4) upOneDrive(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
328
uploadUrl := u.UploadUrls[0]
329
var finish int64 = 0
330
DEFAULT := int64(u.ChunkSize)
331
retryCount := 0
332
maxRetries := 3
333
for finish < file.GetSize() {
334
if utils.IsCanceled(ctx) {
335
return ctx.Err()
336
}
337
left := file.GetSize() - finish
338
byteSize := min(left, DEFAULT)
339
utils.Log.Debugf("[CloudreveV4-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
340
byteData := make([]byte, byteSize)
341
n, err := io.ReadFull(file, byteData)
342
utils.Log.Debug(err, n)
343
if err != nil {
344
return err
345
}
346
req, err := http.NewRequest(http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
347
if err != nil {
348
return err
349
}
350
req = req.WithContext(ctx)
351
req.ContentLength = byteSize
352
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
353
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.GetSize()))
354
req.Header.Set("User-Agent", d.getUA())
355
finish += byteSize
356
res, err := base.HttpClient.Do(req)
357
if err != nil {
358
return err
359
}
360
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
361
switch {
362
case res.StatusCode >= 500 && res.StatusCode <= 504:
363
retryCount++
364
if retryCount > maxRetries {
365
res.Body.Close()
366
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
367
}
368
backoff := time.Duration(1<<retryCount) * time.Second
369
utils.Log.Warnf("[CloudreveV4-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
370
time.Sleep(backoff)
371
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
372
data, _ := io.ReadAll(res.Body)
373
res.Body.Close()
374
return errors.New(string(data))
375
default:
376
res.Body.Close()
377
retryCount = 0
378
finish += byteSize
379
up(float64(finish) * 100 / float64(file.GetSize()))
380
}
381
}
382
// 上传成功发送回调请求
383
return d.request(http.MethodPost, "/callback/onedrive/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
384
req.SetBody("{}")
385
}, nil)
386
}
387
388
func (d *CloudreveV4) upS3(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
389
var finish int64 = 0
390
var chunk int = 0
391
var etags []string
392
DEFAULT := int64(u.ChunkSize)
393
retryCount := 0
394
maxRetries := 3
395
for finish < file.GetSize() {
396
if utils.IsCanceled(ctx) {
397
return ctx.Err()
398
}
399
left := file.GetSize() - finish
400
byteSize := min(left, DEFAULT)
401
utils.Log.Debugf("[CloudreveV4-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
402
byteData := make([]byte, byteSize)
403
n, err := io.ReadFull(file, byteData)
404
utils.Log.Debug(err, n)
405
if err != nil {
406
return err
407
}
408
req, err := http.NewRequest(http.MethodPut, u.UploadUrls[chunk],
409
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
410
if err != nil {
411
return err
412
}
413
req = req.WithContext(ctx)
414
req.ContentLength = byteSize
415
res, err := base.HttpClient.Do(req)
416
if err != nil {
417
return err
418
}
419
etag := res.Header.Get("ETag")
420
res.Body.Close()
421
switch {
422
case res.StatusCode != 200:
423
retryCount++
424
if retryCount > maxRetries {
425
return fmt.Errorf("upload failed after %d retries due to server errors", maxRetries)
426
}
427
backoff := time.Duration(1<<retryCount) * time.Second
428
utils.Log.Warnf("server error %d, retrying after %v...", res.StatusCode, backoff)
429
time.Sleep(backoff)
430
case etag == "":
431
return errors.New("faild to get ETag from header")
432
default:
433
retryCount = 0
434
etags = append(etags, etag)
435
finish += byteSize
436
up(float64(finish) * 100 / float64(file.GetSize()))
437
chunk++
438
}
439
}
440
441
// s3LikeFinishUpload
442
bodyBuilder := &strings.Builder{}
443
bodyBuilder.WriteString("<CompleteMultipartUpload>")
444
for i, etag := range etags {
445
bodyBuilder.WriteString(fmt.Sprintf(
446
`<Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>`,
447
i+1, // PartNumber 从 1 开始
448
etag,
449
))
450
}
451
bodyBuilder.WriteString("</CompleteMultipartUpload>")
452
req, err := http.NewRequest(
453
"POST",
454
u.CompleteURL,
455
strings.NewReader(bodyBuilder.String()),
456
)
457
if err != nil {
458
return err
459
}
460
req.Header.Set("Content-Type", "application/xml")
461
req.Header.Set("User-Agent", d.getUA())
462
res, err := base.HttpClient.Do(req)
463
if err != nil {
464
return err
465
}
466
defer res.Body.Close()
467
if res.StatusCode != http.StatusOK {
468
body, _ := io.ReadAll(res.Body)
469
return fmt.Errorf("up status: %d, error: %s", res.StatusCode, string(body))
470
}
471
472
// 上传成功发送回调请求
473
return d.request(http.MethodPost, "/callback/s3/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
474
req.SetBody("{}")
475
}, nil)
476
}
477
478