xunleicloud support

This commit is contained in:
foxxorcat 2022-03-03 15:44:41 +08:00
parent 90b57dacee
commit 2964d5a6db
7 changed files with 826 additions and 0 deletions

View File

@ -20,6 +20,7 @@ import (
_ "github.com/Xhofe/alist/drivers/shandian"
_ "github.com/Xhofe/alist/drivers/teambition"
_ "github.com/Xhofe/alist/drivers/webdav"
_ "github.com/Xhofe/alist/drivers/xunlei"
_ "github.com/Xhofe/alist/drivers/yandex"
log "github.com/sirupsen/logrus"
"strings"

274
drivers/xunlei/driver.go Normal file
View File

@ -0,0 +1,274 @@
package xunlei
import (
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/Xhofe/alist/conf"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
log "github.com/sirupsen/logrus"
)
type XunLeiCloud struct{}
func init() {
base.RegisterDriver(new(XunLeiCloud))
}
func (driver XunLeiCloud) Config() base.DriverConfig {
return base.DriverConfig{
Name: "XunLeiCloud",
LocalSort: true,
}
}
func (driver XunLeiCloud) Items() []base.Item {
return []base.Item{
{
Name: "username",
Label: "username",
Type: base.TypeString,
Required: true,
Description: "account username/phone number",
},
{
Name: "password",
Label: "password",
Type: base.TypeString,
Required: true,
Description: "account password",
},
{
Name: "root_folder",
Label: "root folder file_id",
Type: base.TypeString,
Required: true,
},
}
}
func (driver XunLeiCloud) Save(account *model.Account, old *model.Account) error {
if account == nil {
return nil
}
return GetState(account).Login(account)
}
func (driver XunLeiCloud) File(path string, account *model.Account) (*model.File, error) {
path = utils.ParsePath(path)
if path == "/" {
return &model.File{
Id: account.RootFolder,
Name: account.Name,
Size: 0,
Type: conf.FOLDER,
Driver: driver.Config().Name,
UpdatedAt: account.UpdatedAt,
}, nil
}
dir, name := filepath.Split(path)
files, err := driver.Files(dir, account)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == name {
return &file, nil
}
}
return nil, base.ErrPathNotFound
}
func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.File, error) {
file, err := driver.File(utils.ParsePath(path), account)
if err != nil {
return nil, err
}
var fileList FileList
url := fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files?parent_id=%s&page_token=%s&with_audit=true&filters=%s", file.Id, "", url.QueryEscape(`{"phase": {"eq": "PHASE_TYPE_COMPLETE"}, "trashed":{"eq":false}}`))
if err = GetState(account).Request("GET", url, nil, &fileList, account); err != nil {
return nil, err
}
files := make([]model.File, 0, len(fileList.Files))
for _, file := range fileList.Files {
if file.Kind == FOLDER || (file.Kind == FILE && file.Audit.Status == "STATUS_OK") {
files = append(files, *driver.formatFile(&file))
}
}
return files, nil
}
func (driver XunLeiCloud) formatFile(file *Files) *model.File {
size, _ := strconv.ParseInt(file.Size, 10, 64)
tp := conf.FOLDER
if file.Kind == FILE {
tp = utils.GetFileType(file.FileExtension)
}
return &model.File{
Id: file.ID,
Name: file.Name,
Size: size,
Type: tp,
Driver: driver.Config().Name,
UpdatedAt: file.CreatedTime,
Thumbnail: file.ThumbnailLink,
}
}
func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Link, error) {
file, err := driver.File(utils.ParsePath(args.Path), account)
if err != nil {
return nil, err
}
if file.Type == conf.FOLDER {
return nil, base.ErrNotFile
}
var lfile Files
if err = GetState(account).Request("GET", fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files/%s?&with_audit=true", file.Id), nil, &lfile, account); err != nil {
return nil, err
}
return &base.Link{
Headers: []base.Header{
{Name: "User-Agent", Value: base.UserAgent},
},
Url: lfile.WebContentLink,
}, nil
}
func (driver XunLeiCloud) Path(path string, account *model.Account) (*model.File, []model.File, error) {
path = utils.ParsePath(path)
log.Debugf("xunlei path: %s", path)
file, err := driver.File(path, account)
if err != nil {
return nil, nil, err
}
if !file.IsDir() {
return file, nil, nil
}
files, err := driver.Files(path, account)
if err != nil {
return nil, nil, err
}
return nil, files, nil
}
func (driver XunLeiCloud) Preview(path string, account *model.Account) (interface{}, error) {
return nil, base.ErrNotSupport
}
func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error {
dir, name := filepath.Split(path)
parentFile, err := driver.File(dir, account)
if err != nil {
return err
}
if !parentFile.IsDir() {
return base.ErrNotFolder
}
return GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files", &base.Json{"kind": FOLDER, "name": name, "parent_id": parentFile.Id}, nil, account)
}
func (driver XunLeiCloud) Move(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(filepath.Dir(dst), account)
if err != nil {
return err
}
return GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files:batchMove", &base.Json{"to": base.Json{"parent_id": dstDirFile.Id}, "ids": []string{srcFile.Id}}, nil, account)
}
func (driver XunLeiCloud) Copy(src string, dst string, account *model.Account) error {
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
dstDirFile, err := driver.File(filepath.Dir(dst), account)
if err != nil {
return err
}
return GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files:batchCopy", &base.Json{"to": base.Json{"parent_id": dstDirFile.Id}, "ids": []string{srcFile.Id}}, nil, account)
}
func (driver XunLeiCloud) Delete(path string, account *model.Account) error {
srcFile, err := driver.File(path, account)
if err != nil {
return err
}
return GetState(account).Request("PATCH", fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files/%s/trash", srcFile.Id), &base.Json{}, nil, account)
}
func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account) error {
if file == nil {
return base.ErrEmptyFile
}
parentFile, err := driver.File(file.ParentPath, account)
if err != nil {
return err
}
tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
if err != nil {
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
gcid, err := getGcid(io.TeeReader(file, tempFile), int64(file.Size))
if err != nil {
return err
}
var rep UploadTaskResponse
err = GetState(account).Request("POST", "https://api-pan.xunlei.com/drive/v1/files", &base.Json{
"kind": FILE,
"parent_id": parentFile.Id,
"name": file.Name,
"size": fmt.Sprint(file.Size),
"hash": gcid,
"upload_type": UPLOAD_TYPE_RESUMABLE,
}, &rep, account)
if err != nil {
return err
}
param := rep.Resumable.Params
client, err := oss.New(param.Endpoint, param.AccessKeyID, param.AccessKeySecret, oss.SecurityToken(param.SecurityToken), oss.EnableMD5(true), oss.HTTPClient(xunleiClient.GetClient()))
if err != nil {
return err
}
bucket, err := client.Bucket(param.Bucket)
if err != nil {
return err
}
return bucket.UploadFile(param.Key, tempFile.Name(), 4*1024*1024, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration))
}
func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error {
_, dstName := filepath.Split(dst)
srcFile, err := driver.File(src, account)
if err != nil {
return err
}
return GetState(account).Request("PATCH", fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/files/%s", srcFile.Id), &base.Json{"name": dstName}, nil, account)
}
var _ base.Driver = (*XunLeiCloud)(nil)

154
drivers/xunlei/types.go Normal file
View File

@ -0,0 +1,154 @@
package xunlei
import (
"time"
)
type Erron struct {
Error string `json:"error"`
ErrorCode int64 `json:"error_code"`
ErrorDescription string `json:"error_description"`
// ErrorDetails interface{} `json:"error_details"`
}
type CaptchaTokenRequest struct {
Action string `json:"action"`
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
DeviceID string `json:"device_id"`
Meta map[string]string `json:"meta"`
//RedirectUri string `json:"redirect_uri"`
}
type CaptchaTokenResponse struct {
CaptchaToken string `json:"captcha_token"`
ExpiresIn int64 `json:"expires_in"`
Url string `json:"url"`
}
type TokenResponse struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
Sub string `json:"sub"`
UserID string `json:"user_id"`
}
type SignInRequest struct {
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Username string `json:"username"`
Password string `json:"password"`
}
type FileList struct {
Kind string `json:"kind"`
NextPageToken string `json:"next_page_token"`
Files []Files `json:"files"`
Version string `json:"version"`
VersionOutdated bool `json:"version_outdated"`
}
type Files struct {
Kind string `json:"kind"`
ID string `json:"id"`
ParentID string `json:"parent_id"`
Name string `json:"name"`
UserID string `json:"user_id"`
Size string `json:"size"`
Revision string `json:"revision"`
FileExtension string `json:"file_extension"`
MimeType string `json:"mime_type"`
Starred bool `json:"starred"`
WebContentLink string `json:"web_content_link"`
CreatedTime *time.Time `json:"created_time"`
ModifiedTime *time.Time `json:"modified_time"`
IconLink string `json:"icon_link"`
ThumbnailLink string `json:"thumbnail_link"`
Md5Checksum string `json:"md5_checksum"`
Hash string `json:"hash"`
//Links struct{} `json:"links"`
Phase string `json:"phase"`
Audit struct {
Status string `json:"status"`
Message string `json:"message"`
Title string `json:"title"`
} `json:"audit"`
/* Medias []struct {
Category string `json:"category"`
IconLink string `json:"icon_link"`
IsDefault bool `json:"is_default"`
IsOrigin bool `json:"is_origin"`
IsVisible bool `json:"is_visible"`
//Link interface{} `json:"link"`
MediaID string `json:"media_id"`
MediaName string `json:"media_name"`
NeedMoreQuota bool `json:"need_more_quota"`
Priority int `json:"priority"`
RedirectLink string `json:"redirect_link"`
ResolutionName string `json:"resolution_name"`
Video struct {
AudioCodec string `json:"audio_codec"`
BitRate int `json:"bit_rate"`
Duration int `json:"duration"`
FrameRate int `json:"frame_rate"`
Height int `json:"height"`
VideoCodec string `json:"video_codec"`
VideoType string `json:"video_type"`
Width int `json:"width"`
} `json:"video"`
VipTypes []string `json:"vip_types"`
} `json:"medias"` */
Trashed bool `json:"trashed"`
DeleteTime string `json:"delete_time"`
OriginalURL string `json:"original_url"`
//Params struct{} `json:"params"`
OriginalFileIndex int `json:"original_file_index"`
Space string `json:"space"`
//Apps []interface{} `json:"apps"`
Writable bool `json:"writable"`
FolderType string `json:"folder_type"`
//Collection interface{} `json:"collection"`
}
type UploadTaskResponse struct {
UploadType string `json:"upload_type"`
/*//UPLOAD_TYPE_FORM
Form struct {
//Headers struct{} `json:"headers"`
Kind string `json:"kind"`
Method string `json:"method"`
MultiParts struct {
OSSAccessKeyID string `json:"OSSAccessKeyId"`
Signature string `json:"Signature"`
Callback string `json:"callback"`
Key string `json:"key"`
Policy string `json:"policy"`
XUserData string `json:"x:user_data"`
} `json:"multi_parts"`
URL string `json:"url"`
} `json:"form"`*/
//UPLOAD_TYPE_RESUMABLE
Resumable struct {
Kind string `json:"kind"`
Params struct {
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
Expiration time.Time `json:"expiration"`
Key string `json:"key"`
SecurityToken string `json:"security_token"`
} `json:"params"`
Provider string `json:"provider"`
} `json:"resumable"`
File Files `json:"file"`
}

103
drivers/xunlei/util.go Normal file
View File

@ -0,0 +1,103 @@
package xunlei
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"net/url"
"github.com/Xhofe/alist/utils"
)
const (
// 小米浏览器
CLIENT_ID = "X7MtiU0Gb5YqWv-6"
CLIENT_SECRET = "84MYEih3Eeu2HF4RrGce3Q"
CLIENT_VERSION = "5.1.0.51045"
ALG_VERSION = "1"
PACKAGE_NAME = "com.xunlei.xcloud.lib"
)
var Algorithms = []string{
"",
"BXza40wm+P4zw8rEFpHA",
"UfZLfKfYRmKTA0",
"OMBGVt/9Wcaln1XaBz",
"Jn217F4rk5FPPWyhoeV",
"w5OwkGo0pGpb0Xe/XZ5T3",
"5guM3DNiY4F78x49zQ97q75",
"QXwn4D2j884wJgrYXjGClM/IVrJX",
"NXBRosYvbHIm6w8vEB",
"2kZ8Ie1yW2ib4O2iAkNpJobP",
"11CoVJJQEc",
"xf3QWysVwnVsNv5DCxU+cgNT1rK",
"9eEfKkrqkfw",
"T78dnANexYRbiZy",
}
const (
FOLDER = "drive#folder"
FILE = "drive#file"
RESUMABLE = "drive#resumable"
)
const (
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
)
func captchaSign(driverID string, time int64) string {
str := fmt.Sprint(CLIENT_ID, CLIENT_VERSION, PACKAGE_NAME, driverID, time)
for _, algorithm := range Algorithms {
str = utils.GetMD5Encode(fmt.Sprint(str, algorithm))
}
return fmt.Sprint(ALG_VERSION, ".", str)
}
func getAction(method string, u string) string {
c, _ := url.Parse(u)
return fmt.Sprint(method, ":", c.Path)
}
func getGcid(r io.Reader, size int64) (string, error) {
calcBlockSize := func(j int64) int64 {
if j >= 0 && j <= 134217728 {
return 262144
}
if j <= 134217728 || j > 268435456 {
if j <= 268435456 || j > 536870912 {
return 2097152
}
return 1048576
}
return 524288
}
/*
calcBlockSize := func(j int64) int64 {
psize := int64(0x40000)
for j/psize > 0x200 {
psize <<= 1
}
return psize
}
*/
hash1 := sha1.New()
hash2 := sha1.New()
for {
hash2.Reset()
if n, err := io.CopyN(hash2, r, calcBlockSize(size)); err != nil && n == 0 {
if err != io.EOF {
return "", err
}
break
}
hash1.Write(hash2.Sum(nil))
}
return hex.EncodeToString(hash1.Sum(nil)), nil
}

288
drivers/xunlei/xunlei.go Normal file
View File

@ -0,0 +1,288 @@
package xunlei
import (
"fmt"
"sync"
"time"
"github.com/Xhofe/alist/drivers/base"
"github.com/Xhofe/alist/model"
"github.com/Xhofe/alist/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
var xunleiClient = resty.New().SetTimeout(120 * time.Second)
// 一个账户只允许登陆一次
var userStateCache = struct {
sync.Mutex
States map[string]*State
}{States: make(map[string]*State)}
func GetState(account *model.Account) *State {
userStateCache.Lock()
defer userStateCache.Unlock()
if v, ok := userStateCache.States[account.Username]; ok && v != nil {
return v
}
state := new(State).Init()
userStateCache.States[account.Username] = state
return state
}
type State struct {
sync.Mutex
captchaToken string
captchaTokenExpiresTime int64
tokenType string
accessToken string
refreshToken string
tokenExpiresTime int64 //Milli
userID string
}
func (s *State) init() *State {
s.captchaToken = ""
s.captchaTokenExpiresTime = 0
s.tokenType = ""
s.accessToken = ""
s.refreshToken = ""
s.tokenExpiresTime = 0
s.userID = "0"
return s
}
func (s *State) getToken(account *model.Account) (string, error) {
if s.isTokensExpires() {
if err := s.refreshToken_(account); err != nil {
return "", err
}
}
return fmt.Sprint(s.tokenType, " ", s.accessToken), nil
}
func (s *State) getCaptchaToken(action string, account *model.Account) (string, error) {
if s.isCaptchaTokenExpires() {
return s.newCaptchaToken(action, nil, account)
}
return s.captchaToken, nil
}
func (s *State) isCaptchaTokenExpires() bool {
return time.Now().UnixMilli() >= s.captchaTokenExpiresTime || s.captchaToken == "" || s.tokenType == ""
}
func (s *State) isTokensExpires() bool {
return time.Now().UnixMilli() >= s.tokenExpiresTime || s.accessToken == ""
}
func (s *State) newCaptchaToken(action string, meta map[string]string, account *model.Account) (string, error) {
ctime := time.Now().UnixMilli()
driverID := utils.GetMD5Encode(account.Username)
creq := CaptchaTokenRequest{
Action: action,
CaptchaToken: s.captchaToken,
ClientID: CLIENT_ID,
DeviceID: driverID,
Meta: map[string]string{
"captcha_sign": captchaSign(driverID, ctime),
"client_version": CLIENT_VERSION,
"package_name": PACKAGE_NAME,
"timestamp": fmt.Sprint(ctime),
"user_id": s.userID,
},
}
for k, v := range meta {
creq.Meta[k] = v
}
var e Erron
var resp CaptchaTokenResponse
_, err := xunleiClient.R().
SetHeader("X-Device-Id", driverID).
SetBody(&creq).
SetError(&e).
SetResult(&resp).
Post("https://xluser-ssl.xunlei.com/v1/shield/captcha/init?client_id=" + CLIENT_ID)
if err != nil {
return "", err
}
if e.ErrorCode != 0 {
log.Debugf("%+v\n %+v", e, account)
return "", fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
}
if resp.Url != "" {
return "", fmt.Errorf("需要验证验证码")
}
s.captchaTokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
s.captchaToken = resp.CaptchaToken
log.Debugf("%+v\n %+v", s.captchaToken, account)
return s.captchaToken, nil
}
func (s *State) refreshToken_(account *model.Account) error {
var e Erron
var resp TokenResponse
_, err := xunleiClient.R().
SetResult(&resp).SetError(&e).
SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": s.refreshToken,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}).
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).SetQueryParam("client_id", CLIENT_ID).
Post("https://xluser-ssl.xunlei.com/v1/auth/token")
if err != nil {
return err
}
switch e.ErrorCode {
case 4122, 4121:
return s.login(account)
case 0:
s.tokenExpiresTime = (time.Now().UnixMilli() + resp.ExpiresIn*1000) - 30000
s.tokenType = resp.TokenType
s.accessToken = resp.AccessToken
s.refreshToken = resp.RefreshToken
s.userID = resp.UserID
return nil
default:
log.Debugf("%+v\n %+v", e, account)
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
}
}
func (s *State) login(account *model.Account) error {
s.init()
ctime := time.Now().UnixMilli()
url := "https://xluser-ssl.xunlei.com/v1/auth/signin"
captchaToken, err := s.newCaptchaToken(getAction("POST", url), map[string]string{"username": account.Username}, account)
if err != nil {
return err
}
signReq := SignInRequest{
CaptchaToken: captchaToken,
ClientID: CLIENT_ID,
ClientSecret: CLIENT_SECRET,
Username: account.Username,
Password: account.Password,
}
var e Erron
var resp TokenResponse
_, err = xunleiClient.R().
SetResult(&resp).
SetError(&e).
SetBody(&signReq).
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).
SetQueryParam("client_id", CLIENT_ID).
Post(url)
if err != nil {
return err
}
defer model.SaveAccount(account)
if e.ErrorCode != 0 {
account.Status = e.Error
log.Debugf("%+v\n %+v", e, account)
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
}
account.Status = "work"
s.tokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
s.tokenType = resp.TokenType
s.accessToken = resp.AccessToken
s.refreshToken = resp.RefreshToken
s.userID = resp.UserID
log.Debugf("%+v\n %+v", resp, account)
return nil
}
func (s *State) Request(method string, url string, body interface{}, resp interface{}, account *model.Account) error {
s.Lock()
token, err := s.getToken(account)
if err != nil {
return err
}
captchaToken, err := s.getCaptchaToken(getAction(method, url), account)
if err != nil {
return err
}
s.Unlock()
var e Erron
req := xunleiClient.R().
SetError(&e).
SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).
SetHeader("Authorization", token).
SetHeader("X-Captcha-Token", captchaToken).
SetQueryParam("client_id", CLIENT_ID)
if body != nil {
req.SetBody(body)
}
if resp != nil {
req.SetResult(resp)
}
switch method {
case "GET":
_, err = req.Get(url)
case "POST":
_, err = req.Post(url)
case "DELETE":
_, err = req.Delete(url)
case "PATCH":
_, err = req.Patch(url)
case "PUT":
_, err = req.Put(url)
default:
return base.ErrNotSupport
}
if err != nil {
return err
}
switch e.ErrorCode {
case 0:
return nil
case 9:
s.newCaptchaToken(getAction(method, url), nil, account)
fallthrough
case 4122, 4121:
return s.Request(method, url, body, resp, account)
default:
log.Debugf("%+v\n %+v", e, account)
return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
}
}
func (s *State) Init() *State {
s.Lock()
defer s.Unlock()
return s.init()
}
func (s *State) GetCaptchaToken(action string, account *model.Account) (string, error) {
s.Lock()
defer s.Unlock()
return s.getCaptchaToken(action, account)
}
func (s *State) GetToken(account *model.Account) (string, error) {
s.Lock()
defer s.Unlock()
return s.getToken(account)
}
func (s *State) Login(account *model.Account) error {
s.Lock()
defer s.Unlock()
return s.login(account)
}

3
go.mod
View File

@ -23,8 +23,11 @@ require (
gorm.io/gorm v1.23.1
)
require golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
require (
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/aliyun/aliyun-oss-go-sdk v2.2.1+incompatible
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect

3
go.sum
View File

@ -20,6 +20,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/aliyun/aliyun-oss-go-sdk v2.2.1+incompatible h1:uuJIwCFhbZy+zdvLy5zrcIToPEQP0s5CFOZ0Zj03O/w=
github.com/aliyun/aliyun-oss-go-sdk v2.2.1+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/allegro/bigcache/v2 v2.2.5 h1:mRc8r6GQjuJsmSKQNPsR5jQVXc8IJ1xsW5YXUYMLfqI=
github.com/allegro/bigcache/v2 v2.2.5/go.mod h1:FppZsIO+IZk7gCuj5FiIDHGygD9xvWQcqg1uIPMb6tY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@ -659,6 +661,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=