Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/baidu_netdisk/driver.go
1520 views
1
package baidu_netdisk
2
3
import (
4
"context"
5
"crypto/md5"
6
"encoding/hex"
7
"errors"
8
"io"
9
"net/url"
10
"os"
11
stdpath "path"
12
"strconv"
13
"time"
14
15
"golang.org/x/sync/semaphore"
16
17
"github.com/alist-org/alist/v3/drivers/base"
18
"github.com/alist-org/alist/v3/internal/conf"
19
"github.com/alist-org/alist/v3/internal/driver"
20
"github.com/alist-org/alist/v3/internal/errs"
21
"github.com/alist-org/alist/v3/internal/model"
22
"github.com/alist-org/alist/v3/pkg/errgroup"
23
"github.com/alist-org/alist/v3/pkg/utils"
24
"github.com/avast/retry-go"
25
log "github.com/sirupsen/logrus"
26
)
27
28
type BaiduNetdisk struct {
29
model.Storage
30
Addition
31
32
uploadThread int
33
vipType int // 会员类型,0普通用户(4G/4M)、1普通会员(10G/16M)、2超级会员(20G/32M)
34
}
35
36
func (d *BaiduNetdisk) Config() driver.Config {
37
return config
38
}
39
40
func (d *BaiduNetdisk) GetAddition() driver.Additional {
41
return &d.Addition
42
}
43
44
func (d *BaiduNetdisk) Init(ctx context.Context) error {
45
d.uploadThread, _ = strconv.Atoi(d.UploadThread)
46
if d.uploadThread < 1 || d.uploadThread > 32 {
47
d.uploadThread, d.UploadThread = 3, "3"
48
}
49
50
if _, err := url.Parse(d.UploadAPI); d.UploadAPI == "" || err != nil {
51
d.UploadAPI = "https://d.pcs.baidu.com"
52
}
53
54
res, err := d.get("/xpan/nas", map[string]string{
55
"method": "uinfo",
56
}, nil)
57
log.Debugf("[baidu] get uinfo: %s", string(res))
58
if err != nil {
59
return err
60
}
61
d.vipType = utils.Json.Get(res, "vip_type").ToInt()
62
return nil
63
}
64
65
func (d *BaiduNetdisk) Drop(ctx context.Context) error {
66
return nil
67
}
68
69
func (d *BaiduNetdisk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
70
files, err := d.getFiles(dir.GetPath())
71
if err != nil {
72
return nil, err
73
}
74
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
75
return fileToObj(src), nil
76
})
77
}
78
79
func (d *BaiduNetdisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
80
if d.DownloadAPI == "crack" {
81
return d.linkCrack(file, args)
82
} else if d.DownloadAPI == "crack_video" {
83
return d.linkCrackVideo(file, args)
84
}
85
return d.linkOfficial(file, args)
86
}
87
88
func (d *BaiduNetdisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
89
var newDir File
90
_, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "", &newDir, 0, 0)
91
if err != nil {
92
return nil, err
93
}
94
return fileToObj(newDir), nil
95
}
96
97
func (d *BaiduNetdisk) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
98
data := []base.Json{
99
{
100
"path": srcObj.GetPath(),
101
"dest": dstDir.GetPath(),
102
"newname": srcObj.GetName(),
103
},
104
}
105
_, err := d.manage("move", data)
106
if err != nil {
107
return nil, err
108
}
109
if srcObj, ok := srcObj.(*model.ObjThumb); ok {
110
srcObj.SetPath(stdpath.Join(dstDir.GetPath(), srcObj.GetName()))
111
srcObj.Modified = time.Now()
112
return srcObj, nil
113
}
114
return nil, nil
115
}
116
117
func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
118
data := []base.Json{
119
{
120
"path": srcObj.GetPath(),
121
"newname": newName,
122
},
123
}
124
_, err := d.manage("rename", data)
125
if err != nil {
126
return nil, err
127
}
128
129
if srcObj, ok := srcObj.(*model.ObjThumb); ok {
130
srcObj.SetPath(stdpath.Join(stdpath.Dir(srcObj.GetPath()), newName))
131
srcObj.Name = newName
132
srcObj.Modified = time.Now()
133
return srcObj, nil
134
}
135
return nil, nil
136
}
137
138
func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
139
data := []base.Json{
140
{
141
"path": srcObj.GetPath(),
142
"dest": dstDir.GetPath(),
143
"newname": srcObj.GetName(),
144
},
145
}
146
_, err := d.manage("copy", data)
147
return err
148
}
149
150
func (d *BaiduNetdisk) Remove(ctx context.Context, obj model.Obj) error {
151
data := []string{obj.GetPath()}
152
_, err := d.manage("delete", data)
153
return err
154
}
155
156
func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
157
contentMd5 := stream.GetHash().GetHash(utils.MD5)
158
if len(contentMd5) < utils.MD5.Width {
159
return nil, errors.New("invalid hash")
160
}
161
162
streamSize := stream.GetSize()
163
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
164
mtime := stream.ModTime().Unix()
165
ctime := stream.CreateTime().Unix()
166
blockList, _ := utils.Json.MarshalToString([]string{contentMd5})
167
168
var newFile File
169
_, err := d.create(path, streamSize, 0, "", blockList, &newFile, mtime, ctime)
170
if err != nil {
171
return nil, err
172
}
173
// 修复时间,具体原因见 Put 方法注释的 **注意**
174
newFile.Ctime = stream.CreateTime().Unix()
175
newFile.Mtime = stream.ModTime().Unix()
176
return fileToObj(newFile), nil
177
}
178
179
// Put
180
//
181
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
182
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
183
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
184
// rapid upload
185
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
186
return newObj, nil
187
}
188
189
var (
190
cache = stream.GetFile()
191
tmpF *os.File
192
err error
193
)
194
if _, ok := cache.(io.ReaderAt); !ok {
195
tmpF, err = os.CreateTemp(conf.Conf.TempDir, "file-*")
196
if err != nil {
197
return nil, err
198
}
199
defer func() {
200
_ = tmpF.Close()
201
_ = os.Remove(tmpF.Name())
202
}()
203
cache = tmpF
204
}
205
206
streamSize := stream.GetSize()
207
sliceSize := d.getSliceSize(streamSize)
208
count := int(streamSize / sliceSize)
209
lastBlockSize := streamSize % sliceSize
210
if lastBlockSize > 0 {
211
count++
212
} else {
213
lastBlockSize = sliceSize
214
}
215
216
//cal md5 for first 256k data
217
const SliceSize int64 = 256 * utils.KB
218
// cal md5
219
blockList := make([]string, 0, count)
220
byteSize := sliceSize
221
fileMd5H := md5.New()
222
sliceMd5H := md5.New()
223
sliceMd5H2 := md5.New()
224
slicemd5H2Write := utils.LimitWriter(sliceMd5H2, SliceSize)
225
writers := []io.Writer{fileMd5H, sliceMd5H, slicemd5H2Write}
226
if tmpF != nil {
227
writers = append(writers, tmpF)
228
}
229
written := int64(0)
230
231
for i := 1; i <= count; i++ {
232
if utils.IsCanceled(ctx) {
233
return nil, ctx.Err()
234
}
235
if i == count {
236
byteSize = lastBlockSize
237
}
238
n, err := utils.CopyWithBufferN(io.MultiWriter(writers...), stream, byteSize)
239
written += n
240
if err != nil && err != io.EOF {
241
return nil, err
242
}
243
blockList = append(blockList, hex.EncodeToString(sliceMd5H.Sum(nil)))
244
sliceMd5H.Reset()
245
}
246
if tmpF != nil {
247
if written != streamSize {
248
return nil, errs.NewErr(err, "CreateTempFile failed, incoming stream actual size= %d, expect = %d ", written, streamSize)
249
}
250
_, err = tmpF.Seek(0, io.SeekStart)
251
if err != nil {
252
return nil, errs.NewErr(err, "CreateTempFile failed, can't seek to 0 ")
253
}
254
}
255
contentMd5 := hex.EncodeToString(fileMd5H.Sum(nil))
256
sliceMd5 := hex.EncodeToString(sliceMd5H2.Sum(nil))
257
blockListStr, _ := utils.Json.MarshalToString(blockList)
258
path := stdpath.Join(dstDir.GetPath(), stream.GetName())
259
mtime := stream.ModTime().Unix()
260
ctime := stream.CreateTime().Unix()
261
262
// step.1 预上传
263
// 尝试获取之前的进度
264
precreateResp, ok := base.GetUploadProgress[*PrecreateResp](d, d.AccessToken, contentMd5)
265
if !ok {
266
params := map[string]string{
267
"method": "precreate",
268
}
269
form := map[string]string{
270
"path": path,
271
"size": strconv.FormatInt(streamSize, 10),
272
"isdir": "0",
273
"autoinit": "1",
274
"rtype": "3",
275
"block_list": blockListStr,
276
"content-md5": contentMd5,
277
"slice-md5": sliceMd5,
278
}
279
joinTime(form, ctime, mtime)
280
281
log.Debugf("[baidu_netdisk] precreate data: %s", form)
282
_, err = d.postForm("/xpan/file", params, form, &precreateResp)
283
if err != nil {
284
return nil, err
285
}
286
log.Debugf("%+v", precreateResp)
287
if precreateResp.ReturnType == 2 {
288
//rapid upload, since got md5 match from baidu server
289
// 修复时间,具体原因见 Put 方法注释的 **注意**
290
precreateResp.File.Ctime = ctime
291
precreateResp.File.Mtime = mtime
292
return fileToObj(precreateResp.File), nil
293
}
294
}
295
// step.2 上传分片
296
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
297
retry.Attempts(1),
298
retry.Delay(time.Second),
299
retry.DelayType(retry.BackOffDelay))
300
sem := semaphore.NewWeighted(3)
301
for i, partseq := range precreateResp.BlockList {
302
if utils.IsCanceled(upCtx) {
303
break
304
}
305
306
i, partseq, offset, byteSize := i, partseq, int64(partseq)*sliceSize, sliceSize
307
if partseq+1 == count {
308
byteSize = lastBlockSize
309
}
310
threadG.Go(func(ctx context.Context) error {
311
if err = sem.Acquire(ctx, 1); err != nil {
312
return err
313
}
314
defer sem.Release(1)
315
params := map[string]string{
316
"method": "upload",
317
"access_token": d.AccessToken,
318
"type": "tmpfile",
319
"path": path,
320
"uploadid": precreateResp.Uploadid,
321
"partseq": strconv.Itoa(partseq),
322
}
323
err := d.uploadSlice(ctx, params, stream.GetName(),
324
driver.NewLimitedUploadStream(ctx, io.NewSectionReader(cache, offset, byteSize)))
325
if err != nil {
326
return err
327
}
328
up(float64(threadG.Success()) * 100 / float64(len(precreateResp.BlockList)))
329
precreateResp.BlockList[i] = -1
330
return nil
331
})
332
}
333
if err = threadG.Wait(); err != nil {
334
// 如果属于用户主动取消,则保存上传进度
335
if errors.Is(err, context.Canceled) {
336
precreateResp.BlockList = utils.SliceFilter(precreateResp.BlockList, func(s int) bool { return s >= 0 })
337
base.SaveUploadProgress(d, precreateResp, d.AccessToken, contentMd5)
338
}
339
return nil, err
340
}
341
342
// step.3 创建文件
343
var newFile File
344
_, err = d.create(path, streamSize, 0, precreateResp.Uploadid, blockListStr, &newFile, mtime, ctime)
345
if err != nil {
346
return nil, err
347
}
348
// 修复时间,具体原因见 Put 方法注释的 **注意**
349
newFile.Ctime = ctime
350
newFile.Mtime = mtime
351
return fileToObj(newFile), nil
352
}
353
354
func (d *BaiduNetdisk) uploadSlice(ctx context.Context, params map[string]string, fileName string, file io.Reader) error {
355
res, err := base.RestyClient.R().
356
SetContext(ctx).
357
SetQueryParams(params).
358
SetFileReader("file", fileName, file).
359
Post(d.UploadAPI + "/rest/2.0/pcs/superfile2")
360
if err != nil {
361
return err
362
}
363
log.Debugln(res.RawResponse.Status + res.String())
364
errCode := utils.Json.Get(res.Body(), "error_code").ToInt()
365
errNo := utils.Json.Get(res.Body(), "errno").ToInt()
366
if errCode != 0 || errNo != 0 {
367
return errs.NewErr(errs.StreamIncomplete, "error in uploading to baidu, will retry. response=%s", res.String())
368
}
369
return nil
370
}
371
372
var _ driver.Driver = (*BaiduNetdisk)(nil)
373
374