|
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 |
|
conf Conf |
|
config driver.Config |
|
} |
|
|
|
func (d *ILanZou) Config() driver.Config { |
|
return d.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 |
|
var res []ListItem |
|
for { |
|
var resp ListResp |
|
_, err := d.proved("/record/file/list", http.MethodGet, func(req *resty.Request) { |
|
params := []string{ |
|
"offset=" + strconv.Itoa(offset), |
|
"limit=60", |
|
"folderId=" + dir.GetID(), |
|
"type=0", |
|
} |
|
queryString := strings.Join(params, "&") |
|
req.SetQueryString(queryString).SetResult(&resp) |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
res = append(res, resp.List...) |
|
if resp.Offset < resp.TotalPage { |
|
offset++ |
|
} else { |
|
break |
|
} |
|
} |
|
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), |
|
|
|
Name: f.FileName, |
|
Size: f.FileSize * 1024, |
|
Modified: updTime, |
|
Ctime: updTime, |
|
IsFolder: false, |
|
|
|
} |
|
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(d.conf.base + "/" + d.conf.unproved + "/file/redirect") |
|
if err != nil { |
|
return nil, err |
|
} |
|
ts, ts_str, err := getTimestamp(d.conf.secret) |
|
|
|
params := []string{ |
|
"uuid=" + url.QueryEscape(d.UUID), |
|
"devType=6", |
|
"devCode=" + url.QueryEscape(d.UUID), |
|
"devModel=chrome", |
|
"devVersion=" + url.QueryEscape(d.conf.devVersion), |
|
"appVersion=", |
|
"timestamp=" + ts_str, |
|
"appToken=" + url.QueryEscape(d.Token), |
|
"enable=0", |
|
} |
|
|
|
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret) |
|
if err != nil { |
|
return nil, err |
|
} |
|
params = append(params, "downloadId="+url.QueryEscape(hex.EncodeToString(downloadId))) |
|
|
|
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), ts)), d.conf.secret) |
|
if err != nil { |
|
return nil, err |
|
} |
|
params = append(params, "auth="+url.QueryEscape(hex.EncodeToString(auth))) |
|
|
|
u.RawQuery = strings.Join(params, "&") |
|
realURL := u.String() |
|
|
|
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{ |
|
|
|
"Referer": d.conf.site + "/", |
|
"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", |
|
}).Get(realURL) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if res.StatusCode() == 302 { |
|
realURL = res.Header().Get("location") |
|
} else { |
|
return nil, fmt.Errorf("redirect failed, status: %d, msg: %s", res.StatusCode(), utils.Json.Get(res.Body(), "msg").ToString()) |
|
} |
|
link := model.Link{URL: realURL} |
|
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(), |
|
|
|
Name: dirName, |
|
Size: 0, |
|
Modified: time.Now(), |
|
Ctime: time.Now(), |
|
IsFolder: true, |
|
|
|
}, 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(), |
|
|
|
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) { |
|
|
|
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() |
|
|
|
tempFile, err := stream.CacheFullInTempFile() |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer func() { |
|
_ = tempFile.Close() |
|
}() |
|
if _, err = utils.CopyWithBuffer(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)) |
|
|
|
res, err := d.proved("/7n/getUpToken", http.MethodPost, func(req *resty.Request) { |
|
req.SetBody(base.Json{ |
|
"fileId": "", |
|
"fileName": stream.GetName(), |
|
"fileSize": stream.GetSize()/1024 + 1, |
|
"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().SetHeader("Authorization", "UpToken "+upToken).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads", d.conf.bucket, 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/%s/objects/%s/uploads/%s/%d", d.conf.bucket, keyBase64, uploadId, i) |
|
res, err = d.upClient.R().SetHeader("Authorization", "UpToken "+upToken).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().SetHeader("Authorization", "UpToken "+upToken).SetBody(base.Json{ |
|
"fnmae": stream.GetName(), |
|
"parts": parts, |
|
}).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s", d.conf.bucket, keyBase64, uploadId)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
token = utils.Json.Get(res.Body(), "token").ToString() |
|
} |
|
|
|
var resp UploadResultResp |
|
for i := 0; i < 10; i++ { |
|
_, err = d.unproved("/7n/results", http.MethodPost, func(req *resty.Request) { |
|
params := []string{ |
|
"tokenList=" + token, |
|
"tokenTime=" + time.Now().Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)"), |
|
} |
|
queryString := strings.Join(params, "&") |
|
req.SetQueryString(queryString).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), |
|
|
|
Name: file.FileName, |
|
Size: stream.GetSize(), |
|
Modified: stream.ModTime(), |
|
Ctime: stream.CreateTime(), |
|
IsFolder: false, |
|
HashInfo: utils.NewHashInfo(utils.MD5, etag), |
|
}, nil |
|
} |
|
|
|
|
|
|
|
|
|
|
|
var _ driver.Driver = (*ILanZou)(nil) |
|
|