diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27d8a508..55ad7910 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 3c7c898b..6591a774 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 5d8ae7dc..e93f144b 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 27ffcefb..eeb6bebd 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 82d66223..012d350e 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 b9275931..cf0fb8e5 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 00000000..287ec433 --- /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 00000000..a502eefe --- /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 00000000..9bc954b0 --- /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 00000000..5ab40a52 --- /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 00000000..68027f41 --- /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 ebe77b30..3a1f61ec 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 a597702a..d77fe7a1 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 6d39ad22..bd2dddbc 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 c73c292c..87f9df04 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 6c5315c9..040b9bae 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 2e9d4c23..d98bd4c0 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 { +func GetClient(account *model.Account) *Client { + if v, ok := userClients.Load(account.Username); ok { + return v.(*Client) + } + + client := &Client{ + Client: base.RestyClient, + + clientID: account.ClientId, + clientSecret: account.ClientSecret, + clientVersion: account.ClientVersion, + packageName: account.PackageName, + algorithms: strings.Split(account.Algorithms, ","), + userAgent: account.UserAgent, + deviceID: account.DeviceId, + } + userClients.Store(account.Username, client) + return client +} + +type Client struct { + *resty.Client 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 + clientID string + clientSecret string + clientVersion string + packageName string + algorithms []string + userAgent string + deviceID string + + captchaToken string + + token string + refreshToken string + userID string } -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{ +// 请求验证码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(¶m). 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 + if resp.CaptchaToken == "" { + return fmt.Errorf("empty captchaToken") + } + c.captchaToken = resp.CaptchaToken + return 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 - } - - 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 } - 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 + + if resp.RefreshToken == "" { + return base.ErrEmptyToken + } + + 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() - captchaToken, err := s.getCaptchaToken(getAction(method, url), account) - if err != nil { - return nil, err + 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) +} - req := xunleiClient.R(). - SetHeaders(map[string]string{ - "X-Device-Id": utils.GetMD5Encode(account.Username), - "Authorization": token, - "X-Captcha-Token": captchaToken, +// 刷新token +func (c *Client) RefreshToken() error { + c.Lock() + defer c.Unlock() + + 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, }). - 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 + Post(XLUSER_API_URL + "/auth/token") + if err != nil { + return err + } + if e.HasError() { + return &e } + 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 e14be95a..60033015 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 c26826b2..d4f8d50d 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,