Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ilanzou driver #5810

Merged
merged 6 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [ILanzou](https://www.ilanzou.com/)
- [x] [Aliyundrive share](https://www.alipan.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
Expand Down
1 change: 1 addition & 0 deletions README_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/)
- [x] [蓝奏云优享版](https://www.ilanzou.com/)
- [x] [阿里云盘分享](https://www.alipan.com/)
- [x] [谷歌相册](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
Expand Down
1 change: 1 addition & 0 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [ILanzou](https://www.ilanzou.com/)
- [x] [Aliyundrive share](https://www.alipan.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
Expand Down
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/ilanzou"
_ "github.com/alist-org/alist/v3/drivers/ipfs_api"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/local"
Expand Down
365 changes: 365 additions & 0 deletions drivers/ilanzou/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
package template

import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/foxxorcat/mopan-sdk-go"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)

type ILanZou struct {
model.Storage
Addition

userID string
account string
upClient *resty.Client
}

func (d *ILanZou) Config() driver.Config {
return config
}

func (d *ILanZou) GetAddition() driver.Additional {
return &d.Addition
}

func (d *ILanZou) Init(ctx context.Context) error {
d.upClient = base.NewRestyClient().SetTimeout(time.Minute * 10)
if d.UUID == "" {
res, err := d.unproved("/getUuid", http.MethodGet, nil)
if err != nil {
return err
}
d.UUID = utils.Json.Get(res, "uuid").ToString()
}
res, err := d.proved("/user/account/map", http.MethodGet, nil)
if err != nil {
return err
}
d.userID = utils.Json.Get(res, "map", "userId").ToString()
d.account = utils.Json.Get(res, "map", "account").ToString()
log.Debugf("[ilanzou] init response: %s", res)
return nil
}

func (d *ILanZou) Drop(ctx context.Context) error {
return nil
}

func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
offset := 1
limit := 60
var res []ListItem
for {
var resp ListResp
_, err := d.proved("/record/file/list", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"type": "0",
"folderId": dir.GetID(),
"offset": strconv.Itoa(offset),
"limit": strconv.Itoa(limit),
}).SetResult(&resp)
})
if err != nil {
return nil, err
}
res = append(res, resp.List...)
if resp.TotalPage <= resp.Offset {
break
}
offset++
}
return utils.SliceConvert(res, func(f ListItem) (model.Obj, error) {
updTime, err := time.ParseInLocation("2006-01-02 15:04:05", f.UpdTime, time.Local)
if err != nil {
return nil, err
}
obj := model.Object{
ID: strconv.FormatInt(f.FileId, 10),
//Path: "",
Name: f.FileName,
Size: f.FileSize * 1024,
Modified: updTime,
Ctime: updTime,
IsFolder: false,
//HashInfo: utils.HashInfo{},
}
if f.FileType == 2 {
obj.IsFolder = true
obj.Size = 0
obj.ID = strconv.FormatInt(f.FolderId, 10)
obj.Name = f.FolderName
}
return &obj, nil
})
}

func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
u, err := url.Parse("https://api.ilanzou.com/unproved/file/redirect")
if err != nil {
return nil, err
}
query := u.Query()
query.Set("uuid", d.UUID)
query.Set("devType", "6")
query.Set("devCode", d.UUID)
query.Set("devModel", "chrome")
query.Set("devVersion", "120")
query.Set("appVersion", "")
ts, err := getTimestamp()
if err != nil {
return nil, err
}
query.Set("timestamp", ts)
//query.Set("appToken", d.Token)
query.Set("enable", "1")
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), AesSecret)
if err != nil {
return nil, err
}
query.Set("downloadId", hex.EncodeToString(downloadId))
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), time.Now().UnixMilli())), AesSecret)
if err != nil {
return nil, err
}
query.Set("auth", hex.EncodeToString(auth))
u.RawQuery = query.Encode()
link := model.Link{URL: u.String()}
return &link, nil
}

func (d *ILanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
res, err := d.proved("/file/folder/save", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"folderDesc": "",
"folderId": parentDir.GetID(),
"folderName": dirName,
})
})
if err != nil {
return nil, err
}
return &model.Object{
ID: utils.Json.Get(res, "list", "0", "id").ToString(),
//Path: "",
Name: dirName,
Size: 0,
Modified: time.Now(),
Ctime: time.Now(),
IsFolder: true,
//HashInfo: utils.HashInfo{},
}, nil
}

func (d *ILanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var fileIds, folderIds []string
if srcObj.IsDir() {
folderIds = []string{srcObj.GetID()}
} else {
fileIds = []string{srcObj.GetID()}
}
_, err := d.proved("/file/folder/move", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"folderIds": strings.Join(folderIds, ","),
"fileIds": strings.Join(fileIds, ","),
"targetId": dstDir.GetID(),
})
})
if err != nil {
return nil, err
}
return srcObj, nil
}

func (d *ILanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
var err error
if srcObj.IsDir() {
_, err = d.proved("/file/folder/edit", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"folderDesc": "",
"folderId": srcObj.GetID(),
"folderName": newName,
})
})
} else {
_, err = d.proved("/file/edit", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"fileDesc": "",
"fileId": srcObj.GetID(),
"fileName": newName,
})
})
}
if err != nil {
return nil, err
}
return &model.Object{
ID: srcObj.GetID(),
//Path: "",
Name: newName,
Size: srcObj.GetSize(),
Modified: time.Now(),
Ctime: srcObj.CreateTime(),
IsFolder: srcObj.IsDir(),
}, nil
}

func (d *ILanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
// TODO copy obj, optional
return nil, errs.NotImplement
}

func (d *ILanZou) Remove(ctx context.Context, obj model.Obj) error {
var fileIds, folderIds []string
if obj.IsDir() {
folderIds = []string{obj.GetID()}
} else {
fileIds = []string{obj.GetID()}
}
_, err := d.proved("/file/delete", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"folderIds": strings.Join(folderIds, ","),
"fileIds": strings.Join(fileIds, ","),
"status": 0,
})
})
return err
}

const DefaultPartSize = 1024 * 1024 * 8

func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
h := md5.New()
// need to calculate md5 of the full content
tempFile, err := stream.CacheFullInTempFile()
if err != nil {
return nil, err
}
defer func() {
_ = tempFile.Close()
}()
if _, err = io.Copy(h, tempFile); err != nil {
return nil, err
}
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
etag := hex.EncodeToString(h.Sum(nil))
// get upToken
res, err := d.proved("/7n/getUpToken", http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"fileId": "",
"fileName": stream.GetName(),
"fileSize": stream.GetSize() / 1024,
"folderId": dstDir.GetID(),
"md5": etag,
"type": 1,
})
})
if err != nil {
return nil, err
}
upToken := utils.Json.Get(res, "upToken").ToString()
now := time.Now()
key := fmt.Sprintf("disk/%d/%d/%d/%s/%016d", now.Year(), now.Month(), now.Day(), d.account, now.UnixMilli())
var token string
if stream.GetSize() > DefaultPartSize {
res, err := d.upClient.R().SetMultipartFormData(map[string]string{
"token": upToken,
"key": key,
"fname": stream.GetName(),
}).SetMultipartField("file", stream.GetName(), stream.GetMimetype(), tempFile).
Post("https://upload.qiniup.com/")
if err != nil {
return nil, err
}
token = utils.Json.Get(res.Body(), "token").ToString()
} else {
keyBase64 := base64.URLEncoding.EncodeToString([]byte(key))
res, err := d.upClient.R().Post(fmt.Sprintf("https://upload.qiniup.com/buckets/wpanstore-lanzou/objects/%s/uploads", keyBase64))
if err != nil {
return nil, err
}
uploadId := utils.Json.Get(res.Body(), "uploadId").ToString()
parts := make([]Part, 0)
partNum := (stream.GetSize() + DefaultPartSize - 1) / DefaultPartSize
for i := 1; i <= int(partNum); i++ {
u := fmt.Sprintf("https://upload.qiniup.com/buckets/wpanstore-lanzou/objects/%s/uploads/%s/%d", keyBase64, uploadId, i)
res, err = d.upClient.R().SetBody(io.LimitReader(tempFile, DefaultPartSize)).Put(u)
if err != nil {
return nil, err
}
etag := utils.Json.Get(res.Body(), "etag").ToString()
parts = append(parts, Part{
PartNumber: i,
ETag: etag,
})
}
res, err = d.upClient.R().SetBody(base.Json{
"fnmae": stream.GetName(),
"parts": parts,
}).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/wpanstore-lanzou/objects/%s/uploads/%s", keyBase64, uploadId))
if err != nil {
return nil, err
}
token = utils.Json.Get(res.Body(), "token").ToString()
}
// commit upload
var resp UploadResultResp
for i := 0; i < 10; i++ {
_, err = d.unproved("/7n/results", http.MethodPost, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"tokenList": token,
"tokenTime": time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"),
}).SetResult(&resp)
})
if err != nil {
return nil, err
}
if len(resp.List) == 0 {
return nil, fmt.Errorf("upload failed, empty response")
}
if resp.List[0].Status == 1 {
break
}
time.Sleep(time.Second * 1)
}
file := resp.List[0]
if file.Status != 1 {
return nil, fmt.Errorf("upload failed, status: %d", resp.List[0].Status)
}
return &model.Object{
ID: strconv.FormatInt(file.FileId, 10),
//Path: ,
Name: file.FileName,
Size: stream.GetSize(),
Modified: stream.ModTime(),
Ctime: stream.CreateTime(),
IsFolder: false,
HashInfo: utils.NewHashInfo(utils.MD5, etag),
}, nil
}

//func (d *ILanZou) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}

var _ driver.Driver = (*ILanZou)(nil)
Loading
Loading