|
package aliyundrive |
|
|
|
import ( |
|
"crypto/sha256" |
|
"encoding/hex" |
|
"errors" |
|
"fmt" |
|
"net/http" |
|
|
|
"github.com/alist-org/alist/v3/drivers/base" |
|
"github.com/alist-org/alist/v3/internal/op" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
"github.com/dustinxie/ecc" |
|
"github.com/go-resty/resty/v2" |
|
"github.com/google/uuid" |
|
) |
|
|
|
func (d *AliDrive) createSession() error { |
|
state, ok := global.Load(d.UserID) |
|
if !ok { |
|
return fmt.Errorf("can't load user state, user_id: %s", d.UserID) |
|
} |
|
d.sign() |
|
state.retry++ |
|
if state.retry > 3 { |
|
state.retry = 0 |
|
return fmt.Errorf("createSession failed after three retries") |
|
} |
|
_, err, _ := d.request("https://api.alipan.com/users/v1/users/device/create_session", http.MethodPost, func(req *resty.Request) { |
|
req.SetBody(base.Json{ |
|
"deviceName": "samsung", |
|
"modelName": "SM-G9810", |
|
"nonce": 0, |
|
"pubKey": PublicKeyToHex(&state.privateKey.PublicKey), |
|
"refreshToken": d.RefreshToken, |
|
}) |
|
}, nil) |
|
if err == nil{ |
|
state.retry = 0 |
|
} |
|
return err |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
func (d *AliDrive) sign() { |
|
state, _ := global.Load(d.UserID) |
|
secpAppID := "5dde4e1bdf9e4966b387ba58f4b3fdc3" |
|
singdata := fmt.Sprintf("%s:%s:%s:%d", secpAppID, state.deviceID, d.UserID, 0) |
|
hash := sha256.Sum256([]byte(singdata)) |
|
data, _ := ecc.SignBytes(state.privateKey, hash[:], ecc.RecID|ecc.LowerS) |
|
state.signature = hex.EncodeToString(data) |
|
} |
|
|
|
|
|
|
|
func (d *AliDrive) refreshToken() error { |
|
url := "https://auth.alipan.com/v2/account/token" |
|
var resp base.TokenResp |
|
var e RespErr |
|
_, err := base.RestyClient.R(). |
|
|
|
SetBody(base.Json{"refresh_token": d.RefreshToken, "grant_type": "refresh_token"}). |
|
SetResult(&resp). |
|
SetError(&e). |
|
Post(url) |
|
if err != nil { |
|
return err |
|
} |
|
if e.Code != "" { |
|
return fmt.Errorf("failed to refresh token: %s", e.Message) |
|
} |
|
if resp.RefreshToken == "" { |
|
return errors.New("failed to refresh token: refresh token is empty") |
|
} |
|
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken |
|
op.MustSaveDriverStorage(d) |
|
return nil |
|
} |
|
|
|
func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error, RespErr) { |
|
req := base.RestyClient.R() |
|
state, ok := global.Load(d.UserID) |
|
if !ok { |
|
if url == "https://api.alipan.com/v2/user/get" { |
|
state = &State{} |
|
} else { |
|
return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{} |
|
} |
|
} |
|
req.SetHeaders(map[string]string{ |
|
"Authorization": "Bearer\t" + d.AccessToken, |
|
"content-type": "application/json", |
|
"origin": "https://www.alipan.com", |
|
"Referer": "https://alipan.com/", |
|
"X-Signature": state.signature, |
|
"x-request-id": uuid.NewString(), |
|
"X-Canary": "client=Android,app=adrive,version=v4.1.0", |
|
"X-Device-Id": state.deviceID, |
|
}) |
|
if callback != nil { |
|
callback(req) |
|
} else { |
|
req.SetBody("{}") |
|
} |
|
if resp != nil { |
|
req.SetResult(resp) |
|
} |
|
var e RespErr |
|
req.SetError(&e) |
|
res, err := req.Execute(method, url) |
|
if err != nil { |
|
return nil, err, e |
|
} |
|
if e.Code != "" { |
|
switch e.Code { |
|
case "AccessTokenInvalid": |
|
err = d.refreshToken() |
|
if err != nil { |
|
return nil, err, e |
|
} |
|
case "DeviceSessionSignatureInvalid": |
|
err = d.createSession() |
|
if err != nil { |
|
return nil, err, e |
|
} |
|
default: |
|
return nil, errors.New(e.Message), e |
|
} |
|
return d.request(url, method, callback, resp) |
|
} else if res.IsError() { |
|
return nil, errors.New("bad status code " + res.Status()), e |
|
} |
|
return res.Body(), nil, e |
|
} |
|
|
|
func (d *AliDrive) getFiles(fileId string) ([]File, error) { |
|
marker := "first" |
|
res := make([]File, 0) |
|
for marker != "" { |
|
if marker == "first" { |
|
marker = "" |
|
} |
|
var resp Files |
|
data := base.Json{ |
|
"drive_id": d.DriveId, |
|
"fields": "*", |
|
"image_thumbnail_process": "image/resize,w_400/format,jpeg", |
|
"image_url_process": "image/resize,w_1920/format,jpeg", |
|
"limit": 200, |
|
"marker": marker, |
|
"order_by": d.OrderBy, |
|
"order_direction": d.OrderDirection, |
|
"parent_file_id": fileId, |
|
"video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300", |
|
"url_expire_sec": 14400, |
|
} |
|
_, err, _ := d.request("https://api.alipan.com/v2/file/list", http.MethodPost, func(req *resty.Request) { |
|
req.SetBody(data) |
|
}, &resp) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
marker = resp.NextMarker |
|
res = append(res, resp.Items...) |
|
} |
|
return res, nil |
|
} |
|
|
|
func (d *AliDrive) batch(srcId, dstId string, url string) error { |
|
res, err, _ := d.request("https://api.alipan.com/v3/batch", http.MethodPost, func(req *resty.Request) { |
|
req.SetBody(base.Json{ |
|
"requests": []base.Json{ |
|
{ |
|
"headers": base.Json{ |
|
"Content-Type": "application/json", |
|
}, |
|
"method": "POST", |
|
"id": srcId, |
|
"body": base.Json{ |
|
"drive_id": d.DriveId, |
|
"file_id": srcId, |
|
"to_drive_id": d.DriveId, |
|
"to_parent_file_id": dstId, |
|
}, |
|
"url": url, |
|
}, |
|
}, |
|
"resource": "file", |
|
}) |
|
}, nil) |
|
if err != nil { |
|
return err |
|
} |
|
status := utils.Json.Get(res, "responses", 0, "status").ToInt() |
|
if status < 400 && status >= 100 { |
|
return nil |
|
} |
|
return errors.New(string(res)) |
|
} |
|
|