Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/aliyundrive/driver.go
1986 views
1
package aliyundrive
2
3
import (
4
"bytes"
5
"context"
6
"crypto/sha1"
7
"encoding/base64"
8
"encoding/hex"
9
"fmt"
10
"io"
11
"math"
12
"math/big"
13
"net/http"
14
"os"
15
"time"
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/internal/stream"
23
"github.com/alist-org/alist/v3/pkg/cron"
24
"github.com/alist-org/alist/v3/pkg/utils"
25
"github.com/go-resty/resty/v2"
26
log "github.com/sirupsen/logrus"
27
)
28
29
type AliDrive struct {
30
model.Storage
31
Addition
32
AccessToken string
33
cron *cron.Cron
34
DriveId string
35
UserID string
36
}
37
38
func (d *AliDrive) Config() driver.Config {
39
return config
40
}
41
42
func (d *AliDrive) GetAddition() driver.Additional {
43
return &d.Addition
44
}
45
46
func (d *AliDrive) Init(ctx context.Context) error {
47
// TODO login / refresh token
48
//op.MustSaveDriverStorage(d)
49
err := d.refreshToken()
50
if err != nil {
51
return err
52
}
53
// get driver id
54
res, err, _ := d.request("https://api.alipan.com/v2/user/get", http.MethodPost, nil, nil)
55
if err != nil {
56
return err
57
}
58
d.DriveId = d.Addition.DeviceID
59
d.UserID = utils.Json.Get(res, "user_id").ToString()
60
d.cron = cron.NewCron(time.Hour * 2)
61
d.cron.Do(func() {
62
err := d.refreshToken()
63
if err != nil {
64
log.Errorf("%+v", err)
65
}
66
})
67
if global.Has(d.UserID) {
68
return nil
69
}
70
// init deviceID
71
deviceID := utils.HashData(utils.SHA256, []byte(d.UserID))
72
// init privateKey
73
privateKey, _ := NewPrivateKeyFromHex(deviceID)
74
state := State{
75
privateKey: privateKey,
76
deviceID: deviceID,
77
}
78
// store state
79
global.Store(d.UserID, &state)
80
// init signature
81
d.sign()
82
return nil
83
}
84
85
func (d *AliDrive) Drop(ctx context.Context) error {
86
if d.cron != nil {
87
d.cron.Stop()
88
}
89
return nil
90
}
91
92
func (d *AliDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
93
files, err := d.getFiles(dir.GetID())
94
if err != nil {
95
return nil, err
96
}
97
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
98
return fileToObj(src), nil
99
})
100
}
101
102
func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
103
data := base.Json{
104
"drive_id": d.DriveId,
105
"file_id": file.GetID(),
106
"expire_sec": 14400,
107
}
108
res, err, _ := d.request("https://api.alipan.com/v2/file/get_download_url", http.MethodPost, func(req *resty.Request) {
109
req.SetBody(data)
110
}, nil)
111
if err != nil {
112
return nil, err
113
}
114
return &model.Link{
115
Header: http.Header{
116
"Referer": []string{"https://www.alipan.com/"},
117
},
118
URL: utils.Json.Get(res, "url").ToString(),
119
}, nil
120
}
121
122
func (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
123
_, err, _ := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
124
req.SetBody(base.Json{
125
"check_name_mode": "refuse",
126
"drive_id": d.DriveId,
127
"name": dirName,
128
"parent_file_id": parentDir.GetID(),
129
"type": "folder",
130
})
131
}, nil)
132
return err
133
}
134
135
func (d *AliDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
136
err := d.batch(srcObj.GetID(), dstDir.GetID(), "/file/move")
137
return err
138
}
139
140
func (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
141
_, err, _ := d.request("https://api.alipan.com/v3/file/update", http.MethodPost, func(req *resty.Request) {
142
req.SetBody(base.Json{
143
"check_name_mode": "refuse",
144
"drive_id": d.DriveId,
145
"file_id": srcObj.GetID(),
146
"name": newName,
147
})
148
}, nil)
149
return err
150
}
151
152
func (d *AliDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
153
err := d.batch(srcObj.GetID(), dstDir.GetID(), "/file/copy")
154
return err
155
}
156
157
func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error {
158
_, err, _ := d.request("https://api.alipan.com/v2/recyclebin/trash", http.MethodPost, func(req *resty.Request) {
159
req.SetBody(base.Json{
160
"drive_id": d.DriveId,
161
"file_id": obj.GetID(),
162
})
163
}, nil)
164
return err
165
}
166
167
func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error {
168
file := stream.FileStream{
169
Obj: streamer,
170
Reader: streamer,
171
Mimetype: streamer.GetMimetype(),
172
}
173
const DEFAULT int64 = 10485760
174
var count = int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT)))
175
176
partInfoList := make([]base.Json, 0, count)
177
for i := 1; i <= count; i++ {
178
partInfoList = append(partInfoList, base.Json{"part_number": i})
179
}
180
reqBody := base.Json{
181
"check_name_mode": "overwrite",
182
"drive_id": d.DriveId,
183
"name": file.GetName(),
184
"parent_file_id": dstDir.GetID(),
185
"part_info_list": partInfoList,
186
"size": file.GetSize(),
187
"type": "file",
188
}
189
190
var localFile *os.File
191
if fileStream, ok := file.Reader.(*stream.FileStream); ok {
192
localFile, _ = fileStream.Reader.(*os.File)
193
}
194
if d.RapidUpload {
195
buf := bytes.NewBuffer(make([]byte, 0, 1024))
196
_, err := utils.CopyWithBufferN(buf, file, 1024)
197
if err != nil {
198
return err
199
}
200
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
201
if localFile != nil {
202
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
203
return err
204
}
205
} else {
206
// 把头部拼接回去
207
file.Reader = struct {
208
io.Reader
209
io.Closer
210
}{
211
Reader: io.MultiReader(buf, file),
212
Closer: &file,
213
}
214
}
215
} else {
216
reqBody["content_hash_name"] = "none"
217
reqBody["proof_version"] = "v1"
218
}
219
220
var resp UploadResp
221
_, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
222
req.SetBody(reqBody)
223
}, &resp)
224
225
if err != nil && e.Code != "PreHashMatched" {
226
return err
227
}
228
229
if d.RapidUpload && e.Code == "PreHashMatched" {
230
delete(reqBody, "pre_hash")
231
h := sha1.New()
232
if localFile != nil {
233
if err = utils.CopyWithCtx(ctx, h, localFile, 0, nil); err != nil {
234
return err
235
}
236
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
237
return err
238
}
239
} else {
240
tempFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
241
if err != nil {
242
return err
243
}
244
defer func() {
245
_ = tempFile.Close()
246
_ = os.Remove(tempFile.Name())
247
}()
248
if err = utils.CopyWithCtx(ctx, io.MultiWriter(tempFile, h), file, 0, nil); err != nil {
249
return err
250
}
251
localFile = tempFile
252
}
253
reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil))
254
reqBody["content_hash_name"] = "sha1"
255
reqBody["proof_version"] = "v1"
256
257
/*
258
js 隐性转换太坑不知道有没有bug
259
var n = e.access_token,
260
r = new BigNumber('0x'.concat(md5(n).slice(0, 16))),
261
i = new BigNumber(t.file.size),
262
o = i ? r.mod(i) : new gt.BigNumber(0);
263
(t.file.slice(o.toNumber(), Math.min(o.plus(8).toNumber(), t.file.size)))
264
*/
265
buf := make([]byte, 8)
266
r, _ := new(big.Int).SetString(utils.GetMD5EncodeStr(d.AccessToken)[:16], 16)
267
i := new(big.Int).SetInt64(file.GetSize())
268
o := new(big.Int).SetInt64(0)
269
if file.GetSize() > 0 {
270
o = r.Mod(r, i)
271
}
272
n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8])
273
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
274
275
_, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) {
276
req.SetBody(reqBody)
277
}, &resp)
278
if err != nil && e.Code != "PreHashMatched" {
279
return err
280
}
281
if resp.RapidUpload {
282
return nil
283
}
284
// 秒传失败
285
if _, err = localFile.Seek(0, io.SeekStart); err != nil {
286
return err
287
}
288
file.Reader = localFile
289
}
290
291
rateLimited := driver.NewLimitedUploadStream(ctx, file)
292
for i, partInfo := range resp.PartInfoList {
293
if utils.IsCanceled(ctx) {
294
return ctx.Err()
295
}
296
url := partInfo.UploadUrl
297
if d.InternalUpload {
298
url = partInfo.InternalUploadUrl
299
}
300
req, err := http.NewRequest("PUT", url, io.LimitReader(rateLimited, DEFAULT))
301
if err != nil {
302
return err
303
}
304
req = req.WithContext(ctx)
305
res, err := base.HttpClient.Do(req)
306
if err != nil {
307
return err
308
}
309
_ = res.Body.Close()
310
if count > 0 {
311
up(float64(i) * 100 / float64(count))
312
}
313
}
314
var resp2 base.Json
315
_, err, e = d.request("https://api.alipan.com/v2/file/complete", http.MethodPost, func(req *resty.Request) {
316
req.SetBody(base.Json{
317
"drive_id": d.DriveId,
318
"file_id": resp.FileId,
319
"upload_id": resp.UploadId,
320
})
321
}, &resp2)
322
if err != nil && e.Code != "PreHashMatched" {
323
return err
324
}
325
if resp2["file_id"] == resp.FileId {
326
return nil
327
}
328
return fmt.Errorf("%+v", resp2)
329
}
330
331
func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
332
var resp base.Json
333
var url string
334
data := base.Json{
335
"drive_id": d.DriveId,
336
"file_id": args.Obj.GetID(),
337
}
338
switch args.Method {
339
case "doc_preview":
340
url = "https://api.alipan.com/v2/file/get_office_preview_url"
341
data["access_token"] = d.AccessToken
342
case "video_preview":
343
url = "https://api.alipan.com/v2/file/get_video_preview_play_info"
344
data["category"] = "live_transcoding"
345
data["url_expire_sec"] = 14400
346
default:
347
return nil, errs.NotSupport
348
}
349
_, err, _ := d.request(url, http.MethodPost, func(req *resty.Request) {
350
req.SetBody(data)
351
}, &resp)
352
if err != nil {
353
return nil, err
354
}
355
return resp, nil
356
}
357
358
var _ driver.Driver = (*AliDrive)(nil)
359
360