Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/ilanzou/driver.go
1987 views
1
package template
2
3
import (
4
"context"
5
"encoding/base64"
6
"encoding/hex"
7
"fmt"
8
"io"
9
"net/http"
10
"net/url"
11
"strconv"
12
"strings"
13
"time"
14
15
"github.com/alist-org/alist/v3/drivers/base"
16
"github.com/alist-org/alist/v3/internal/driver"
17
"github.com/alist-org/alist/v3/internal/errs"
18
"github.com/alist-org/alist/v3/internal/model"
19
"github.com/alist-org/alist/v3/internal/stream"
20
"github.com/alist-org/alist/v3/pkg/utils"
21
"github.com/foxxorcat/mopan-sdk-go"
22
"github.com/go-resty/resty/v2"
23
log "github.com/sirupsen/logrus"
24
)
25
26
type ILanZou struct {
27
model.Storage
28
Addition
29
30
userID string
31
account string
32
upClient *resty.Client
33
conf Conf
34
config driver.Config
35
}
36
37
func (d *ILanZou) Config() driver.Config {
38
return d.config
39
}
40
41
func (d *ILanZou) GetAddition() driver.Additional {
42
return &d.Addition
43
}
44
45
func (d *ILanZou) Init(ctx context.Context) error {
46
d.upClient = base.NewRestyClient().SetTimeout(time.Minute * 10)
47
if d.UUID == "" {
48
res, err := d.unproved("/getUuid", http.MethodGet, nil)
49
if err != nil {
50
return err
51
}
52
d.UUID = utils.Json.Get(res, "uuid").ToString()
53
}
54
res, err := d.proved("/user/account/map", http.MethodGet, nil)
55
if err != nil {
56
return err
57
}
58
d.userID = utils.Json.Get(res, "map", "userId").ToString()
59
d.account = utils.Json.Get(res, "map", "account").ToString()
60
log.Debugf("[ilanzou] init response: %s", res)
61
return nil
62
}
63
64
func (d *ILanZou) Drop(ctx context.Context) error {
65
return nil
66
}
67
68
func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
69
offset := 1
70
var res []ListItem
71
for {
72
var resp ListResp
73
_, err := d.proved("/record/file/list", http.MethodGet, func(req *resty.Request) {
74
params := []string{
75
"offset=" + strconv.Itoa(offset),
76
"limit=60",
77
"folderId=" + dir.GetID(),
78
"type=0",
79
}
80
queryString := strings.Join(params, "&")
81
req.SetQueryString(queryString).SetResult(&resp)
82
})
83
if err != nil {
84
return nil, err
85
}
86
res = append(res, resp.List...)
87
if resp.Offset < resp.TotalPage {
88
offset++
89
} else {
90
break
91
}
92
}
93
return utils.SliceConvert(res, func(f ListItem) (model.Obj, error) {
94
updTime, err := time.ParseInLocation("2006-01-02 15:04:05", f.UpdTime, time.Local)
95
if err != nil {
96
return nil, err
97
}
98
obj := model.Object{
99
ID: strconv.FormatInt(f.FileId, 10),
100
//Path: "",
101
Name: f.FileName,
102
Size: f.FileSize * 1024,
103
Modified: updTime,
104
Ctime: updTime,
105
IsFolder: false,
106
//HashInfo: utils.HashInfo{},
107
}
108
if f.FileType == 2 {
109
obj.IsFolder = true
110
obj.Size = 0
111
obj.ID = strconv.FormatInt(f.FolderId, 10)
112
obj.Name = f.FolderName
113
}
114
return &obj, nil
115
})
116
}
117
118
func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
119
u, err := url.Parse(d.conf.base + "/" + d.conf.unproved + "/file/redirect")
120
if err != nil {
121
return nil, err
122
}
123
ts, ts_str, _ := getTimestamp(d.conf.secret)
124
125
params := []string{
126
"uuid=" + url.QueryEscape(d.UUID),
127
"devType=6",
128
"devCode=" + url.QueryEscape(d.UUID),
129
"devModel=chrome",
130
"devVersion=" + url.QueryEscape(d.conf.devVersion),
131
"appVersion=",
132
"timestamp=" + ts_str,
133
"appToken=" + url.QueryEscape(d.Token),
134
"enable=0",
135
}
136
137
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret)
138
if err != nil {
139
return nil, err
140
}
141
params = append(params, "downloadId="+url.QueryEscape(hex.EncodeToString(downloadId)))
142
143
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), ts)), d.conf.secret)
144
if err != nil {
145
return nil, err
146
}
147
params = append(params, "auth="+url.QueryEscape(hex.EncodeToString(auth)))
148
149
u.RawQuery = strings.Join(params, "&")
150
realURL := u.String()
151
// get the url after redirect
152
req := base.NoRedirectClient.R()
153
154
req.SetHeaders(map[string]string{
155
"Referer": d.conf.site + "/",
156
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
157
})
158
if d.Addition.Ip != "" {
159
req.SetHeader("X-Forwarded-For", d.Addition.Ip)
160
}
161
162
res, err := req.Get(realURL)
163
if err != nil {
164
return nil, err
165
}
166
if res.StatusCode() == 302 {
167
realURL = res.Header().Get("location")
168
} else {
169
return nil, fmt.Errorf("redirect failed, status: %d, msg: %s", res.StatusCode(), utils.Json.Get(res.Body(), "msg").ToString())
170
}
171
link := model.Link{URL: realURL}
172
return &link, nil
173
}
174
175
func (d *ILanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
176
res, err := d.proved("/file/folder/save", http.MethodPost, func(req *resty.Request) {
177
req.SetBody(base.Json{
178
"folderDesc": "",
179
"folderId": parentDir.GetID(),
180
"folderName": dirName,
181
})
182
})
183
if err != nil {
184
return nil, err
185
}
186
return &model.Object{
187
ID: utils.Json.Get(res, "list", 0, "id").ToString(),
188
//Path: "",
189
Name: dirName,
190
Size: 0,
191
Modified: time.Now(),
192
Ctime: time.Now(),
193
IsFolder: true,
194
//HashInfo: utils.HashInfo{},
195
}, nil
196
}
197
198
func (d *ILanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
199
var fileIds, folderIds []string
200
if srcObj.IsDir() {
201
folderIds = []string{srcObj.GetID()}
202
} else {
203
fileIds = []string{srcObj.GetID()}
204
}
205
_, err := d.proved("/file/folder/move", http.MethodPost, func(req *resty.Request) {
206
req.SetBody(base.Json{
207
"folderIds": strings.Join(folderIds, ","),
208
"fileIds": strings.Join(fileIds, ","),
209
"targetId": dstDir.GetID(),
210
})
211
})
212
if err != nil {
213
return nil, err
214
}
215
return srcObj, nil
216
}
217
218
func (d *ILanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
219
var err error
220
if srcObj.IsDir() {
221
_, err = d.proved("/file/folder/edit", http.MethodPost, func(req *resty.Request) {
222
req.SetBody(base.Json{
223
"folderDesc": "",
224
"folderId": srcObj.GetID(),
225
"folderName": newName,
226
})
227
})
228
} else {
229
_, err = d.proved("/file/edit", http.MethodPost, func(req *resty.Request) {
230
req.SetBody(base.Json{
231
"fileDesc": "",
232
"fileId": srcObj.GetID(),
233
"fileName": newName,
234
})
235
})
236
}
237
if err != nil {
238
return nil, err
239
}
240
return &model.Object{
241
ID: srcObj.GetID(),
242
//Path: "",
243
Name: newName,
244
Size: srcObj.GetSize(),
245
Modified: time.Now(),
246
Ctime: srcObj.CreateTime(),
247
IsFolder: srcObj.IsDir(),
248
}, nil
249
}
250
251
func (d *ILanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
252
// TODO copy obj, optional
253
return nil, errs.NotImplement
254
}
255
256
func (d *ILanZou) Remove(ctx context.Context, obj model.Obj) error {
257
var fileIds, folderIds []string
258
if obj.IsDir() {
259
folderIds = []string{obj.GetID()}
260
} else {
261
fileIds = []string{obj.GetID()}
262
}
263
_, err := d.proved("/file/delete", http.MethodPost, func(req *resty.Request) {
264
req.SetBody(base.Json{
265
"folderIds": strings.Join(folderIds, ","),
266
"fileIds": strings.Join(fileIds, ","),
267
"status": 0,
268
})
269
})
270
return err
271
}
272
273
const DefaultPartSize = 1024 * 1024 * 8
274
275
func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
276
etag := s.GetHash().GetHash(utils.MD5)
277
var err error
278
if len(etag) != utils.MD5.Width {
279
_, etag, err = stream.CacheFullInTempFileAndHash(s, utils.MD5)
280
if err != nil {
281
return nil, err
282
}
283
}
284
// get upToken
285
res, err := d.proved("/7n/getUpToken", http.MethodPost, func(req *resty.Request) {
286
req.SetBody(base.Json{
287
"fileId": "",
288
"fileName": s.GetName(),
289
"fileSize": s.GetSize()/1024 + 1,
290
"folderId": dstDir.GetID(),
291
"md5": etag,
292
"type": 1,
293
})
294
})
295
if err != nil {
296
return nil, err
297
}
298
upToken := utils.Json.Get(res, "upToken").ToString()
299
now := time.Now()
300
key := fmt.Sprintf("disk/%d/%d/%d/%s/%016d", now.Year(), now.Month(), now.Day(), d.account, now.UnixMilli())
301
reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
302
Reader: &driver.SimpleReaderWithSize{
303
Reader: s,
304
Size: s.GetSize(),
305
},
306
UpdateProgress: up,
307
})
308
var token string
309
if s.GetSize() <= DefaultPartSize {
310
res, err := d.upClient.R().SetContext(ctx).SetMultipartFormData(map[string]string{
311
"token": upToken,
312
"key": key,
313
"fname": s.GetName(),
314
}).SetMultipartField("file", s.GetName(), s.GetMimetype(), reader).
315
Post("https://upload.qiniup.com/")
316
if err != nil {
317
return nil, err
318
}
319
token = utils.Json.Get(res.Body(), "token").ToString()
320
} else {
321
keyBase64 := base64.URLEncoding.EncodeToString([]byte(key))
322
res, err := d.upClient.R().SetHeader("Authorization", "UpToken "+upToken).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads", d.conf.bucket, keyBase64))
323
if err != nil {
324
return nil, err
325
}
326
uploadId := utils.Json.Get(res.Body(), "uploadId").ToString()
327
parts := make([]Part, 0)
328
partNum := (s.GetSize() + DefaultPartSize - 1) / DefaultPartSize
329
for i := 1; i <= int(partNum); i++ {
330
u := fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s/%d", d.conf.bucket, keyBase64, uploadId, i)
331
res, err = d.upClient.R().SetContext(ctx).SetHeader("Authorization", "UpToken "+upToken).SetBody(io.LimitReader(reader, DefaultPartSize)).Put(u)
332
if err != nil {
333
return nil, err
334
}
335
etag := utils.Json.Get(res.Body(), "etag").ToString()
336
parts = append(parts, Part{
337
PartNumber: i,
338
ETag: etag,
339
})
340
}
341
res, err = d.upClient.R().SetHeader("Authorization", "UpToken "+upToken).SetBody(base.Json{
342
"fnmae": s.GetName(),
343
"parts": parts,
344
}).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s", d.conf.bucket, keyBase64, uploadId))
345
if err != nil {
346
return nil, err
347
}
348
token = utils.Json.Get(res.Body(), "token").ToString()
349
}
350
// commit upload
351
var resp UploadResultResp
352
for i := 0; i < 10; i++ {
353
_, err = d.unproved("/7n/results", http.MethodPost, func(req *resty.Request) {
354
params := []string{
355
"tokenList=" + token,
356
"tokenTime=" + time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"),
357
}
358
queryString := strings.Join(params, "&")
359
req.SetQueryString(queryString).SetResult(&resp)
360
})
361
if err != nil {
362
return nil, err
363
}
364
if len(resp.List) == 0 {
365
return nil, fmt.Errorf("upload failed, empty response")
366
}
367
if resp.List[0].Status == 1 {
368
break
369
}
370
time.Sleep(time.Second * 1)
371
}
372
file := resp.List[0]
373
if file.Status != 1 {
374
return nil, fmt.Errorf("upload failed, status: %d", resp.List[0].Status)
375
}
376
return &model.Object{
377
ID: strconv.FormatInt(file.FileId, 10),
378
//Path: ,
379
Name: file.FileName,
380
Size: s.GetSize(),
381
Modified: s.ModTime(),
382
Ctime: s.CreateTime(),
383
IsFolder: false,
384
HashInfo: utils.NewHashInfo(utils.MD5, etag),
385
}, nil
386
}
387
388
//func (d *ILanZou) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
389
// return nil, errs.NotSupport
390
//}
391
392
var _ driver.Driver = (*ILanZou)(nil)
393
394