Repository: AlistGo/alist (language: go)

Issue:
Content-Type 问题
### Please make sure of the following things

- [X] I have read the [documentation](https://alist-doc.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

在 `server/controllers/file/upload.go UploadFiles()` 中，由于直接使用接收到的 Content-Type 作为文件的 MIMIType，并且在 Upload 时直接发出去，这可能引发一系列的问题。如：

### 上传错误
如果使用 quark + webdav 上传，在 `UpPre()` 过程中需要使用 `format_type` 参数，而这一参数来自于上述的 `model.FileStream.MIMEType`，如果 webdav PUT 报文未携带 Content-Type 信息（如 python 的 webdav3 库），则会报错
```
[GIN] 2022/04/28 - 12:29:17 | 302 |     66.6303ms |       127.0.0.1 | HEAD     "/dav/quark/test/"
[GIN] 2022/04/28 - 12:29:17 | 200 |       294.8µs |       127.0.0.1 | HEAD     "/p/quark/test?sign=8db09a326853fe4f"
ERRO[2022-04-28 12:29:17] upload error: Bad Parameter: [format_type is null!] 
ERRO[2022-04-28 12:29:17] Bad Parameter: [format_type is null!]        
[GIN] 2022/04/28 - 12:29:17 | 405 |     50.0236ms |       127.0.0.1 | PUT      "/dav/quark/test/cloud.py"
[GIN] 2022/04/28 - 12:29:17 | 207 |    318.0504ms |       127.0.0.1 | PROPFIND  "/dav/quark/test/"
[GIN] 2022/04/28 - 12:29:17 | 207 |       342.4µs |       127.0.0.1 | PROPFIND  "/dav/quark/"
[GIN] 2022/04/28 - 12:29:17 | 207 |       286.5µs |       127.0.0.1 | PROPFIND  "/dav/"
```
### 类型错误
另一种情况是，RaiDrive 在进行文件传输时，不论什么文件，都将 Content-Type 设置为 application/octet-stream，这将导致 quark 无法正确识别文件类型，从而使网页端视频文件无法在线播放，zip文件无法在线解压

![image](https://user-images.githubusercontent.com/34620278/165682125-0dae3b60-ba49-47bd-8705-4ad349a34a17.png)
![image](https://user-images.githubusercontent.com/34620278/165682438-5c6c7d7c-65c9-4888-93a8-f96ed6edebaa.png)



### Suggested solution / 实现思路

希望可以在接受或者发送文件时重新生成/识别一次 MIMEType

### Additional context / 附件

_No response_


Discussion / hints:

Fix patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 27d8a508922..55ad79107d5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,7 +7,7 @@
 Prerequisites:
 
 - [git](https://nodejs.org/zh-cn/)
-- [Go 1.17+](https://golang.org/doc/install)
+- [Go 1.18+](https://golang.org/doc/install)
 - [gcc](https://gcc.gnu.org/)
 - [nodejs](https://nodejs.org/)
 
diff --git a/bootstrap/setting.go b/bootstrap/setting.go
index 3c7c898be08..6591a774d3a 100644
--- a/bootstrap/setting.go
+++ b/bootstrap/setting.go
@@ -266,6 +266,22 @@ func InitSettings() {
 			Group:       model.BACK,
 			Description: "Experimental function, not recommended as it's still under development",
 		},
+		{
+			Key:         "Aria2 RPC url",
+			Value:       "http://localhost:6800/jsonrpc",
+			Description: "Aria2 RPC url, e.g. 'http://aria2.example.com:6800/jsonrpc'",
+			Type:        "string",
+			Access:      model.PRIVATE,
+			Group:       model.BACK,
+		},
+		{
+			Key:         "Aria2 RPC secret",
+			Value:       "",
+			Description: "Aria2 RPC secret, e.g. '123456'",
+			Type:        "string",
+			Access:      model.PRIVATE,
+			Group:       model.BACK,
+		},
 	}
 	for i, _ := range settings {
 		v := settings[i]
diff --git a/drivers/123/123.go b/drivers/123/123.go
index 5d8ae7dcb24..e93f144baf6 100644
--- a/drivers/123/123.go
+++ b/drivers/123/123.go
@@ -3,7 +3,6 @@ package _23
 import (
 	"errors"
 	"fmt"
-	"github.com/Xhofe/alist/conf"
 	"github.com/Xhofe/alist/drivers/base"
 	"github.com/Xhofe/alist/model"
 	"github.com/Xhofe/alist/utils"
@@ -40,7 +39,7 @@ func (driver Pan123) Login(account *model.Account) error {
 	return err
 }
 
-func (driver Pan123) FormatFile(file *Pan123File) *model.File {
+func (driver Pan123) FormatFile(file *File) *model.File {
 	f := &model.File{
 		Id:        strconv.FormatInt(file.FileId, 10),
 		Name:      file.FileName,
@@ -52,9 +51,9 @@ func (driver Pan123) FormatFile(file *Pan123File) *model.File {
 	return f
 }
 
-func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]Pan123File, error) {
+func (driver Pan123) GetFiles(parentId string, account *model.Account) ([]File, error) {
 	next := "0"
-	res := make([]Pan123File, 0)
+	res := make([]File, 0)
 	for next != "-1" {
 		var resp Pan123Files
 		query := map[string]string{
@@ -139,7 +138,7 @@ func (driver Pan123) Request(url string, method int, headers, query map[string]s
 //	return body, nil
 //}
 
-func (driver Pan123) GetFile(path string, account *model.Account) (*Pan123File, error) {
+func (driver Pan123) GetFile(path string, account *model.Account) (*File, error) {
 	dir, name := filepath.Split(path)
 	dir = utils.ParsePath(dir)
 	_, err := driver.Files(dir, account)
@@ -147,14 +146,15 @@ func (driver Pan123) GetFile(path string, account *model.Account) (*Pan123File,
 		return nil, err
 	}
 	parentFiles_, _ := base.GetCache(dir, account)
-	parentFiles, _ := parentFiles_.([]Pan123File)
+	parentFiles, _ := parentFiles_.([]File)
 	for _, file := range parentFiles {
 		if file.FileName == name {
-			if file.Type != conf.FOLDER {
-				return &file, err
-			} else {
-				return nil, base.ErrNotFile
-			}
+			//if file.Type != conf.FOLDER {
+			//	return &file, err
+			//} else {
+			//	return nil, base.ErrNotFile
+			//}
+			return &file, nil
 		}
 	}
 	return nil, base.ErrPathNotFound
diff --git a/drivers/123/driver.go b/drivers/123/driver.go
index 27ffcefb578..eeb6bebd4e9 100644
--- a/drivers/123/driver.go
+++ b/drivers/123/driver.go
@@ -108,10 +108,10 @@ func (driver Pan123) File(path string, account *model.Account) (*model.File, err
 
 func (driver Pan123) Files(path string, account *model.Account) ([]model.File, error) {
 	path = utils.ParsePath(path)
-	var rawFiles []Pan123File
+	var rawFiles []File
 	cache, err := base.GetCache(path, account)
 	if err == nil {
-		rawFiles, _ = cache.([]Pan123File)
+		rawFiles, _ = cache.([]File)
 	} else {
 		file, err := driver.File(path, account)
 		if err != nil {
@@ -278,12 +278,13 @@ func (driver Pan123) Delete(path string, account *model.Account) error {
 	if err != nil {
 		return err
 	}
+	log.Debugln("delete 123 file: ", file)
 	data := base.Json{
 		"driveId":           0,
 		"operation":         true,
-		"fileTrashInfoList": file,
+		"fileTrashInfoList": []File{*file},
 	}
-	_, err = driver.Request("https://www.123pan.com/api/file/trash",
+	_, err = driver.Request("https://www.123pan.com/b/api/file/trash",
 		base.Post, nil, nil, &data, nil, false, account)
 	return err
 }
diff --git a/drivers/123/types.go b/drivers/123/types.go
index 82d66223e4d..012d350eb72 100644
--- a/drivers/123/types.go
+++ b/drivers/123/types.go
@@ -7,7 +7,7 @@ import (
 	"time"
 )
 
-type Pan123File struct {
+type File struct {
 	FileName  string     `json:"FileName"`
 	Size      int64      `json:"Size"`
 	UpdateAt  *time.Time `json:"UpdateAt"`
@@ -17,15 +17,15 @@ type Pan123File struct {
 	S3KeyFlag string     `json:"S3KeyFlag"`
 }
 
-func (f Pan123File) GetSize() uint64 {
+func (f File) GetSize() uint64 {
 	return uint64(f.Size)
 }
 
-func (f Pan123File) GetName() string {
+func (f File) GetName() string {
 	return f.FileName
 }
 
-func (f Pan123File) GetType() int {
+func (f File) GetType() int {
 	if f.Type == 1 {
 		return conf.FOLDER
 	}
@@ -47,8 +47,8 @@ type Pan123TokenResp struct {
 type Pan123Files struct {
 	BaseResp
 	Data struct {
-		InfoList []Pan123File `json:"InfoList"`
-		Next     string       `json:"Next"`
+		InfoList []File `json:"InfoList"`
+		Next     string `json:"Next"`
 	} `json:"data"`
 }
 
diff --git a/drivers/all.go b/drivers/all.go
index b9275931071..cf0fb8e5932 100644
--- a/drivers/all.go
+++ b/drivers/all.go
@@ -25,6 +25,7 @@ import (
 	_ "github.com/Xhofe/alist/drivers/webdav"
 	_ "github.com/Xhofe/alist/drivers/xunlei"
 	_ "github.com/Xhofe/alist/drivers/yandex"
+	_ "github.com/Xhofe/alist/drivers/baiduphoto"
 	log "github.com/sirupsen/logrus"
 	"strings"
 )
diff --git a/drivers/baiduphoto/baidu.go b/drivers/baiduphoto/baidu.go
new file mode 100644
index 00000000000..287ec433ef6
--- /dev/null
+++ b/drivers/baiduphoto/baidu.go
@@ -0,0 +1,259 @@
+package baiduphoto
+
+import (
+	"fmt"
+	"net/http"
+
+	"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"
+)
+
+func (driver Baidu) RefreshToken(account *model.Account) error {
+	err := driver.refreshToken(account)
+	if err != nil && err == base.ErrEmptyToken {
+		err = driver.refreshToken(account)
+	}
+	if err != nil {
+		account.Status = err.Error()
+	}
+	_ = model.SaveAccount(account)
+	return err
+}
+
+func (driver Baidu) refreshToken(account *model.Account) error {
+	u := "https://openapi.baidu.com/oauth/2.0/token"
+	var resp base.TokenResp
+	var e TokenErrResp
+	_, err := base.RestyClient.R().
+		SetResult(&resp).
+		SetError(&e).
+		SetQueryParams(map[string]string{
+			"grant_type":    "refresh_token",
+			"refresh_token": account.RefreshToken,
+			"client_id":     account.ClientId,
+			"client_secret": account.ClientSecret,
+		}).Get(u)
+	if err != nil {
+		return err
+	}
+	if e.ErrorMsg != "" {
+		return &e
+	}
+	if resp.RefreshToken == "" {
+		return base.ErrEmptyToken
+	}
+	account.Status = "work"
+	account.AccessToken, account.RefreshToken = resp.AccessToken, resp.RefreshToken
+	return nil
+}
+
+func (driver Baidu) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
+	req := base.RestyClient.R()
+	req.SetQueryParam("access_token", account.AccessToken)
+	if callback != nil {
+		callback(req)
+	}
+
+	res, err := req.Execute(method, url)
+	if err != nil {
+		return nil, err
+	}
+	log.Debug(res.String())
+
+	var erron Erron
+	if err = utils.Json.Unmarshal(res.Body(), &erron); err != nil {
+		return nil, err
+	}
+
+	switch erron.Errno {
+	case 0:
+		return res, nil
+	case -6:
+		if err = driver.RefreshToken(account); err != nil {
+			return nil, err
+		}
+	default:
+		return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron.Errno)
+	}
+	return driver.Request(method, url, callback, account)
+}
+
+// 获取所有根文件
+func (driver Baidu) GetAllFile(account *model.Account) (files []File, err error) {
+	var cursor string
+
+	for {
+		var resp FileListResp
+		_, err = driver.Request(http.MethodGet, FILE_API_URL+"/list", func(r *resty.Request) {
+			r.SetQueryParams(map[string]string{
+				"need_thumbnail":     "1",
+				"need_filter_hidden": "0",
+				"cursor":             cursor,
+			})
+			r.SetResult(&resp)
+		}, account)
+		if err != nil {
+			return
+		}
+
+		cursor = resp.Cursor
+		files = append(files, resp.List...)
+
+		if !resp.HasNextPage() {
+			return
+		}
+	}
+}
+
+// 获取所有相册
+func (driver Baidu) GetAllAlbum(account *model.Account) (albums []Album, err error) {
+	var cursor string
+	for {
+		var resp AlbumListResp
+		_, err = driver.Request(http.MethodGet, ALBUM_API_URL+"/list", func(r *resty.Request) {
+			r.SetQueryParams(map[string]string{
+				"need_amount": "1",
+				"limit":       "100",
+				"cursor":      cursor,
+			})
+			r.SetResult(&resp)
+		}, account)
+		if err != nil {
+			return
+		}
+		if albums == nil {
+			albums = make([]Album, 0, resp.TotalCount)
+		}
+
+		cursor = resp.Cursor
+		albums = append(albums, resp.List...)
+
+		if !resp.HasNextPage() {
+			return
+		}
+	}
+}
+
+// 获取相册中所有文件
+func (driver Baidu) GetAllAlbumFile(albumID string, account *model.Account) (files []AlbumFile, err error) {
+	var cursor string
+	for {
+		var resp AlbumFileListResp
+		_, err = driver.Request(http.MethodGet, ALBUM_API_URL+"/listfile", func(r *resty.Request) {
+			r.SetQueryParams(map[string]string{
+				"album_id":    splitID(albumID)[0],
+				"need_amount": "1",
+				"limit":       "1000",
+				"cursor":      cursor,
+			})
+			r.SetResult(&resp)
+		}, account)
+		if err != nil {
+			return
+		}
+		if files == nil {
+			files = make([]AlbumFile, 0, resp.TotalCount)
+		}
+
+		cursor = resp.Cursor
+		files = append(files, resp.List...)
+
+		if !resp.HasNextPage() {
+			return
+		}
+	}
+}
+
+// 创建相册
+func (driver Baidu) CreateAlbum(name string, account *model.Account) error {
+	if !checkName(name) {
+		return ErrNotSupportName
+	}
+	_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/create", func(r *resty.Request) {
+		r.SetQueryParams(map[string]string{
+			"title":  name,
+			"tid":    getTid(),
+			"source": "0",
+		})
+	}, account)
+	return err
+}
+
+// 相册改名
+func (driver Baidu) SetAlbumName(albumID string, name string, account *model.Account) error {
+	if !checkName(name) {
+		return ErrNotSupportName
+	}
+
+	e := splitID(albumID)
+	_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/settitle", func(r *resty.Request) {
+		r.SetFormData(map[string]string{
+			"title":    name,
+			"album_id": e[0],
+			"tid":      e[1],
+		})
+	}, account)
+	return err
+}
+
+// 删除相册
+func (driver Baidu) DeleteAlbum(albumID string, account *model.Account) error {
+	e := splitID(albumID)
+	_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/delete", func(r *resty.Request) {
+		r.SetFormData(map[string]string{
+			"album_id":            e[0],
+			"tid":                 e[1],
+			"delete_origin_image": "0", // 是否删除原图 0 不删除
+		})
+	}, account)
+	return err
+}
+
+// 删除相册文件
+func (driver Baidu) DeleteAlbumFile(albumID string, account *model.Account, fileIDs ...string) error {
+	e := splitID(albumID)
+	_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/delfile", func(r *resty.Request) {
+		r.SetFormData(map[string]string{
+			"album_id":   e[0],
+			"tid":        e[1],
+			"list":       fsidsFormat(fileIDs...),
+			"del_origin": "0", // 是否删除原图 0 不删除 1 删除
+		})
+	}, account)
+	return err
+}
+
+// 增加相册文件
+func (driver Baidu) AddAlbumFile(albumID string, account *model.Account, fileIDs ...string) error {
+	e := splitID(albumID)
+	_, err := driver.Request(http.MethodGet, ALBUM_API_URL+"/addfile", func(r *resty.Request) {
+		r.SetQueryParams(map[string]string{
+			"album_id": e[0],
+			"tid":      e[1],
+			"list":     fsidsFormatNotUk(fileIDs...),
+		})
+	}, account)
+	return err
+}
+
+// 保存相册文件为根文件
+func (driver Baidu) CopyAlbumFile(albumID string, account *model.Account, fileID string) (*CopyFile, error) {
+	var resp CopyFileResp
+	e := splitID(fileID)
+	_, err := driver.Request(http.MethodPost, ALBUM_API_URL+"/copyfile", func(r *resty.Request) {
+		r.SetFormData(map[string]string{
+			"album_id": splitID(albumID)[0],
+			"tid":      e[2],
+			"uk":       e[1],
+			"list":     fsidsFormatNotUk(fileID),
+		})
+		r.SetResult(&resp)
+	}, account)
+	if err != nil {
+		return nil, err
+	}
+	return &resp.List[0], err
+}
diff --git a/drivers/baiduphoto/driver.go b/drivers/baiduphoto/driver.go
new file mode 100644
index 00000000000..a502eefeafb
--- /dev/null
+++ b/drivers/baiduphoto/driver.go
@@ -0,0 +1,452 @@
+package baiduphoto
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"math"
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"github.com/Xhofe/alist/conf"
+	"github.com/Xhofe/alist/drivers/base"
+	"github.com/Xhofe/alist/model"
+	"github.com/Xhofe/alist/utils"
+	"github.com/go-resty/resty/v2"
+)
+
+type Baidu struct{}
+
+func init() {
+	base.RegisterDriver(new(Baidu))
+}
+
+func (driver Baidu) Config() base.DriverConfig {
+	return base.DriverConfig{
+		Name:      "Baidu.Photo",
+		LocalSort: true,
+	}
+}
+
+func (driver Baidu) Items() []base.Item {
+	return []base.Item{
+		{
+			Name:     "refresh_token",
+			Label:    "refresh token",
+			Type:     base.TypeString,
+			Required: true,
+		},
+		{
+			Name:  "root_folder",
+			Label: "album_id",
+			Type:  base.TypeString,
+		},
+		{
+			Name:     "client_id",
+			Label:    "client id",
+			Default:  "iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v",
+			Type:     base.TypeString,
+			Required: true,
+		},
+		{
+			Name:     "client_secret",
+			Label:    "client secret",
+			Default:  "jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG",
+			Type:     base.TypeString,
+			Required: true,
+		},
+	}
+}
+
+func (driver Baidu) Save(account *model.Account, old *model.Account) error {
+	if account == nil {
+		return nil
+	}
+	return driver.RefreshToken(account)
+}
+
+func (driver Baidu) 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 Baidu) Files(path string, account *model.Account) ([]model.File, error) {
+	path = utils.ParsePath(path)
+	var files []model.File
+	cache, err := base.GetCache(path, account)
+	if err == nil {
+		files, _ = cache.([]model.File)
+		return files, nil
+	}
+
+	file, err := driver.File(path, account)
+	if err != nil {
+		return nil, err
+	}
+
+	if IsAlbum(file) {
+		albumFiles, err := driver.GetAllAlbumFile(file.Id, account)
+		if err != nil {
+			return nil, err
+		}
+		files = make([]model.File, 0, len(albumFiles))
+		for _, file := range albumFiles {
+			var thumbnail string
+			if len(file.Thumburl) > 0 {
+				thumbnail = file.Thumburl[0]
+			}
+			files = append(files, model.File{
+				Id:        joinID(file.Fsid, file.Uk, file.Tid),
+				Name:      file.Name(),
+				Size:      file.Size,
+				Type:      utils.GetFileType(filepath.Ext(file.Path)),
+				Driver:    driver.Config().Name,
+				UpdatedAt: getTime(file.Mtime),
+				Thumbnail: thumbnail,
+			})
+		}
+	} else if IsRoot(file) {
+		albums, err := driver.GetAllAlbum(account)
+		if err != nil {
+			return nil, err
+		}
+
+		files = make([]model.File, 0, len(albums))
+		for _, album := range albums {
+			files = append(files, model.File{
+				Id:        joinID(album.AlbumID, album.Tid),
+				Name:      album.Title,
+				Size:      0,
+				Type:      conf.FOLDER,
+				Driver:    driver.Config().Name,
+				UpdatedAt: getTime(album.Mtime),
+			})
+		}
+	} else {
+		return nil, base.ErrNotSupport
+	}
+
+	if len(files) > 0 {
+		_ = base.SetCache(path, files, account)
+	}
+	return files, nil
+}
+
+func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) {
+	file, err := driver.File(args.Path, account)
+	if err != nil {
+		return nil, err
+	}
+	if !IsAlbumFile(file) {
+		return nil, base.ErrNotSupport
+	}
+
+	album, err := driver.File(filepath.Dir(utils.ParsePath(args.Path)), account)
+	if err != nil {
+		return nil, err
+	}
+
+	e := splitID(file.Id)
+	res, err := base.NoRedirectClient.R().
+		SetQueryParams(map[string]string{
+			"access_token": account.AccessToken,
+			"album_id":     splitID(album.Id)[0],
+			"tid":          e[2],
+			"fsid":         e[0],
+			"uk":           e[1],
+		}).
+		Head(ALBUM_API_URL + "/download")
+	if err != nil {
+		return nil, err
+	}
+	return &base.Link{
+		Headers: []base.Header{
+			{Name: "User-Agent", Value: base.UserAgent},
+		},
+		Url: res.Header().Get("location"),
+	}, nil
+}
+
+func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) {
+	path = utils.ParsePath(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 Baidu) Preview(path string, account *model.Account) (interface{}, error) {
+	return nil, base.ErrNotSupport
+}
+
+func (driver Baidu) Rename(src string, dst string, account *model.Account) error {
+	srcFile, err := driver.File(src, account)
+	if err != nil {
+		return err
+	}
+
+	if IsAlbum(srcFile) {
+		return driver.SetAlbumName(srcFile.Id, filepath.Base(dst), account)
+	}
+	return base.ErrNotSupport
+}
+
+func (driver Baidu) MakeDir(path string, account *model.Account) error {
+	dir, name := filepath.Split(path)
+	parentFile, err := driver.File(dir, account)
+	if err != nil {
+		return err
+	}
+
+	if !IsRoot(parentFile) {
+		return base.ErrNotSupport
+	}
+	return driver.CreateAlbum(name, account)
+}
+
+func (driver Baidu) Move(src string, dst string, account *model.Account) error {
+	srcFile, err := driver.File(src, account)
+	if err != nil {
+		return err
+	}
+
+	if IsAlbumFile(srcFile) {
+		// 移动相册文件
+		dstAlbum, err := driver.File(filepath.Dir(dst), account)
+		if err != nil {
+			return err
+		}
+		if !IsAlbum(dstAlbum) {
+			return base.ErrNotSupport
+		}
+
+		srcAlbum, err := driver.File(filepath.Dir(src), account)
+		if err != nil {
+			return err
+		}
+
+		newFile, err := driver.CopyAlbumFile(srcAlbum.Id, account, srcFile.Id)
+		if err != nil {
+			return err
+		}
+		err = driver.DeleteAlbumFile(srcAlbum.Id, account, srcFile.Id)
+		if err != nil {
+			return err
+		}
+		err = driver.AddAlbumFile(dstAlbum.Id, account, joinID(newFile.Fsid))
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+	return base.ErrNotSupport
+}
+
+func (driver Baidu) Copy(src string, dst string, account *model.Account) error {
+	srcFile, err := driver.File(src, account)
+	if err != nil {
+		return err
+	}
+
+	if IsAlbumFile(srcFile) {
+		// 复制相册文件
+		dstAlbum, err := driver.File(filepath.Dir(dst), account)
+		if err != nil {
+			return err
+		}
+		if !IsAlbum(dstAlbum) {
+			return base.ErrNotSupport
+		}
+
+		srcAlbum, err := driver.File(filepath.Dir(src), account)
+		if err != nil {
+			return err
+		}
+
+		newFile, err := driver.CopyAlbumFile(srcAlbum.Id, account, srcFile.Id)
+		if err != nil {
+			return err
+		}
+		err = driver.AddAlbumFile(dstAlbum.Id, account, joinID(newFile.Fsid))
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+	return base.ErrNotSupport
+}
+
+func (driver Baidu) Delete(path string, account *model.Account) error {
+	file, err := driver.File(path, account)
+	if err != nil {
+		return err
+	}
+
+	// 删除相册
+	if IsAlbum(file) {
+		return driver.DeleteAlbum(file.Id, account)
+	}
+
+	// 生成相册文件
+	if IsAlbumFile(file) {
+		// 删除相册文件
+		album, err := driver.File(filepath.Dir(path), account)
+		if err != nil {
+			return err
+		}
+		return driver.DeleteAlbumFile(album.Id, account, file.Id)
+	}
+	return base.ErrNotSupport
+}
+
+func (driver Baidu) 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
+	}
+
+	if !IsAlbum(parentFile) {
+		return base.ErrNotSupport
+	}
+
+	tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*")
+	if err != nil {
+		return err
+	}
+	defer func() {
+		tempFile.Close()
+		os.Remove(tempFile.Name())
+	}()
+
+	// 计算需要的数据
+	const DEFAULT = 1 << 22
+	const SliceSize = 1 << 18
+	count := int(math.Ceil(float64(file.Size) / float64(DEFAULT)))
+
+	sliceMD5List := make([]string, 0, count)
+	fileMd5 := md5.New()
+	sliceMd5 := md5.New()
+	for i := 1; i <= count; i++ {
+		if n, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, tempFile), file, DEFAULT); err != io.EOF && n == 0 {
+			return err
+		}
+		sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil)))
+		sliceMd5.Reset()
+	}
+
+	if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
+		return err
+	}
+
+	content_md5 := hex.EncodeToString(fileMd5.Sum(nil))
+	slice_md5 := content_md5
+	if file.GetSize() > SliceSize {
+		sliceData := make([]byte, SliceSize)
+		if _, err = io.ReadFull(tempFile, sliceData); err != nil {
+			return err
+		}
+		sliceMd5.Write(sliceData)
+		slice_md5 = hex.EncodeToString(sliceMd5.Sum(nil))
+		if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
+			return err
+		}
+	}
+
+	// 开始执行上传
+	params := map[string]string{
+		"autoinit":    "1",
+		"isdir":       "0",
+		"rtype":       "1",
+		"ctype":       "11",
+		"path":        utils.ParsePath(file.Name),
+		"size":        fmt.Sprint(file.Size),
+		"slice-md5":   slice_md5,
+		"content-md5": content_md5,
+		"block_list":  MustString(utils.Json.MarshalToString(sliceMD5List)),
+	}
+
+	// 预上传
+	var precreateResp PrecreateResp
+	_, err = driver.Request(http.MethodPost, FILE_API_URL+"/precreate", func(r *resty.Request) {
+		r.SetFormData(params)
+		r.SetResult(&precreateResp)
+	}, account)
+	if err != nil {
+		return err
+	}
+
+	switch precreateResp.ReturnType {
+	case 1: // 上传文件
+		uploadParams := map[string]string{
+			"method":   "upload",
+			"path":     params["path"],
+			"uploadid": precreateResp.UploadID,
+		}
+
+		for i := 0; i < count; i++ {
+			uploadParams["partseq"] = fmt.Sprint(i)
+			_, err = driver.Request(http.MethodPost, "https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
+				r.SetQueryParams(uploadParams)
+				r.SetFileReader("file", file.Name, io.LimitReader(tempFile, DEFAULT))
+			}, account)
+			if err != nil {
+				return err
+			}
+		}
+		fallthrough
+	case 2: // 创建文件
+		params["uploadid"] = precreateResp.UploadID
+		_, err = driver.Request(http.MethodPost, FILE_API_URL+"/create", func(r *resty.Request) {
+			r.SetFormData(params)
+			r.SetResult(&precreateResp)
+		}, account)
+		if err != nil {
+			return err
+		}
+		fallthrough
+	case 3: // 增加到相册
+		err = driver.AddAlbumFile(parentFile.Id, account, joinID(precreateResp.Data.FsID))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+var _ base.Driver = (*Baidu)(nil)
diff --git a/drivers/baiduphoto/types.go b/drivers/baiduphoto/types.go
new file mode 100644
index 00000000000..9bc954b0376
--- /dev/null
+++ b/drivers/baiduphoto/types.go
@@ -0,0 +1,125 @@
+package baiduphoto
+
+import (
+	"fmt"
+	"path/filepath"
+)
+
+type TokenErrResp struct {
+	ErrorDescription string `json:"error_description"`
+	ErrorMsg         string `json:"error"`
+}
+
+func (e *TokenErrResp) Error() string {
+	return fmt.Sprint(e.ErrorMsg, " : ", e.ErrorDescription)
+}
+
+type Erron struct {
+	Errno     int `json:"errno"`
+	RequestID int `json:"request_id"`
+}
+
+type Page struct {
+	HasMore int    `json:"has_more"`
+	Cursor  string `json:"cursor"`
+}
+
+func (p Page) HasNextPage() bool {
+	return p.HasMore == 1
+}
+
+type (
+	FileListResp struct {
+		Page
+		List []File `json:"list"`
+	}
+
+	File struct {
+		Fsid     int64    `json:"fsid"` // 文件ID
+		Path     string   `json:"path"` // 文件路径
+		Size     int64    `json:"size"`
+		Ctime    int64    `json:"ctime"` // 创建时间 s
+		Mtime    int64    `json:"mtime"` // 修改时间 s
+		Thumburl []string `json:"thumburl"`
+	}
+)
+
+func (f File) Name() string {
+	return filepath.Base(f.Path)
+}
+
+/*相册部分*/
+type (
+	AlbumListResp struct {
+		Page
+		List       []Album `json:"list"`
+		Reset      int64   `json:"reset"`
+		TotalCount int64   `json:"total_count"`
+	}
+
+	Album struct {
+		AlbumID    string `json:"album_id"`
+		Tid        int64  `json:"tid"`
+		Title      string `json:"title"`
+		JoinTime   int64  `json:"join_time"`
+		CreateTime int64  `json:"create_time"`
+		Mtime      int64  `json:"mtime"`
+	}
+
+	AlbumFileListResp struct {
+		Page
+		List       []AlbumFile `json:"list"`
+		Reset      int64       `json:"reset"`
+		TotalCount int64       `json:"total_count"`
+	}
+
+	AlbumFile struct {
+		File
+		Tid int64 `json:"tid"`
+		Uk  int64 `json:"uk"`
+	}
+)
+
+type (
+	CopyFileResp struct {
+		List []CopyFile `json:"list"`
+	}
+	CopyFile struct {
+		FromFsid  int64  `json:"from_fsid"` // 源ID
+		Fsid      int64  `json:"fsid"`      // 目标ID
+		Path      string `json:"path"`
+		ShootTime int    `json:"shoot_time"`
+	}
+)
+
+/*上传部分*/
+type (
+	UploadFile struct {
+		FsID           int64  `json:"fs_id"`
+		Size           int    `json:"size"`
+		Md5            string `json:"md5"`
+		ServerFilename string `json:"server_filename"`
+		Path           string `json:"path"`
+		Ctime          int    `json:"ctime"`
+		Mtime          int    `json:"mtime"`
+		Isdir          int    `json:"isdir"`
+		Category       int    `json:"category"`
+		ServerMd5      string `json:"server_md5"`
+		ShootTime      int    `json:"shoot_time"`
+	}
+
+	CreateFileResp struct {
+		Data UploadFile `json:"data"`
+	}
+
+	PrecreateResp struct {
+		ReturnType int `json:"return_type"` //存在返回2 不存在返回1 已经保存3
+		//存在返回
+		CreateFileResp
+
+		//不存在返回
+		Path      string  `json:"path"`
+		UploadID  string  `json:"uploadid"`
+		Blocklist []int64 `json:"block_list"`
+	}
+)
diff --git a/drivers/baiduphoto/util.go b/drivers/baiduphoto/util.go
new file mode 100644
index 00000000000..5ab40a52c85
--- /dev/null
+++ b/drivers/baiduphoto/util.go
@@ -0,0 +1,83 @@
+package baiduphoto
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"math/rand"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/Xhofe/alist/model"
+)
+
+const (
+	API_URL       = "https://photo.baidu.com/youai"
+	ALBUM_API_URL = API_URL + "/album/v1"
+	FILE_API_URL  = API_URL + "/file/v1"
+)
+
+var (
+	ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20")
+)
+
+//Tid生成
+func getTid() string {
+	return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))
+}
+
+// 检查名称
+func checkName(name string) bool {
+	return len(name) <= 20 && regexp.MustCompile("[\u4e00-\u9fa5A-Za-z0-9_]").MatchString(name)
+}
+
+func getTime(t int64) *time.Time {
+	tm := time.Unix(t, 0)
+	return &tm
+}
+
+func fsidsFormat(ids ...string) string {
+	var buf []string
+	for _, id := range ids {
+		e := strings.Split(id, "|")
+		buf = append(buf, fmt.Sprintf("{\"fsid\":%s,\"uk\":%s}", e[0], e[1]))
+	}
+	return fmt.Sprintf("[%s]", strings.Join(buf, ","))
+}
+
+func fsidsFormatNotUk(ids ...string) string {
+	var buf []string
+	for _, id := range ids {
+		buf = append(buf, fmt.Sprintf("{\"fsid\":%s}", strings.Split(id, "|")[0]))
+	}
+	return fmt.Sprintf("[%s]", strings.Join(buf, ","))
+}
+
+func splitID(id string) []string {
+	return strings.SplitN(id, "|", 3)[:3]
+}
+
+func joinID(ids ...interface{}) string {
+	idsStr := make([]string, 0, len(ids))
+	for _, id := range ids {
+		idsStr = append(idsStr, fmt.Sprint(id))
+	}
+	return strings.Join(idsStr, "|")
+}
+
+func IsAlbum(file *model.File) bool {
+	return file.Id != "" && file.IsDir()
+}
+
+func IsAlbumFile(file *model.File) bool {
+	return file.Id != "" && !file.IsDir()
+}
+
+func IsRoot(file *model.File) bool {
+	return file.Id == "" && file.IsDir()
+}
+
+func MustString(str string, err error) string {
+	return str
+}
diff --git a/drivers/base/base.go b/drivers/base/base.go
new file mode 100644
index 00000000000..68027f41595
--- /dev/null
+++ b/drivers/base/base.go
@@ -0,0 +1,61 @@
+package base
+
+import "github.com/Xhofe/alist/model"
+
+type Base struct{}
+
+func (b Base) Config() DriverConfig {
+	return DriverConfig{}
+}
+
+func (b Base) Items() []Item {
+	return nil
+}
+
+func (b Base) Save(account *model.Account, old *model.Account) error {
+	return ErrNotImplement
+}
+
+func (b Base) File(path string, account *model.Account) (*model.File, error) {
+	return nil, ErrNotImplement
+}
+
+func (b Base) Files(path string, account *model.Account) ([]model.File, error) {
+	return nil, ErrNotImplement
+}
+
+func (b Base) Link(args Args, account *model.Account) (*Link, error) {
+	return nil, ErrNotImplement
+}
+
+func (b Base) Path(path string, account *model.Account) (*model.File, []model.File, error) {
+	return nil, nil, ErrNotImplement
+}
+
+func (b Base) Preview(path string, account *model.Account) (interface{}, error) {
+	return nil, ErrNotImplement
+}
+
+func (b Base) MakeDir(path string, account *model.Account) error {
+	return ErrNotImplement
+}
+
+func (b Base) Move(src string, dst string, account *model.Account) error {
+	return ErrNotImplement
+}
+
+func (b Base) Rename(src string, dst string, account *model.Account) error {
+	return ErrNotImplement
+}
+
+func (b Base) Copy(src string, dst string, account *model.Account) error {
+	return ErrNotImplement
+}
+
+func (b Base) Delete(path string, account *model.Account) error {
+	return ErrNotImplement
+}
+
+func (b Base) Upload(file *model.FileStream, account *model.Account) error {
+	return ErrNotImplement
+}
diff --git a/drivers/quark/types.go b/drivers/quark/types.go
index ebe77b3004a..3a1f61ec5cc 100644
--- a/drivers/quark/types.go
+++ b/drivers/quark/types.go
@@ -51,37 +51,37 @@ type SortResp struct {
 type DownResp struct {
 	Resp
 	Data []struct {
-		Fid          string `json:"fid"`
-		FileName     string `json:"file_name"`
-		PdirFid      string `json:"pdir_fid"`
-		Category     int    `json:"category"`
-		FileType     int    `json:"file_type"`
-		Size         int    `json:"size"`
-		FormatType   string `json:"format_type"`
-		Status       int    `json:"status"`
-		Tags         string `json:"tags"`
-		LCreatedAt   int64  `json:"l_created_at"`
-		LUpdatedAt   int64  `json:"l_updated_at"`
-		NameSpace    int    `json:"name_space"`
-		Thumbnail    string `json:"thumbnail"`
-		DownloadUrl  string `json:"download_url"`
-		Md5          string `json:"md5"`
-		RiskType     int    `json:"risk_type"`
-		RangeSize    int    `json:"range_size"`
-		BackupSign   int    `json:"backup_sign"`
-		ObjCategory  string `json:"obj_category"`
-		Duration     int    `json:"duration"`
-		FileSource   string `json:"file_source"`
-		File         bool   `json:"file"`
-		CreatedAt    int64  `json:"created_at"`
-		UpdatedAt    int64  `json:"updated_at"`
-		PrivateExtra struct {
-		} `json:"_private_extra"`
+		//Fid          string `json:"fid"`
+		//FileName     string `json:"file_name"`
+		//PdirFid      string `json:"pdir_fid"`
+		//Category     int    `json:"category"`
+		//FileType     int    `json:"file_type"`
+		//Size         int    `json:"size"`
+		//FormatType   string `json:"format_type"`
+		//Status       int    `json:"status"`
+		//Tags         string `json:"tags"`
+		//LCreatedAt   int64  `json:"l_created_at"`
+		//LUpdatedAt   int64  `json:"l_updated_at"`
+		//NameSpace    int    `json:"name_space"`
+		//Thumbnail    string `json:"thumbnail"`
+		DownloadUrl string `json:"download_url"`
+		//Md5          string `json:"md5"`
+		//RiskType     int    `json:"risk_type"`
+		//RangeSize    int    `json:"range_size"`
+		//BackupSign   int    `json:"backup_sign"`
+		//ObjCategory  string `json:"obj_category"`
+		//Duration     int    `json:"duration"`
+		//FileSource   string `json:"file_source"`
+		//File         bool   `json:"file"`
+		//CreatedAt    int64  `json:"created_at"`
+		//UpdatedAt    int64  `json:"updated_at"`
+		//PrivateExtra struct {
+		//} `json:"_private_extra"`
 	} `json:"data"`
-	Metadata struct {
-		Acc2 string `json:"acc2"`
-		Acc1 string `json:"acc1"`
-	} `json:"metadata"`
+	//Metadata struct {
+	//	Acc2 string `json:"acc2"`
+	//	Acc1 string `json:"acc1"`
+	//} `json:"metadata"`
 }
 
 type UpPreResp struct {
diff --git a/drivers/template/driver.go b/drivers/template/driver.go
index a597702ad91..d77fe7a19d7 100644
--- a/drivers/template/driver.go
+++ b/drivers/template/driver.go
@@ -9,6 +9,7 @@ import (
 )
 
 type Template struct {
+	base.Base
 }
 
 func (driver Template) Config() base.DriverConfig {
@@ -111,39 +112,40 @@ func (driver Template) Path(path string, account *model.Account) (*model.File, [
 	return nil, files, nil
 }
 
-func (driver Template) Preview(path string, account *model.Account) (interface{}, error) {
-	//TODO preview interface if driver support
-	return nil, base.ErrNotImplement
-}
-
-func (driver Template) MakeDir(path string, account *model.Account) error {
-	//TODO make dir
-	return base.ErrNotImplement
-}
-
-func (driver Template) Move(src string, dst string, account *model.Account) error {
-	//TODO move file/dir
-	return base.ErrNotImplement
-}
-
-func (driver Template) Rename(src string, dst string, account *model.Account) error {
-	//TODO rename file/dir
-	return base.ErrNotImplement
-}
-
-func (driver Template) Copy(src string, dst string, account *model.Account) error {
-	//TODO copy file/dir
-	return base.ErrNotImplement
-}
-
-func (driver Template) Delete(path string, account *model.Account) error {
-	//TODO delete file/dir
-	return base.ErrNotImplement
-}
-
-func (driver Template) Upload(file *model.FileStream, account *model.Account) error {
-	//TODO upload file
-	return base.ErrNotImplement
-}
+// Optional function
+//func (driver Template) Preview(path string, account *model.Account) (interface{}, error) {
+//	//TODO preview interface if driver support
+//	return nil, base.ErrNotImplement
+//}
+//
+//func (driver Template) MakeDir(path string, account *model.Account) error {
+//	//TODO make dir
+//	return base.ErrNotImplement
+//}
+//
+//func (driver Template) Move(src string, dst string, account *model.Account) error {
+//	//TODO move file/dir
+//	return base.ErrNotImplement
+//}
+//
+//func (driver Template) Rename(src string, dst string, account *model.Account) error {
+//	//TODO rename file/dir
+//	return base.ErrNotImplement
+//}
+//
+//func (driver Template) Copy(src string, dst string, account *model.Account) error {
+//	//TODO copy file/dir
+//	return base.ErrNotImplement
+//}
+//
+//func (driver Template) Delete(path string, account *model.Account) error {
+//	//TODO delete file/dir
+//	return base.ErrNotImplement
+//}
+//
+//func (driver Template) Upload(file *model.FileStream, account *model.Account) error {
+//	//TODO upload file
+//	return base.ErrNotImplement
+//}
 
 var _ base.Driver = (*Template)(nil)
diff --git a/drivers/xunlei/driver.go b/drivers/xunlei/driver.go
index 6d39ad226d1..bd2dddbc3c2 100644
--- a/drivers/xunlei/driver.go
+++ b/drivers/xunlei/driver.go
@@ -7,15 +7,16 @@ import (
 	"os"
 	"path/filepath"
 	"strconv"
-
-	"github.com/aliyun/aliyun-oss-go-sdk/oss"
-	"github.com/go-resty/resty/v2"
+	"strings"
+	"time"
 
 	"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"
+	"github.com/aliyun/aliyun-oss-go-sdk/oss"
+	"github.com/go-resty/resty/v2"
+	"github.com/google/uuid"
 )
 
 type XunLeiCloud struct{}
@@ -48,11 +49,64 @@ func (driver XunLeiCloud) Items() []base.Item {
 			Description: "account password",
 		},
 		{
-			Name:     "root_folder",
-			Label:    "root folder file_id",
+			Name:  "captcha_token",
+			Label: "verified captcha token",
+			Type:  base.TypeString,
+		},
+		{
+			Name:  "root_folder",
+			Label: "root folder file_id",
+			Type:  base.TypeString,
+		},
+		{
+			Name:     "client_version",
+			Label:    "client version",
+			Default:  "7.43.0.7998",
 			Type:     base.TypeString,
 			Required: true,
 		},
+		{
+			Name:     "client_id",
+			Label:    "client id",
+			Default:  "Xp6vsxz_7IYVw2BB",
+			Type:     base.TypeString,
+			Required: true,
+		},
+		{
+			Name:     "client_secret",
+			Label:    "client secret",
+			Default:  "Xp6vsy4tN9toTVdMSpomVdXpRmES",
+			Type:     base.TypeString,
+			Required: true,
+		},
+		{
+			Name:     "algorithms",
+			Label:    "algorithms",
+			Default:  "hrVPGbeqYPs+CIscj05VpAtjalzY5yjpvlMS8bEo,DrI0uTP,HHK0VXyMgY0xk2K0o,BBaXsExvL3GadmIacjWv7ISUJp3ifAwqbJumu,5toJ7ejB+bh1,5LsZTFAFjgvFvIl1URBgOAJ,QcJ5Ry+,hYgZVz8r7REROaCYfd9,zw6gXgkk/8TtGrmx6EGfekPESLnbZfDFwqR,gtSwLnMBa8h12nF3DU6+LwEQPHxd,fMG8TvtAYbCkxuEbIm0Xi/Lb7Z",
+			Type:     base.TypeString,
+			Required: true,
+		},
+		{
+			Name:     "package_name",
+			Label:    "package name",
+			Default:  "com.xunlei.downloadprovider",
+			Type:     base.TypeString,
+			Required: true,
+		},
+		{
+			Name:     "user_agent",
+			Label:    "user agent",
+			Default:  "ANDROID-com.xunlei.downloadprovider/7.43.0.7998 netWorkType/WIFI appid/40 deviceName/Samsung_Sm-g9810 deviceModel/SM-G9810 OSVersion/7.1.2 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_0_9+) (JAVA 0)",
+			Type:     base.TypeString,
+			Required: false,
+		},
+		{
+			Name:     "device_id",
+			Label:    "device id",
+			Default:  utils.GetMD5Encode(uuid.NewString()),
+			Type:     base.TypeString,
+			Required: false,
+		},
 	}
 }
 
@@ -60,10 +114,18 @@ func (driver XunLeiCloud) Save(account *model.Account, old *model.Account) error
 	if account == nil {
 		return nil
 	}
-	state := GetState(account)
-	if state.isTokensExpires() {
-		return state.Login(account)
+
+	client := GetClient(account)
+	// 指定验证通过的captchaToken
+	if client.captchaToken != "" {
+		client.captchaToken = account.CaptchaToken
+		account.CaptchaToken = ""
+	}
+
+	if client.token == "" {
+		return client.Login(account)
 	}
+
 	account.Status = "work"
 	model.SaveAccount(account)
 	return nil
@@ -101,17 +163,19 @@ func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.Fi
 		files, _ := cache.([]model.File)
 		return files, nil
 	}
-	file, err := driver.File(path, account)
+
+	parentFile, err := driver.File(path, account)
 	if err != nil {
 		return nil, err
 	}
 
+	time.Sleep(time.Millisecond * 400)
 	files := make([]model.File, 0)
 	for {
 		var fileList FileList
-		_, err = GetState(account).Request("GET", FILE_API_URL, func(r *resty.Request) {
+		_, err = GetClient(account).Request("GET", FILE_API_URL, func(r *resty.Request) {
 			r.SetQueryParams(map[string]string{
-				"parent_id":  file.Id,
+				"parent_id":  parentFile.Id,
 				"page_token": fileList.NextPageToken,
 				"with_audit": "true",
 				"filters":    `{"phase": {"eq": "PHASE_TYPE_COMPLETE"}, "trashed":{"eq":false}}`,
@@ -162,8 +226,9 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li
 		return nil, base.ErrNotFile
 	}
 	var lFile Files
-	_, err = GetState(account).Request("GET", FILE_API_URL+"/{id}", func(r *resty.Request) {
-		r.SetPathParam("id", file.Id)
+	clinet := GetClient(account)
+	_, err = clinet.Request("GET", FILE_API_URL+"/{fileID}", func(r *resty.Request) {
+		r.SetPathParam("fileID", file.Id)
 		r.SetQueryParam("with_audit", "true")
 		r.SetResult(&lFile)
 	}, account)
@@ -172,7 +237,7 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li
 	}
 	return &base.Link{
 		Headers: []base.Header{
-			{Name: "User-Agent", Value: base.UserAgent},
+			{Name: "User-Agent", Value: clinet.userAgent},
 		},
 		Url: lFile.WebContentLink,
 	}, nil
@@ -180,7 +245,6 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li
 
 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
@@ -199,6 +263,18 @@ func (driver XunLeiCloud) Preview(path string, account *model.Account) (interfac
 	return nil, base.ErrNotSupport
 }
 
+func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) error {
+	srcFile, err := driver.File(src, account)
+	if err != nil {
+		return err
+	}
+	_, err = GetClient(account).Request("PATCH", FILE_API_URL+"/{fileID}", func(r *resty.Request) {
+		r.SetPathParam("fileID", srcFile.Id)
+		r.SetBody(&base.Json{"name": filepath.Base(dst)})
+	}, account)
+	return err
+}
+
 func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error {
 	dir, name := filepath.Split(path)
 	parentFile, err := driver.File(dir, account)
@@ -208,7 +284,7 @@ func (driver XunLeiCloud) MakeDir(path string, account *model.Account) error {
 	if !parentFile.IsDir() {
 		return base.ErrNotFolder
 	}
-	_, err = GetState(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
+	_, err = GetClient(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
 		r.SetBody(&base.Json{
 			"kind":      FOLDER,
 			"name":      name,
@@ -229,7 +305,7 @@ func (driver XunLeiCloud) Move(src string, dst string, account *model.Account) e
 		return err
 	}
 
-	_, err = GetState(account).Request("POST", FILE_API_URL+":batchMove", func(r *resty.Request) {
+	_, err = GetClient(account).Request("POST", FILE_API_URL+":batchMove", func(r *resty.Request) {
 		r.SetBody(&base.Json{
 			"to":  base.Json{"parent_id": dstDirFile.Id},
 			"ids": []string{srcFile.Id},
@@ -248,7 +324,7 @@ func (driver XunLeiCloud) Copy(src string, dst string, account *model.Account) e
 	if err != nil {
 		return err
 	}
-	_, err = GetState(account).Request("POST", FILE_API_URL+":batchCopy", func(r *resty.Request) {
+	_, err = GetClient(account).Request("POST", FILE_API_URL+":batchCopy", func(r *resty.Request) {
 		r.SetBody(&base.Json{
 			"to":  base.Json{"parent_id": dstDirFile.Id},
 			"ids": []string{srcFile.Id},
@@ -262,8 +338,8 @@ func (driver XunLeiCloud) Delete(path string, account *model.Account) error {
 	if err != nil {
 		return err
 	}
-	_, err = GetState(account).Request("PATCH", FILE_API_URL+"/{id}/trash", func(r *resty.Request) {
-		r.SetPathParam("id", srcFile.Id)
+	_, err = GetClient(account).Request("PATCH", FILE_API_URL+"/{fileID}/trash", func(r *resty.Request) {
+		r.SetPathParam("fileID", srcFile.Id)
 		r.SetBody(&base.Json{})
 	}, account)
 	return err
@@ -294,7 +370,7 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account)
 	tempFile.Close()
 
 	var resp UploadTaskResponse
-	_, err = GetState(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
+	_, err = GetClient(account).Request("POST", FILE_API_URL, func(r *resty.Request) {
 		r.SetBody(&base.Json{
 			"kind":        FILE,
 			"parent_id":   parentFile.Id,
@@ -311,6 +387,7 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account)
 
 	param := resp.Resumable.Params
 	if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
+		param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
 		client, err := oss.New(param.Endpoint, param.AccessKeyID, param.AccessKeySecret, oss.SecurityToken(param.SecurityToken), oss.EnableMD5(true))
 		if err != nil {
 			return err
@@ -319,22 +396,12 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account)
 		if err != nil {
 			return err
 		}
-		return bucket.UploadFile(param.Key, tempFile.Name(), 1<<22, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration))
+		err = bucket.UploadFile(param.Key, tempFile.Name(), 1<<22, oss.Routines(3), oss.Checkpoint(true, ""), oss.Expires(param.Expiration))
+		if err != nil {
+			return err
+		}
 	}
 	return nil
 }
 
-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
-	}
-	_, err = GetState(account).Request("PATCH", FILE_API_URL+"/{id}", func(r *resty.Request) {
-		r.SetPathParam("id", srcFile.Id)
-		r.SetBody(&base.Json{"name": dstName})
-	}, account)
-	return err
-}
-
 var _ base.Driver = (*XunLeiCloud)(nil)
diff --git a/drivers/xunlei/types.go b/drivers/xunlei/types.go
index c73c292ca2b..87f9df0457b 100644
--- a/drivers/xunlei/types.go
+++ b/drivers/xunlei/types.go
@@ -1,23 +1,35 @@
 package xunlei
 
 import (
+	"fmt"
 	"time"
 )
 
 type Erron struct {
-	Error            string `json:"error"`
 	ErrorCode        int64  `json:"error_code"`
+	ErrorMsg         string `json:"error"`
 	ErrorDescription string `json:"error_description"`
 	//	ErrorDetails   interface{} `json:"error_details"`
 }
 
+func (e *Erron) HasError() bool {
+	return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
+}
+
+func (e *Erron) Error() string {
+	return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
+}
+
+/*
+* 验证码Token
+**/
 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"`
+	RedirectUri  string            `json:"redirect_uri"`
 }
 
 type CaptchaTokenResponse struct {
@@ -26,6 +38,9 @@ type CaptchaTokenResponse struct {
 	Url          string `json:"url"`
 }
 
+/*
+* 登录
+**/
 type TokenResponse struct {
 	TokenType    string `json:"token_type"`
 	AccessToken  string `json:"access_token"`
@@ -36,6 +51,10 @@ type TokenResponse struct {
 	UserID string `json:"user_id"`
 }
 
+func (t *TokenResponse) Token() string {
+	return fmt.Sprint(t.TokenType, " ", t.AccessToken)
+}
+
 type SignInRequest struct {
 	CaptchaToken string `json:"captcha_token"`
 
@@ -46,6 +65,9 @@ type SignInRequest struct {
 	Password string `json:"password"`
 }
 
+/*
+* 文件
+**/
 type FileList struct {
 	Kind            string  `json:"kind"`
 	NextPageToken   string  `json:"next_page_token"`
@@ -116,6 +138,9 @@ type Files struct {
 	//Collection interface{} `json:"collection"`
 }
 
+/*
+* 上传
+**/
 type UploadTaskResponse struct {
 	UploadType string `json:"upload_type"`
 
diff --git a/drivers/xunlei/util.go b/drivers/xunlei/util.go
index 6c5315c9da3..040b9baec88 100644
--- a/drivers/xunlei/util.go
+++ b/drivers/xunlei/util.go
@@ -3,40 +3,10 @@ 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 (
 	API_URL        = "https://api-pan.xunlei.com/drive/v1"
 	FILE_API_URL   = API_URL + "/files"
@@ -44,9 +14,8 @@ const (
 )
 
 const (
-	FOLDER = "drive#folder"
-	FILE   = "drive#file"
-
+	FOLDER    = "drive#folder"
+	FILE      = "drive#file"
 	RESUMABLE = "drive#resumable"
 )
 
@@ -57,47 +26,32 @@ const (
 	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)
+	return method + ":" + c.Path
 }
 
+// 计算文件Gcid
 func getGcid(r io.Reader, size int64) (string, error) {
 	calcBlockSize := func(j int64) int64 {
-		if j >= 0 && j <= 134217728 {
-			return 262144
+		if j >= 0 && j <= 0x8000000 {
+			return 0x40000
 		}
-		if j <= 134217728 || j > 268435456 {
-			if j <= 268435456 || j > 536870912 {
-				return 2097152
+		if j <= 0x8000000 || j > 0x10000000 {
+			if j <= 0x10000000 || j > 0x20000000 {
+				return 0x200000
 			}
-			return 1048576
+			return 0x100000
 		}
-		return 524288
+		return 0x80000
 	}
-	/*
-		calcBlockSize := func(j int64) int64 {
-			psize := int64(0x40000)
-			for j/psize > 0x200 {
-				psize <<= 1
-			}
-			return psize
-		}
-	*/
 
 	hash1 := sha1.New()
 	hash2 := sha1.New()
+	readSize := calcBlockSize(size)
 	for {
 		hash2.Reset()
-		if n, err := io.CopyN(hash2, r, calcBlockSize(size)); err != nil && n == 0 {
+		if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
 			if err != io.EOF {
 				return "", err
 			}
diff --git a/drivers/xunlei/xunlei.go b/drivers/xunlei/xunlei.go
index 2e9d4c23040..d98bd4c0528 100644
--- a/drivers/xunlei/xunlei.go
+++ b/drivers/xunlei/xunlei.go
@@ -3,6 +3,7 @@ package xunlei
 import (
 	"fmt"
 	"net/http"
+	"strings"
 	"sync"
 	"time"
 
@@ -13,281 +14,239 @@ import (
 	log "github.com/sirupsen/logrus"
 )
 
-var xunleiClient = resty.New().SetHeaders(map[string]string{"Accept": "application/json;charset=UTF-8"}).SetTimeout(base.DefaultTimeout)
+// 缓存登录状态
+var userClients sync.Map
 
-// 一个账户只允许登陆一次
-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
+func GetClient(account *model.Account) *Client {
+	if v, ok := userClients.Load(account.Username); ok {
+		return v.(*Client)
 	}
-	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
-}
+	client := &Client{
+		Client: base.RestyClient,
 
-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
-		}
+		clientID:      account.ClientId,
+		clientSecret:  account.ClientSecret,
+		clientVersion: account.ClientVersion,
+		packageName:   account.PackageName,
+		algorithms:    strings.Split(account.Algorithms, ","),
+		userAgent:     account.UserAgent,
+		deviceID:      account.DeviceId,
 	}
-	return fmt.Sprint(s.tokenType, " ", s.accessToken), nil
+	userClients.Store(account.Username, client)
+	return client
 }
 
-func (s *State) getCaptchaToken(action string, account *model.Account) (string, error) {
-	if s.isCaptchaTokenExpires() {
-		return s.newCaptchaToken(action, nil, account)
-	}
-	return s.captchaToken, nil
-}
+type Client struct {
+	*resty.Client
+	sync.Mutex
 
-func (s *State) isCaptchaTokenExpires() bool {
-	return time.Now().UnixMilli() >= s.captchaTokenExpiresTime || s.captchaToken == "" || s.tokenType == ""
-}
+	clientID      string
+	clientSecret  string
+	clientVersion string
+	packageName   string
+	algorithms    []string
+	userAgent     string
+	deviceID      string
+
+	captchaToken string
 
-func (s *State) isTokensExpires() bool {
-	return time.Now().UnixMilli() >= s.tokenExpiresTime || s.accessToken == ""
+	token        string
+	refreshToken string
+	userID       string
 }
 
-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{
+// 请求验证码token
+func (c *Client) requestCaptchaToken(action string, meta map[string]string) error {
+	param := 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
+		CaptchaToken: c.captchaToken,
+		ClientID:     c.clientID,
+		DeviceID:     c.deviceID,
+		Meta:         meta,
+		RedirectUri:  "xlaccsdk01://xunlei.com/callback?state=harbor",
 	}
 
 	var e Erron
 	var resp CaptchaTokenResponse
-	_, err := xunleiClient.R().
-		SetBody(&creq).
+	_, err := c.Client.R().
+		SetBody(&param).
 		SetError(&e).
 		SetResult(&resp).
-		SetHeader("X-Device-Id", driverID).
-		SetQueryParam("client_id", CLIENT_ID).
 		Post(XLUSER_API_URL + "/shield/captcha/init")
 	if err != nil {
-		return "", err
+		return err
 	}
-	if e.ErrorCode != 0 {
-		return "", fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
+	if e.HasError() {
+		return &e
 	}
+
 	if resp.Url != "" {
-		return "", fmt.Errorf("需要验证验证码")
+		return fmt.Errorf("need verify:%s", resp.Url)
 	}
 
-	s.captchaTokenExpiresTime = (ctime + resp.ExpiresIn*1000) - 30000
-	s.captchaToken = resp.CaptchaToken
-	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(XLUSER_API_URL + "/auth/token")
-	if err != nil {
-		return err
+	if resp.CaptchaToken == "" {
+		return fmt.Errorf("empty captchaToken")
 	}
+	c.captchaToken = resp.CaptchaToken
+	return nil
+}
 
-	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:
-		return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
+// 验证码签名
+func (c *Client) captchaSign(time string) string {
+	str := fmt.Sprint(c.clientID, c.clientVersion, c.packageName, c.deviceID, time)
+	for _, algorithm := range c.algorithms {
+		str = utils.GetMD5Encode(str + algorithm)
 	}
+	return "1." + str
 }
 
-func (s *State) login(account *model.Account) error {
-	s.init()
-	ctime := time.Now().UnixMilli()
+// 登录
+func (c *Client) Login(account *model.Account) (err error) {
+	c.Lock()
+	defer c.Unlock()
+
+	defer func() {
+		if err != nil {
+			account.Status = err.Error()
+		} else {
+			account.Status = "work"
+		}
+		model.SaveAccount(account)
+	}()
+
 	url := XLUSER_API_URL + "/auth/signin"
-	captchaToken, err := s.newCaptchaToken(getAction("POST", url), map[string]string{"username": account.Username}, account)
+	err = c.requestCaptchaToken(getAction(http.MethodPost, url), map[string]string{"username": account.Username})
 	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().
+	_, err = c.Client.R().
 		SetResult(&resp).
 		SetError(&e).
-		SetBody(&signReq).
-		SetHeader("X-Device-Id", utils.GetMD5Encode(account.Username)).
-		SetQueryParam("client_id", CLIENT_ID).
+		SetBody(&SignInRequest{
+			CaptchaToken: c.captchaToken,
+			ClientID:     c.clientID,
+			ClientSecret: c.clientSecret,
+			Username:     account.Username,
+			Password:     account.Password,
+		}).
 		Post(url)
 	if err != nil {
 		return err
 	}
 
-	defer model.SaveAccount(account)
-	if e.ErrorCode != 0 {
-		account.Status = e.Error
-		return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
+	if e.HasError() {
+		return &e
+	}
+
+	if resp.RefreshToken == "" {
+		return base.ErrEmptyToken
 	}
-	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
+
+	c.token = resp.Token()
+	c.refreshToken = resp.RefreshToken
+	c.userID = resp.UserID
 	return nil
 }
 
-func (s *State) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
-	s.Lock()
-	token, err := s.getToken(account)
-	if err != nil {
-		return nil, err
+// 刷新验证码token
+func (c *Client) RefreshCaptchaToken(action string) error {
+	c.Lock()
+	defer c.Unlock()
+
+	timestamp := fmt.Sprint(time.Now().UnixMilli())
+	param := map[string]string{
+		"client_version": c.clientVersion,
+		"package_name":   c.packageName,
+		"user_id":        c.userID,
+		"captcha_sign":   c.captchaSign(timestamp),
+		"timestamp":      timestamp,
 	}
+	return c.requestCaptchaToken(action, param)
+}
+
+// 刷新token
+func (c *Client) RefreshToken() error {
+	c.Lock()
+	defer c.Unlock()
 
-	captchaToken, err := s.getCaptchaToken(getAction(method, url), account)
+	var e Erron
+	var resp TokenResponse
+	_, err := c.Client.R().
+		SetError(&e).
+		SetResult(&resp).
+		SetBody(&base.Json{
+			"grant_type":    "refresh_token",
+			"refresh_token": c.refreshToken,
+			"client_id":     c.clientID,
+			"client_secret": c.clientSecret,
+		}).
+		Post(XLUSER_API_URL + "/auth/token")
 	if err != nil {
-		return nil, err
+		return err
+	}
+	if e.HasError() {
+		return &e
 	}
 
-	req := xunleiClient.R().
-		SetHeaders(map[string]string{
-			"X-Device-Id":     utils.GetMD5Encode(account.Username),
-			"Authorization":   token,
-			"X-Captcha-Token": captchaToken,
-		}).
-		SetQueryParam("client_id", CLIENT_ID)
-
-	callback(req)
-	s.Unlock()
-
-	var res *resty.Response
-	switch method {
-	case "GET":
-		res, err = req.Get(url)
-	case "POST":
-		res, err = req.Post(url)
-	case "DELETE":
-		res, err = req.Delete(url)
-	case "PATCH":
-		res, err = req.Patch(url)
-	case "PUT":
-		res, err = req.Put(url)
-	default:
-		return nil, base.ErrNotSupport
+	if resp.RefreshToken == "" {
+		return base.ErrEmptyToken
 	}
 
+	c.token = resp.TokenType + " " + resp.AccessToken
+	c.refreshToken = resp.RefreshToken
+	c.userID = resp.UserID
+	return nil
+}
+
+func (c *Client) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) {
+	c.Lock()
+	req := c.Client.R().
+		SetHeaders(map[string]string{
+			"X-Device-Id":     c.deviceID,
+			"Authorization":   c.token,
+			"X-Captcha-Token": c.captchaToken,
+			"User-Agent":      c.userAgent,
+			"client_id":       c.clientID,
+		})
+	if callback != nil {
+		callback(req)
+	}
+	c.Unlock()
+
+	res, err := req.Execute(method, url)
 	if err != nil {
 		return nil, err
 	}
 	log.Debug(res.String())
 
 	var e Erron
-	err = utils.Json.Unmarshal(res.Body(), &e)
-	if err != nil {
+	if err = utils.Json.Unmarshal(res.Body(), &e); err != nil {
 		return nil, err
 	}
+
+	// 处理错误
 	switch e.ErrorCode {
-	case 9:
-		_, err = s.newCaptchaToken(getAction(method, url), nil, account)
-		if err != nil {
-			return nil, err
+	case 0:
+		return res, nil
+	case 4122, 4121, 10: // token过期
+		if err = c.RefreshToken(); err == nil {
+			break
 		}
 		fallthrough
-	case 4122, 4121: // Authorization expired
-		return s.Request(method, url, callback, account)
-	case 0:
-		if res.StatusCode() == http.StatusOK {
-			return res, nil
+	case 16: // 登录失效
+		if err = c.Login(account); err != nil {
+			return nil, err
+		}
+	case 9: // 验证码token过期
+		if err = c.RefreshCaptchaToken(getAction(method, url)); err != nil {
+			return nil, err
 		}
-		return nil, fmt.Errorf(res.String())
 	default:
-		return nil, fmt.Errorf("%s : %s", e.Error, e.ErrorDescription)
+		return nil, &e
 	}
-}
-
-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)
+	return c.Request(method, url, callback, account)
 }
diff --git a/model/account.go b/model/account.go
index e14be95a7d7..60033015b1c 100644
--- a/model/account.go
+++ b/model/account.go
@@ -50,6 +50,13 @@ type Account struct {
 	CustomHost    string `json:"custom_host"`
 	ExtractFolder string `json:"extract_folder"`
 	Bool1         bool   `json:"bool_1"`
+	// for xunlei
+	Algorithms    string `json:"algorithms"`
+	ClientVersion string `json:"client_version"`
+	PackageName   string `json:"package_name"`
+	UserAgent    string `json:"user_agent"`
+	CaptchaToken string `json:"captcha_token"`
+	DeviceId     string `json:"device_id"`
 }
 
 var accountsMap = make(map[string]Account)
diff --git a/server/webdav/file.go b/server/webdav/file.go
index c26826b2265..d4f8d50d2f4 100644
--- a/server/webdav/file.go
+++ b/server/webdav/file.go
@@ -7,6 +7,14 @@ package webdav
 import (
 	"context"
 	"fmt"
+	"mime"
+	"net"
+	"net/http"
+	"path"
+	"path/filepath"
+	"strings"
+	"time"
+
 	"github.com/Xhofe/alist/conf"
 	"github.com/Xhofe/alist/drivers/base"
 	"github.com/Xhofe/alist/drivers/operate"
@@ -14,12 +22,6 @@ import (
 	"github.com/Xhofe/alist/server/common"
 	"github.com/Xhofe/alist/utils"
 	log "github.com/sirupsen/logrus"
-	"net"
-	"net/http"
-	"path"
-	"path/filepath"
-	"strings"
-	"time"
 )
 
 type FileSystem struct{}
@@ -197,8 +199,17 @@ func (fs *FileSystem) Upload(ctx context.Context, r *http.Request, rawPath strin
 	} else {
 		delete(upFileMap, rawPath)
 	}
+	mimeType := r.Header.Get("Content-Type")
+	if mimeType == "" || strings.ToLower(mimeType) == "application/octet-stream" {
+		mimeTypeTmp := mime.TypeByExtension(path.Ext(fileName))
+		if mimeTypeTmp != "" {
+			mimeType = mimeTypeTmp
+		} else {
+			mimeType = "application/octet-stream"
+		}
+	}
 	fileData := model.FileStream{
-		MIMEType:   r.Header.Get("Content-Type"),
+		MIMEType:   mimeType,
 		File:       r.Body,
 		Size:       fileSize,
 		Name:       fileName,


Test patch: