mirror of https://github.com/Xhofe/alist
538 lines
14 KiB
Go
538 lines
14 KiB
Go
package _115
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/md5"
|
||
"crypto/tls"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/alist-org/alist/v3/internal/conf"
|
||
"github.com/alist-org/alist/v3/internal/model"
|
||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||
"github.com/alist-org/alist/v3/pkg/utils"
|
||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||
|
||
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
|
||
crypto "github.com/gaoyb7/115drive-webdav/115"
|
||
"github.com/orzogc/fake115uploader/cipher"
|
||
"github.com/pkg/errors"
|
||
)
|
||
|
||
// var UserAgent = driver115.UA115Browser
|
||
func (d *Pan115) login() error {
|
||
var err error
|
||
opts := []driver115.Option{
|
||
driver115.UA(d.getUA()),
|
||
func(c *driver115.Pan115Client) {
|
||
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
|
||
},
|
||
}
|
||
d.client = driver115.New(opts...)
|
||
cr := &driver115.Credential{}
|
||
if d.QRCodeToken != "" {
|
||
s := &driver115.QRCodeSession{
|
||
UID: d.QRCodeToken,
|
||
}
|
||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
||
return errors.Wrap(err, "failed to login by qrcode")
|
||
}
|
||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
|
||
d.QRCodeToken = ""
|
||
} else if d.Cookie != "" {
|
||
if err = cr.FromCookie(d.Cookie); err != nil {
|
||
return errors.Wrap(err, "failed to login by cookies")
|
||
}
|
||
d.client.ImportCredential(cr)
|
||
} else {
|
||
return errors.New("missing cookie or qrcode account")
|
||
}
|
||
return d.client.LoginCheck()
|
||
}
|
||
|
||
func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
|
||
res := make([]FileObj, 0)
|
||
if d.PageSize <= 0 {
|
||
d.PageSize = driver115.FileListLimit
|
||
}
|
||
files, err := d.client.ListWithLimit(fileId, d.PageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, file := range *files {
|
||
res = append(res, FileObj{file})
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
|
||
file, err := d.client.GetFile(fileId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &FileObj{*file}, nil
|
||
}
|
||
|
||
func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
|
||
result := driver115.GetFileInfoResponse{}
|
||
req := d.client.NewRequest().
|
||
SetQueryParam("pick_code", pickCode).
|
||
ForceContentType("application/json;charset=UTF-8").
|
||
SetResult(&result)
|
||
resp, err := req.Get(driver115.ApiFileInfo)
|
||
if err := driver115.CheckErr(err, &result, resp); err != nil {
|
||
return nil, err
|
||
}
|
||
if len(result.Files) == 0 {
|
||
return nil, errors.New("not get file info")
|
||
}
|
||
fileInfo := result.Files[0]
|
||
|
||
f := &FileObj{}
|
||
f.From(fileInfo)
|
||
return f, nil
|
||
}
|
||
|
||
func (d *Pan115) getUA() string {
|
||
return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
|
||
}
|
||
|
||
func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
|
||
key := crypto.GenerateKey()
|
||
result := driver115.DownloadResp{}
|
||
params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
data := crypto.Encode(params, key)
|
||
|
||
bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
|
||
reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String())
|
||
req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
|
||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
req.Header.Set("Cookie", d.Cookie)
|
||
req.Header.Set("User-Agent", ua)
|
||
|
||
resp, err := d.client.Client.GetClient().Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err := utils.Json.Unmarshal(body, &result); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if err = result.Err(string(body)); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
bytes, err := crypto.Decode(string(result.EncodedData), key)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
downloadInfo := driver115.DownloadData{}
|
||
if err := utils.Json.Unmarshal(bytes, &downloadInfo); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, info := range downloadInfo {
|
||
if info.FileSize < 0 {
|
||
return nil, driver115.ErrDownloadEmpty
|
||
}
|
||
info.Header = resp.Request.Header
|
||
return info, nil
|
||
}
|
||
return nil, driver115.ErrUnexpected
|
||
}
|
||
|
||
func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
|
||
userID := strconv.FormatInt(c.client.UserID, 10)
|
||
userIDMd5 := md5.Sum([]byte(userID))
|
||
tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
|
||
return hex.EncodeToString(tokenMd5[:])
|
||
}
|
||
|
||
func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
|
||
var (
|
||
ecdhCipher *cipher.EcdhCipher
|
||
encrypted []byte
|
||
decrypted []byte
|
||
encodedToken string
|
||
err error
|
||
target = "U_1_" + dirID
|
||
bodyBytes []byte
|
||
result = driver115.UploadInitResp{}
|
||
fileSizeStr = strconv.FormatInt(fileSize, 10)
|
||
)
|
||
if ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
userID := strconv.FormatInt(d.client.UserID, 10)
|
||
form := url.Values{}
|
||
form.Set("appid", "0")
|
||
form.Set("appversion", appVer)
|
||
form.Set("userid", userID)
|
||
form.Set("filename", fileName)
|
||
form.Set("filesize", fileSizeStr)
|
||
form.Set("fileid", fileID)
|
||
form.Set("target", target)
|
||
form.Set("sig", d.client.GenerateSignature(fileID, target))
|
||
|
||
signKey, signVal := "", ""
|
||
for retry := true; retry; {
|
||
t := driver115.NowMilli()
|
||
|
||
if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
params := map[string]string{
|
||
"k_ec": encodedToken,
|
||
}
|
||
|
||
form.Set("t", t.String())
|
||
form.Set("token", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
|
||
if signKey != "" && signVal != "" {
|
||
form.Set("sign_key", signKey)
|
||
form.Set("sign_val", signVal)
|
||
}
|
||
if encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req := d.client.NewRequest().
|
||
SetQueryParams(params).
|
||
SetBody(encrypted).
|
||
SetHeaderVerbatim("Content-Type", "application/x-www-form-urlencoded").
|
||
SetDoNotParseResponse(true)
|
||
resp, err := req.Post(driver115.ApiUploadInit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
data := resp.RawBody()
|
||
defer data.Close()
|
||
if bodyBytes, err = io.ReadAll(data); err != nil {
|
||
return nil, err
|
||
}
|
||
if decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {
|
||
return nil, err
|
||
}
|
||
if err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {
|
||
return nil, err
|
||
}
|
||
if result.Status == 7 {
|
||
// Update signKey & signVal
|
||
signKey = result.SignKey
|
||
signVal, err = UploadDigestRange(stream, result.SignCheck)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
} else {
|
||
retry = false
|
||
}
|
||
result.SHA1 = fileID
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {
|
||
var start, end int64
|
||
if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil {
|
||
return
|
||
}
|
||
|
||
length := end - start + 1
|
||
reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
hashStr, err := utils.HashReader(utils.SHA1, reader)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
result = strings.ToUpper(hashStr)
|
||
return
|
||
}
|
||
|
||
// UploadByOSS use aliyun sdk to upload
|
||
func (c *Pan115) UploadByOSS(params *driver115.UploadOSSParams, r io.Reader, dirID string) (*UploadResult, error) {
|
||
ossToken, err := c.client.GetOSSToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bucket, err := ossClient.Bucket(params.Bucket)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var bodyBytes []byte
|
||
if err = bucket.PutObject(params.Object, r, append(
|
||
driver115.OssOption(params, ossToken),
|
||
oss.CallbackResult(&bodyBytes),
|
||
)...); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var uploadResult UploadResult
|
||
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
|
||
return nil, err
|
||
}
|
||
return &uploadResult, uploadResult.Err(string(bodyBytes))
|
||
}
|
||
|
||
// UploadByMultipart upload by mutipart blocks
|
||
func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
|
||
var (
|
||
chunks []oss.FileChunk
|
||
parts []oss.UploadPart
|
||
imur oss.InitiateMultipartUploadResult
|
||
ossClient *oss.Client
|
||
bucket *oss.Bucket
|
||
ossToken *driver115.UploadOSSTokenResp
|
||
bodyBytes []byte
|
||
err error
|
||
)
|
||
|
||
tmpF, err := stream.CacheFullInTempFile()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
options := driver115.DefalutUploadMultipartOptions()
|
||
if len(opts) > 0 {
|
||
for _, f := range opts {
|
||
f(options)
|
||
}
|
||
}
|
||
// oss 启用Sequential必须按顺序上传
|
||
options.ThreadsNum = 1
|
||
|
||
if ossToken, err = d.client.GetOSSToken(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// ossToken一小时后就会失效,所以每50分钟重新获取一次
|
||
ticker := time.NewTicker(options.TokenRefreshTime)
|
||
defer ticker.Stop()
|
||
// 设置超时
|
||
timeout := time.NewTimer(options.Timeout)
|
||
|
||
if chunks, err = SplitFile(fileSize); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if imur, err = bucket.InitiateMultipartUpload(params.Object,
|
||
oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
|
||
oss.UserAgentHeader(driver115.OSSUserAgent),
|
||
oss.EnableSha1(), oss.Sequential(),
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
wg := sync.WaitGroup{}
|
||
wg.Add(len(chunks))
|
||
|
||
chunksCh := make(chan oss.FileChunk)
|
||
errCh := make(chan error)
|
||
UploadedPartsCh := make(chan oss.UploadPart)
|
||
quit := make(chan struct{})
|
||
|
||
// producer
|
||
go chunksProducer(chunksCh, chunks)
|
||
go func() {
|
||
wg.Wait()
|
||
quit <- struct{}{}
|
||
}()
|
||
|
||
// consumers
|
||
for i := 0; i < options.ThreadsNum; i++ {
|
||
go func(threadId int) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
errCh <- fmt.Errorf("recovered in %v", r)
|
||
}
|
||
}()
|
||
for chunk := range chunksCh {
|
||
var part oss.UploadPart // 出现错误就继续尝试,共尝试3次
|
||
for retry := 0; retry < 3; retry++ {
|
||
select {
|
||
case <-ticker.C:
|
||
if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
|
||
errCh <- errors.Wrap(err, "刷新token时出现错误")
|
||
}
|
||
default:
|
||
}
|
||
|
||
buf := make([]byte, chunk.Size)
|
||
if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
|
||
continue
|
||
}
|
||
|
||
if part, err = bucket.UploadPart(imur, bytes.NewBuffer(buf), chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
|
||
break
|
||
}
|
||
}
|
||
if err != nil {
|
||
errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误:%v", stream.GetName(), chunk.Number, err))
|
||
}
|
||
UploadedPartsCh <- part
|
||
}
|
||
}(i)
|
||
}
|
||
|
||
go func() {
|
||
for part := range UploadedPartsCh {
|
||
parts = append(parts, part)
|
||
wg.Done()
|
||
}
|
||
}()
|
||
LOOP:
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
// 到时重新获取ossToken
|
||
if ossToken, err = d.client.GetOSSToken(); err != nil {
|
||
return nil, err
|
||
}
|
||
case <-quit:
|
||
break LOOP
|
||
case <-errCh:
|
||
return nil, err
|
||
case <-timeout.C:
|
||
return nil, fmt.Errorf("time out")
|
||
}
|
||
}
|
||
|
||
// 不知道啥原因,oss那边分片上传不计算sha1,导致115服务器校验错误
|
||
// params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
|
||
if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
|
||
driver115.OssOption(params, ossToken),
|
||
oss.CallbackResult(&bodyBytes),
|
||
)...); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var uploadResult UploadResult
|
||
if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
|
||
return nil, err
|
||
}
|
||
return &uploadResult, uploadResult.Err(string(bodyBytes))
|
||
}
|
||
|
||
func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
|
||
for _, chunk := range chunks {
|
||
ch <- chunk
|
||
}
|
||
}
|
||
|
||
func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
|
||
for i := int64(1); i < 10; i++ {
|
||
if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片
|
||
if chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {
|
||
return
|
||
}
|
||
break
|
||
}
|
||
}
|
||
if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
|
||
if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
|
||
return
|
||
}
|
||
}
|
||
// 单个分片大小不能小于100KB
|
||
if chunks[0].Size < 100*utils.KB {
|
||
if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
|
||
return
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// SplitFileByPartNum splits big file into parts by the num of parts.
|
||
// Split the file with specified parts count, returns the split result when error is nil.
|
||
func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
|
||
if chunkNum <= 0 || chunkNum > 10000 {
|
||
return nil, errors.New("chunkNum invalid")
|
||
}
|
||
|
||
if int64(chunkNum) > fileSize {
|
||
return nil, errors.New("oss: chunkNum invalid")
|
||
}
|
||
|
||
var chunks []oss.FileChunk
|
||
chunk := oss.FileChunk{}
|
||
chunkN := (int64)(chunkNum)
|
||
for i := int64(0); i < chunkN; i++ {
|
||
chunk.Number = int(i + 1)
|
||
chunk.Offset = i * (fileSize / chunkN)
|
||
if i == chunkN-1 {
|
||
chunk.Size = fileSize/chunkN + fileSize%chunkN
|
||
} else {
|
||
chunk.Size = fileSize / chunkN
|
||
}
|
||
chunks = append(chunks, chunk)
|
||
}
|
||
|
||
return chunks, nil
|
||
}
|
||
|
||
// SplitFileByPartSize splits big file into parts by the size of parts.
|
||
// Splits the file by the part size. Returns the FileChunk when error is nil.
|
||
func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
|
||
if chunkSize <= 0 {
|
||
return nil, errors.New("chunkSize invalid")
|
||
}
|
||
|
||
chunkN := fileSize / chunkSize
|
||
if chunkN >= 10000 {
|
||
return nil, errors.New("Too many parts, please increase part size")
|
||
}
|
||
|
||
var chunks []oss.FileChunk
|
||
chunk := oss.FileChunk{}
|
||
for i := int64(0); i < chunkN; i++ {
|
||
chunk.Number = int(i + 1)
|
||
chunk.Offset = i * chunkSize
|
||
chunk.Size = chunkSize
|
||
chunks = append(chunks, chunk)
|
||
}
|
||
|
||
if fileSize%chunkSize > 0 {
|
||
chunk.Number = len(chunks) + 1
|
||
chunk.Offset = int64(len(chunks)) * chunkSize
|
||
chunk.Size = fileSize % chunkSize
|
||
chunks = append(chunks, chunk)
|
||
}
|
||
|
||
return chunks, nil
|
||
}
|