├── device └── README.md ├── share └── README.md ├── examples ├── README.md ├── auth_oauthurl.go ├── account_quota.go ├── file_list.go ├── account_userinfo.go ├── file_metas.go ├── file_upload.go ├── file_streaming.go ├── auth_accesstoken.go ├── auth_userinfo.go ├── auth_refreshtoken.go └── file_download.go ├── account ├── README.md ├── account_test.go └── account.go ├── nas └── README.md ├── file ├── README.md ├── upload_test.go ├── download_test.go ├── file_test.go ├── download.go ├── file.go └── upload.go ├── auth ├── README.md ├── auth_test.go └── auth.go ├── go.mod ├── .gitignore ├── README.md ├── conf └── conf.go ├── LICENSE └── utils ├── functions.go ├── file ├── upload.go └── download.go └── httpclient └── httpclient.go /device/README.md: -------------------------------------------------------------------------------- 1 | # 设备管理 -------------------------------------------------------------------------------- /share/README.md: -------------------------------------------------------------------------------- 1 | # 第三方分享 -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # examples 2 | 使用示例 3 | 4 | -------------------------------------------------------------------------------- /account/README.md: -------------------------------------------------------------------------------- 1 | # 账号 2 | 1. 获取网盘用户信息 3 | 2. 获取用户网盘空间容量信息 -------------------------------------------------------------------------------- /nas/README.md: -------------------------------------------------------------------------------- 1 | # NAS 2 | NAS功能不是很完善,建议直接使用文件管理服务来进行文件的上传和下载 -------------------------------------------------------------------------------- /file/README.md: -------------------------------------------------------------------------------- 1 | # 文件管理 2 | 1. 文件列表 3 | 2. 文件信息 4 | 3. 音视频在线播放地址 5 | 4. 文件上传 6 | 5. 文件下载 -------------------------------------------------------------------------------- /auth/README.md: -------------------------------------------------------------------------------- 1 | # 百度账号授权 2 | 1. 获取OAuth授权url 3 | 2. 获取AccessToken 4 | 3. 刷新AccessToken 5 | 4. 获取授权用户的百度账号信息 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jsyzchen/pan 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/bitly/go-simplejson v0.5.0 7 | github.com/syyongx/php2go v0.9.4 8 | ) 9 | -------------------------------------------------------------------------------- /examples/auth_oauthurl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/auth" 6 | ) 7 | 8 | func main() { 9 | clientID := "tfk7yVXzNbTB7jnSYdfdsg" 10 | clientSecret := "XPOiyTivh1hnxTpiTFBqAADDfvnsql" 11 | redirectUri := "https://coffeephp.com" 12 | authClient := auth.NewAuthClient(clientID, clientSecret) 13 | res := authClient.OAuthUrl(redirectUri) 14 | fmt.Println(res) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea 18 | .DS_Store 19 | go.sum 20 | storage 21 | conf/test_conf.go -------------------------------------------------------------------------------- /file/upload_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/jsyzchen/pan/conf" 5 | "testing" 6 | ) 7 | 8 | func TestUpload(t *testing.T) { 9 | fileUploader := NewUploader(conf.TestData.AccessToken, conf.TestData.Path, conf.TestData.LocalFilePath) 10 | res, err := fileUploader.Upload() 11 | if err != nil { 12 | t.Fail() 13 | } else { 14 | t.Logf("TestUpload Success res: %+v", res) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pan Go Sdk 2 | 该代码库为百度网盘开放平台Go语言的SDK,详细请参考官方技术文档 3 | 4 | ## 下载 5 | ```bash 6 | go get -u github.com/jsyzchen/pan 7 | ``` 8 | 并在项目中引入`github.com/jsyzchen/pan` 9 | ```go 10 | import ( 11 | "github.com/jsyzchen/pan/auth" 12 | "github.com/jsyzchen/pan/file" 13 | ) 14 | ``` 15 | 16 | ## 使用示例 17 | [参考代码](https://github.com/jsyzchen/pan/tree/main/examples) 18 | -------------------------------------------------------------------------------- /examples/account_quota.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/account" 6 | ) 7 | 8 | func main() { 9 | accessToken := "121.a2b8e9decca9d322acc34d7baeb3404f.YgQAQF94HC4F0g9xGgODfm0VmZ_kbKhMYOTEwHT.FiBjnQ" 10 | accountClient := account.NewAccountClient(accessToken) 11 | res, err := accountClient.Quota() 12 | if err != nil { 13 | fmt.Println("err:", err) 14 | return 15 | } 16 | fmt.Println(res) 17 | } 18 | -------------------------------------------------------------------------------- /examples/file_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/file" 6 | ) 7 | 8 | func main() { 9 | accessToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 10 | fileClient := file.NewFileClient(accessToken) 11 | res, err := fileClient.List("/apps/书梯", 0, 100) 12 | if err != nil { 13 | fmt.Println("err:", err) 14 | return 15 | } 16 | fmt.Println(res) 17 | } 18 | -------------------------------------------------------------------------------- /examples/account_userinfo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/account" 6 | ) 7 | 8 | func main() { 9 | accessToken := "121.a2b8e9decca9d322acc34d7baeb3404f.YgQAQF94HC4F0g9xGgODfm0VmZ_kbKhMYOTEwHT.FiBjnQ" 10 | accountClient := account.NewAccountClient(accessToken) 11 | res, err := accountClient.Quota() 12 | if err != nil { 13 | fmt.Println("err:", err) 14 | return 15 | } 16 | fmt.Println(res) 17 | } 18 | -------------------------------------------------------------------------------- /examples/file_metas.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/file" 6 | ) 7 | 8 | func main() { 9 | accessToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 10 | fileClient := file.NewFileClient(accessToken) 11 | fsIDs := []uint64{765773701501523} 12 | res, err := fileClient.Metas(fsIDs) 13 | if err != nil { 14 | fmt.Println("err:", err) 15 | return 16 | } 17 | fmt.Println(res) 18 | } 19 | -------------------------------------------------------------------------------- /examples/file_upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/file" 6 | ) 7 | 8 | func main() { 9 | accessToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 10 | path := "/apps/书梯/CHSS.mkv" 11 | localFilePath := "/Download/CHSS.mkv" 12 | fileUploader := file.NewUploader(accessToken, path, localFilePath) 13 | res, err := fileUploader.Upload() 14 | if err != nil { 15 | fmt.Println("err:", err) 16 | return 17 | } 18 | fmt.Println(res) 19 | } -------------------------------------------------------------------------------- /examples/file_streaming.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/file" 6 | ) 7 | 8 | func main() { 9 | accessToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 10 | fileClient := file.NewFileClient(accessToken) 11 | path := "/apps/书梯/CHSS.mkv" 12 | transcodingType := "M3U8_AUTO_480" 13 | res, err := fileClient.Streaming(path, transcodingType) 14 | if err != nil { 15 | fmt.Println("err:", err) 16 | return 17 | } 18 | fmt.Println(res) 19 | } 20 | -------------------------------------------------------------------------------- /examples/auth_accesstoken.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/auth" 6 | ) 7 | 8 | func main() { 9 | clientID := "tfk7yVXzNbTB7jnSYdfdsg" 10 | clientSecret := "XPOiyTivh1hnxTpiTFBqAADDfvnsql" 11 | code := "746ab1956b0b221221b3dc0c3dce7362" 12 | redirectUri := "https://coffeephp.com" 13 | authClient := auth.NewAuthClient(clientID, clientSecret) 14 | res, err := authClient.AccessToken(code, redirectUri) 15 | if err != nil { 16 | fmt.Println("err:", err) 17 | return 18 | } 19 | fmt.Println(res) 20 | } 21 | -------------------------------------------------------------------------------- /examples/auth_userinfo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/auth" 6 | ) 7 | 8 | func main() { 9 | clientID := "tfk7yVXzNbTB7jnSYdfdsg" 10 | clientSecret := "XPOiyTivh1hnxTpiTFBqAADDfvnsql" 11 | accessToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 12 | authClient := auth.NewAuthClient(clientID, clientSecret) 13 | res, err := authClient.UserInfo(accessToken) 14 | if err != nil { 15 | fmt.Println("err:", err) 16 | return 17 | } 18 | fmt.Println(res) 19 | } 20 | -------------------------------------------------------------------------------- /examples/auth_refreshtoken.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/auth" 6 | ) 7 | 8 | func main() { 9 | clientID := "tfk7yVXzNbTB7jnSYdfdsg" 10 | clientSecret := "XPOiyTivh1hnxTpiTFBqAADDfvnsql" 11 | refreshToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 12 | authClient := auth.NewAuthClient(clientID, clientSecret) 13 | res, err := authClient.RefreshToken(refreshToken) 14 | if err != nil { 15 | fmt.Println("err:", err) 16 | return 17 | } 18 | fmt.Println(res) 19 | } 20 | -------------------------------------------------------------------------------- /account/account_test.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/jsyzchen/pan/conf" 5 | "testing" 6 | ) 7 | 8 | func TestAccount_UserInfo(t *testing.T) { 9 | accountClient := NewAccountClient(conf.TestData.AccessToken) 10 | res, err := accountClient.UserInfo() 11 | if err != nil { 12 | t.Fail() 13 | } 14 | t.Logf("TestAccount_UserInfo res: %+v", res) 15 | } 16 | 17 | func TestAccount_Quota(t *testing.T) { 18 | accountClient := NewAccountClient(conf.TestData.AccessToken) 19 | res, err := accountClient.Quota() 20 | if err != nil { 21 | t.Fail() 22 | } 23 | t.Logf("TestAccount_Quota res: %+v", res) 24 | } 25 | -------------------------------------------------------------------------------- /file/download_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/jsyzchen/pan/conf" 5 | "testing" 6 | ) 7 | 8 | func TestDownload(t *testing.T) { 9 | fileDownloader := NewDownloaderWithFsID(conf.TestData.AccessToken, conf.TestData.FsID, conf.TestData.LocalFilePath) 10 | err := fileDownloader.Download() 11 | if err != nil { 12 | t.Fail() 13 | } else { 14 | t.Logf("TestDownload Success") 15 | } 16 | } 17 | 18 | func TestDownloaderWithPath(t *testing.T) { 19 | fileDownloader := NewDownloaderWithPath(conf.TestData.AccessToken, conf.TestData.Path, conf.TestData.LocalFilePath) 20 | err := fileDownloader.Download() 21 | if err != nil { 22 | t.Fail() 23 | } else { 24 | t.Logf("TestDownload Success") 25 | } 26 | } 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type PcsResponseBase struct { 4 | ErrorCode int `json:"error_code"` 5 | ErrorMsg string `json:"error_msg"` 6 | RequestID uint64 `json:"request_id"` 7 | } 8 | 9 | type CloudDiskResponseBase struct { 10 | ErrorCode int `json:"errno"` 11 | ErrorMsg string `json:"errmsg"` 12 | RequestID uint64 `json:"request_id"` 13 | } 14 | 15 | type TestDataConfig struct { 16 | ClientID string 17 | ClientSecret string 18 | RedirectUri string 19 | Code string 20 | AccessToken string 21 | RefreshToken string 22 | Dir string 23 | FsID uint64 24 | Path string 25 | LocalFilePath string 26 | TranscodingType string 27 | } 28 | 29 | const ( 30 | BaiduOpenApiDomain = "https://openapi.baidu.com" 31 | OpenApiDomain = "https://pan.baidu.com" 32 | PcsDataDomain = "https://d.pcs.baidu.com" 33 | PcsApiDomain = "https://pcs.baidu.com" 34 | ) 35 | 36 | // 测试参数 37 | var TestData TestDataConfig 38 | -------------------------------------------------------------------------------- /file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/jsyzchen/pan/conf" 5 | "testing" 6 | ) 7 | 8 | func TestFile_List(t *testing.T) { 9 | fileClient := NewFileClient(conf.TestData.AccessToken) 10 | res, err := fileClient.List(conf.TestData.Dir, 0, 100) 11 | if err != nil { 12 | t.Errorf("TestList failed, err:%v", err) 13 | } 14 | t.Logf("TestList res: %+v", res) 15 | } 16 | 17 | func TestFile_Metas(t *testing.T) { 18 | fileClient := NewFileClient(conf.TestData.AccessToken) 19 | res, err := fileClient.Metas([]uint64{conf.TestData.FsID}) 20 | if err != nil { 21 | t.Errorf("TestMetas failed, err:%v", err) 22 | } 23 | t.Logf("TestMetas res: %+v", res) 24 | } 25 | 26 | func TestFile_Streaming(t *testing.T) { 27 | fileClient := NewFileClient(conf.TestData.AccessToken) 28 | res, err := fileClient.Streaming(conf.TestData.Path, conf.TestData.TranscodingType) 29 | if err != nil { 30 | t.Errorf("TestFile_Streaming failed, err:%v", err) 31 | } 32 | t.Logf("TestFile_Streaming res: %+v", res) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jsyz Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/functions.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | func InterfaceToString(val interface{}) string { 10 | switch val.(type) { 11 | case string: 12 | return val.(string) 13 | case int: 14 | return strconv.FormatInt(int64(val.(int)), 10) 15 | case int64: 16 | return strconv.FormatInt(val.(int64), 10) 17 | case uint64: 18 | return strconv.FormatUint(val.(uint64), 10) 19 | case float32: 20 | return strconv.FormatFloat(float64(val.(float32)), 'f', -1, 32) 21 | case float64: 22 | return strconv.FormatFloat(val.(float64), 'f', -1, 64) 23 | default: 24 | bytes, _ := json.Marshal(val) 25 | return string(bytes) 26 | } 27 | } 28 | 29 | // 将struct转成url的参数,并去除有json omitempty且值为空的参数,类似param1=1¶m2=2 30 | func StructToUrlQuery(m interface{}) (string, error) { 31 | query := "" 32 | 33 | b, err := json.Marshal(m) 34 | if err != nil { 35 | return query, err 36 | } 37 | 38 | var f map[string]interface{} 39 | err = json.Unmarshal(b, &f) 40 | if err != nil { 41 | return query, err 42 | } 43 | 44 | v := url.Values{} 45 | for key, value := range f { 46 | v.Set(key, InterfaceToString(value)) 47 | } 48 | query = v.Encode() 49 | return query, nil 50 | } 51 | -------------------------------------------------------------------------------- /auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/jsyzchen/pan/conf" 5 | "testing" 6 | ) 7 | 8 | func TestAuth_OAuthUrl(t *testing.T) { 9 | authClient := NewAuthClient(conf.TestData.ClientID, conf.TestData.ClientSecret) 10 | res := authClient.OAuthUrl(conf.TestData.RedirectUri) 11 | t.Logf("TestAuth_OAuthUrl res: %+v", res) 12 | } 13 | 14 | func TestAuth_AccessToken(t *testing.T) { 15 | authClient := NewAuthClient(conf.TestData.ClientID, conf.TestData.ClientSecret) 16 | res, err := authClient.AccessToken(conf.TestData.Code, conf.TestData.RedirectUri) 17 | if err != nil { 18 | t.Errorf("authClient.AccessToken failed, err:%v", err) 19 | } 20 | t.Logf("TestAuth_AccessToken res: %+v", res) 21 | } 22 | 23 | func TestAuth_RefreshToken(t *testing.T) { 24 | authClient := NewAuthClient(conf.TestData.ClientID, conf.TestData.ClientSecret) 25 | res, err := authClient.RefreshToken(conf.TestData.RefreshToken) 26 | if err != nil { 27 | t.Errorf("authClient.AccessToken failed, err:%v", err) 28 | } 29 | t.Logf("TestAuth_RefreshToken res:%+v", res) 30 | } 31 | 32 | func TestAuth_UserInfo(t *testing.T) { 33 | authClient := NewAuthClient(conf.TestData.ClientID, conf.TestData.ClientSecret) 34 | res, err := authClient.UserInfo(conf.TestData.AccessToken) 35 | if err != nil { 36 | t.Errorf("TestAuth_UserInfo failed, err:%v", err) 37 | } 38 | t.Logf("TestAuth_UserInfo res:%+v", res) 39 | } 40 | -------------------------------------------------------------------------------- /examples/file_download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jsyzchen/pan/conf" 6 | "github.com/jsyzchen/pan/file" 7 | ) 8 | 9 | func main() { 10 | accessToken := "122.b0a9ab31cc24b429d460cd3ce1f1af97.Yn53jGAwd_1elGgODFvYl1sp9qOYVUDRiVawin5.tbNcEw" 11 | localFilePath := "/Download/test.jpg" 12 | 13 | // 方式1:通过下载地址直接下载 14 | dLink := "https://d.pcs.baidu.com/file/a3089c75958fb77d45b2ce6cb78fd673?fid=1426856282-250528-434991606534785&rt=pr&sign=FDtAER-DCb740ccc5511e5e8fedcff06b081203-eSDq%2FMAFhWs7qSuYaJfD3%2BbkH98%3D&expires=8h&chkbd=0&chkv=0&dp-logid=2194032036121781573&dp-callid=0&dstime=1610806466&r=446016834&origin_appid=16820976&file_type=0" 15 | fileDownloader := file.NewDownloader(accessToken, dLink, localFilePath) 16 | if err := fileDownloader.Download(); err != nil { 17 | fmt.Println("1.fileDownloader.Download failed, err:", err) 18 | return 19 | } 20 | fmt.Println("1.fileDownloader.Download success") 21 | 22 | // 方式2:通过文件fsID下载 23 | var fsID uint64 24 | fsID = 759719327699432 25 | fileDownloader = file.NewDownloaderWithFsID(accessToken, fsID, localFilePath) 26 | if err := fileDownloader.Download(); err != nil { 27 | fmt.Println("2.fileDownloader.DownloadWithFsID failed, err:", err) 28 | return 29 | } 30 | fmt.Println("2.fileDownloader.Download success") 31 | 32 | // 方式3:通过文件路径下载,非开放平台公开接口,生产环境谨慎使用 33 | fileDownloader = file.NewDownloaderWithPath(conf.TestData.AccessToken, conf.TestData.Path, conf.TestData.LocalFilePath) 34 | err := fileDownloader.Download() 35 | if err != nil { 36 | fmt.Println("3.fileDownloader.DownloaderWithPath failed, err:", err) 37 | return 38 | } 39 | fmt.Println("3.fileDownloader.DownloaderWithPath success") 40 | } -------------------------------------------------------------------------------- /file/download.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "errors" 5 | "github.com/jsyzchen/pan/account" 6 | "github.com/jsyzchen/pan/conf" 7 | "github.com/jsyzchen/pan/utils/file" 8 | "log" 9 | "net/url" 10 | ) 11 | 12 | type Downloader struct { 13 | LocalFilePath string 14 | DownloadLink string 15 | FsID uint64 16 | Path string 17 | AccessToken string 18 | TotalPart int 19 | } 20 | 21 | const ( 22 | PcsFileDownloadUri = "/rest/2.0/pcs/file?method=download" 23 | ) 24 | 25 | func NewDownloader(accessToken string, downloadLink string, localFilePath string) *Downloader { 26 | return &Downloader{ 27 | AccessToken: accessToken, 28 | LocalFilePath: localFilePath, 29 | DownloadLink: downloadLink, 30 | } 31 | } 32 | 33 | func NewDownloaderWithFsID(accessToken string, fsID uint64, localFilePath string) *Downloader { 34 | return &Downloader{ 35 | AccessToken: accessToken, 36 | FsID: fsID, 37 | LocalFilePath: localFilePath, 38 | } 39 | } 40 | 41 | // 非开放平台公开接口,生产环境谨慎使用 42 | func NewDownloaderWithPath(accessToken string, path string, localFilePath string) *Downloader { 43 | return &Downloader{ 44 | AccessToken: accessToken, 45 | Path: path, 46 | LocalFilePath: localFilePath, 47 | } 48 | } 49 | 50 | // 执行下载 51 | func (d *Downloader) Download() error { 52 | downloadLink := "" 53 | if d.LocalFilePath == "" || d.AccessToken == "" { 54 | return errors.New("param error, localFilePath is empty") 55 | } 56 | 57 | if d.DownloadLink != "" {//直接下载 58 | downloadLink = d.DownloadLink 59 | } else if d.FsID != 0 { 60 | // 根据fsID获取下载链接 61 | fileClient := NewFileClient(d.AccessToken) 62 | metas, err := fileClient.Metas([]uint64{d.FsID}) 63 | if err != nil { 64 | log.Println("fileClient.Metas failed, err:", err) 65 | return err 66 | } 67 | if len(metas.List) == 0 { 68 | log.Println("file don't exist") 69 | return errors.New("file don't exist") 70 | } 71 | downloadLink = metas.List[0].DLink 72 | } else if d.Path != "" { // TODO 如何通过文件路径获取下载地址 73 | v := url.Values{} 74 | v.Add("path", d.Path) 75 | v.Add("access_token", d.AccessToken) 76 | body := v.Encode() 77 | downloadLink = conf.PcsApiDomain + PcsFileDownloadUri + "&" + body 78 | } else { 79 | return errors.New("param error") 80 | } 81 | 82 | if downloadLink == "" { 83 | return errors.New("param error, downloadLink is empty") 84 | } 85 | 86 | downloadLink += "&access_token=" + d.AccessToken 87 | downloader := file.NewFileDownloader(downloadLink, d.LocalFilePath) 88 | 89 | accountClient := account.NewAccountClient(d.AccessToken) 90 | if userInfo, err := accountClient.UserInfo(); err == nil { 91 | log.Println("VipType:", userInfo.VipType) 92 | if userInfo.VipType == 2 { //当前用户是超级会员 93 | downloader.SetPartSize(52428800) //设置每分片下载文件大小,50M 94 | downloader.SetCoroutineNum(10) //分片下载并发数,普通用户不支持并发分片下载 95 | } 96 | } 97 | 98 | if err := downloader.Download(); err != nil { 99 | log.Println("download failed, err:", err) 100 | return err 101 | } 102 | 103 | return nil 104 | } 105 | 106 | 107 | -------------------------------------------------------------------------------- /account/account.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/jsyzchen/pan/conf" 8 | "github.com/jsyzchen/pan/utils/httpclient" 9 | "log" 10 | "net/url" 11 | "strconv" 12 | ) 13 | 14 | type UserInfoResponse struct { 15 | BaiduName string `json:"baidu_name"` 16 | NetdiskName string `json:"netdisk_name"` 17 | AvatarUrl string `json:"avatar_url"` 18 | VipType int `json:"vip_type"` 19 | Uk int `json:"uk"` //uk字段对应auth.UserInfo方法返回的user_id 20 | ErrorCode int `json:"errno"` 21 | ErrorMsg string `json:"errmsg"` 22 | RequestID int 23 | RequestIDStr string `json:"request_id"` //用户信息接口返回的request_id为string类型 24 | } 25 | 26 | type QuotaResponse struct { 27 | conf.CloudDiskResponseBase 28 | Total int `json:"total"` 29 | Used int `json:"used"` 30 | Free int `json:"free"` 31 | Expire bool `json:"expire"` 32 | } 33 | 34 | type Account struct { 35 | AccessToken string 36 | } 37 | 38 | const UserInfoUri = "/rest/2.0/xpan/nas?method=uinfo" 39 | const QuotaUri = "/api/quota" 40 | 41 | func NewAccountClient(accessToken string) *Account { 42 | return &Account{ 43 | AccessToken: accessToken, 44 | } 45 | } 46 | 47 | // 获取网盘用户信息 48 | func (a *Account) UserInfo() (UserInfoResponse, error) { 49 | ret := UserInfoResponse{} 50 | 51 | v := url.Values{} 52 | v.Add("access_token", a.AccessToken) 53 | query := v.Encode() 54 | 55 | requestUrl := conf.OpenApiDomain + UserInfoUri + "&" + query 56 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 57 | if err != nil { 58 | log.Println("httpclient.Get failed, err:", err) 59 | return ret, err 60 | } 61 | 62 | if resp.StatusCode != 200 { 63 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 64 | } 65 | 66 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 67 | return ret, err 68 | } 69 | 70 | if ret.ErrorCode != 0 {//错误码不为0 71 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 72 | } 73 | 74 | //兼容用户信息接口返回的request_id为string类型的问题 75 | ret.RequestID, err = strconv.Atoi(ret.RequestIDStr) 76 | if err != nil { 77 | log.Println("strconv.Atoi failed, err:", err) 78 | return ret, err 79 | } 80 | 81 | return ret, nil 82 | } 83 | 84 | // 获取用户网盘容量信息 85 | func (a *Account) Quota() (QuotaResponse, error) { 86 | ret := QuotaResponse{} 87 | 88 | v := url.Values{} 89 | v.Add("access_token", a.AccessToken) 90 | v.Add("checkfree", "1") 91 | v.Add("checkexpire", "1") 92 | query := v.Encode() 93 | 94 | requestUrl := conf.OpenApiDomain + QuotaUri + "?" + query 95 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 96 | if err != nil { 97 | log.Println("httpclient.Get failed, err:", err) 98 | return ret, err 99 | } 100 | 101 | if resp.StatusCode != 200 { 102 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 103 | } 104 | 105 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 106 | return ret, err 107 | } 108 | 109 | if ret.ErrorCode != 0 {//错误码不为0 110 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 111 | } 112 | 113 | return ret, nil 114 | } 115 | -------------------------------------------------------------------------------- /utils/file/upload.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jsyzchen/pan/utils/httpclient" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "mime/multipart" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | ) 14 | 15 | type Uploader struct { 16 | Url string 17 | FilePath string 18 | } 19 | 20 | //NewFileUploader 21 | func NewFileUploader(url, filePath string) *Uploader { 22 | return &Uploader{ 23 | Url: url, 24 | FilePath: filePath, 25 | } 26 | } 27 | 28 | // 上传文件 29 | func (u *Uploader) Upload() ([]byte, error) { 30 | ret := []byte("") 31 | 32 | bodyBuf := &bytes.Buffer{} 33 | bodyWriter := multipart.NewWriter(bodyBuf) 34 | //"file" 为接收时定义的参数名 35 | fileWriter, err := bodyWriter.CreateFormFile("file", filepath.Base(u.FilePath)) 36 | if err != nil { 37 | log.Println("error writing to buffer, err:", err) 38 | return ret, err 39 | } 40 | 41 | //打开文件 42 | fh, err := os.Open(u.FilePath) 43 | if err != nil { 44 | log.Println("error opening file, err:", err) 45 | return ret, err 46 | } 47 | defer fh.Close() 48 | 49 | //iocopy 50 | _, err = io.Copy(fileWriter, fh) 51 | if err != nil { 52 | return ret, err 53 | } 54 | contentType := bodyWriter.FormDataContentType() 55 | bodyWriter.Close() 56 | 57 | //提交请求 58 | request, err := http.NewRequest("POST", u.Url, bodyBuf) 59 | if err != nil { 60 | return ret, err 61 | } 62 | 63 | request.Header.Add("Content-Type", contentType) 64 | //随机设置一个User-Agent 65 | userAgent := httpclient.GetRandomUserAgent() 66 | request.Header.Set("User-Agent", userAgent) 67 | 68 | //处理返回结果 69 | client := &http.Client{} 70 | resp, err := client.Do(request) 71 | //打印接口返回信息 72 | if err != nil { 73 | log.Println("request uploadUrl failed, err:", err) 74 | return ret, err 75 | } 76 | defer resp.Body.Close() 77 | 78 | respBody, err := ioutil.ReadAll(resp.Body) 79 | if err != nil { 80 | return ret, err 81 | } 82 | //根据实际需要,返回相应的信息 83 | return respBody, nil 84 | } 85 | 86 | //直接通过字节上传 87 | func (u *Uploader) UploadByByte(fileByte []byte) ([]byte, error) { 88 | ret := []byte("") 89 | 90 | bodyBuf := &bytes.Buffer{} 91 | 92 | bodyWriter := multipart.NewWriter(bodyBuf) 93 | //"file" 为接收时定义的参数名 94 | fileWriter, err := bodyWriter.CreateFormFile("file", filepath.Base(u.FilePath)) 95 | if err != nil { 96 | log.Println("error writing to buffer, err:", err) 97 | return ret, err 98 | } 99 | 100 | //iocopy 101 | _, err = io.Copy(fileWriter, bytes.NewReader(fileByte)) 102 | if err != nil { 103 | return ret, err 104 | } 105 | contentType := bodyWriter.FormDataContentType() 106 | bodyWriter.Close() 107 | 108 | //提交请求 109 | request, err := http.NewRequest("POST", u.Url, bodyBuf) 110 | if err != nil { 111 | return ret, err 112 | } 113 | 114 | request.Header.Add("Content-Type", contentType) 115 | //随机设置一个User-Agent 116 | userAgent := httpclient.GetRandomUserAgent() 117 | request.Header.Set("User-Agent", userAgent) 118 | 119 | //处理返回结果 120 | client := &http.Client{} 121 | resp, err := client.Do(request) 122 | //打印接口返回信息 123 | if err != nil { 124 | log.Println("上传错误信息:", err) 125 | return ret, err 126 | } 127 | defer resp.Body.Close() 128 | 129 | respBody, err := ioutil.ReadAll(resp.Body) 130 | if err != nil { 131 | return ret, err 132 | } 133 | //根据实际需要,返回相应的信息 134 | return respBody, nil 135 | } 136 | -------------------------------------------------------------------------------- /utils/httpclient/httpclient.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "io/ioutil" 5 | "math/rand" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type HttpResponse struct { 12 | StatusCode int 13 | Header http.Header 14 | Body []byte 15 | } 16 | 17 | func SendRequest(method string, url string, header map[string]string, body string) (HttpResponse, error) { 18 | client := &http.Client{} 19 | var res HttpResponse 20 | var request *http.Request 21 | var err error 22 | if method == "POST" { 23 | request, err = http.NewRequest(method, url, strings.NewReader(body)) 24 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 25 | } else if method == "PUT" { 26 | request, err = http.NewRequest(method, url, strings.NewReader(body)) 27 | } else { 28 | request, err = http.NewRequest(method, url, nil) 29 | } 30 | 31 | if err != nil { 32 | return res, err 33 | } 34 | 35 | for k, v := range header { 36 | if k == "host" { 37 | request.Host = v 38 | } else { 39 | request.Header.Set(k, v) 40 | } 41 | } 42 | response, err := client.Do(request) 43 | if response != nil { 44 | res.StatusCode = response.StatusCode 45 | res.Header = response.Header 46 | } 47 | if err != nil { 48 | return res, err 49 | } 50 | 51 | defer response.Body.Close() 52 | res.Body, err = ioutil.ReadAll(response.Body) 53 | if err != nil { 54 | return res, err 55 | } 56 | return res, nil 57 | } 58 | 59 | func Post(url string, header map[string]string, body string) (HttpResponse, error) { 60 | return SendRequest("POST", url, header, body) 61 | } 62 | 63 | func Put(url string, header map[string]string, body string) (HttpResponse, error) { 64 | return SendRequest("PUT", url, header, body) 65 | } 66 | 67 | func Get(url string, header map[string]string) (HttpResponse, error) { 68 | return SendRequest("GET", url, header, "") 69 | } 70 | 71 | func Head(url string, header map[string]string) (HttpResponse, error) { 72 | return SendRequest("HEAD", url, header, "") 73 | } 74 | 75 | func Delete(url string, header map[string]string) (HttpResponse, error) { 76 | return SendRequest("DELETE", url, header,"") 77 | } 78 | 79 | // 随机获取User-Agent 80 | func GetRandomUserAgent() string { 81 | userAgentList := []string{ 82 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", 83 | "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", 84 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", 85 | "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", 86 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", 87 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", 88 | "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", 89 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", 90 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", 91 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)", 92 | "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", 93 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", 94 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)", 95 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", 96 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", 97 | "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", 98 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", 99 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", 100 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36", 101 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36", 102 | } 103 | userAgentCount := len(userAgentList) 104 | 105 | rand.Seed(time.Now().UnixNano()) 106 | randIndex := rand.Intn(userAgentCount) 107 | 108 | return userAgentList[randIndex] 109 | } -------------------------------------------------------------------------------- /file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/jsyzchen/pan/conf" 8 | "github.com/jsyzchen/pan/utils/httpclient" 9 | "log" 10 | "net/url" 11 | "strconv" 12 | ) 13 | 14 | const ( 15 | ListUri = "/rest/2.0/xpan/file?method=list" 16 | MetasUri = "/rest/2.0/xpan/multimedia?method=filemetas" 17 | StreamingUri = "/rest/2.0/xpan/file?method=streaming" 18 | ) 19 | 20 | type ListResponse struct { 21 | conf.CloudDiskResponseBase 22 | List []struct { 23 | FsID uint64 `json:"fs_id"` 24 | Path string `json:"path"` 25 | ServerFileName string `json:"server_filename"` 26 | Size int `json:"size"` 27 | IsDir int `json:"isdir"` 28 | Category int `json:"category"` 29 | Md5 string `json:"md5"` 30 | DirEmpty string `json:"dir_empty"` 31 | Thumbs map[string]string `json:"thumbs"` 32 | LocalCtime int `json:"local_ctime"` 33 | LocalMtime int `json:"local_mtime"` 34 | ServerCtime int `json:"server_ctime"` 35 | ServerMtime int `json:"server_mtime"` 36 | } 37 | } 38 | 39 | type MetasResponse struct { 40 | ErrorCode int `json:"errno"` 41 | ErrorMsg string `json:"errmsg"` 42 | RequestID int 43 | RequestIDStr string `json:"request_id"` 44 | List []struct { 45 | FsID uint64 `json:"fs_id"` 46 | Path string `json:"path"` 47 | Category int `json:"category"` 48 | FileName string `json:"filename"` 49 | IsDir int `json:"isdir"` 50 | Size int `json:"size"` 51 | Md5 string `json:"md5"` 52 | DLink string `json:"dlink"` 53 | Thumbs map[string]string `json:"thumbs"` 54 | ServerCtime int `json:"server_ctime"` 55 | ServerMtime int `json:"server_mtime"` 56 | DateTaken int `json:"date_taken"` 57 | Width int `json:"width"` 58 | Height int `json:"height"` 59 | } 60 | } 61 | 62 | type ManagerResponse struct { 63 | conf.CloudDiskResponseBase 64 | Info []struct{ 65 | Path string 66 | TaskID int 67 | Errno int 68 | } 69 | } 70 | 71 | type File struct { 72 | AccessToken string 73 | } 74 | 75 | func NewFileClient(accessToken string) *File { 76 | return &File{ 77 | AccessToken: accessToken, 78 | } 79 | } 80 | 81 | // 获取文件列表 82 | func (f *File) List(dir string, start, limit int) (ListResponse, error) { 83 | ret := ListResponse{} 84 | 85 | v := url.Values{} 86 | v.Add("access_token", f.AccessToken) 87 | v.Add("dir", dir) 88 | v.Add("start", strconv.Itoa(start)) 89 | v.Add("limit", strconv.Itoa(limit)) 90 | query := v.Encode() 91 | 92 | requestUrl := conf.OpenApiDomain + ListUri + "&" + query 93 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 94 | if err != nil { 95 | log.Println("httpclient.Get failed, err:", err) 96 | return ret, err 97 | } 98 | 99 | if resp.StatusCode != 200 { 100 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 101 | } 102 | 103 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 104 | return ret, err 105 | } 106 | 107 | if ret.ErrorCode != 0 {//错误码不为0 108 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 109 | } 110 | 111 | return ret, nil 112 | } 113 | 114 | // 通过FsID获取文件信息 115 | func (f *File) Metas(fsIDs []uint64) (MetasResponse, error) { 116 | ret := MetasResponse{} 117 | 118 | fsIDsByte, err := json.Marshal(fsIDs) 119 | if err != nil { 120 | return ret, err 121 | } 122 | 123 | v := url.Values{} 124 | v.Add("access_token", f.AccessToken) 125 | v.Add("fsids", string(fsIDsByte)) 126 | v.Add("dlink", "1") 127 | v.Add("thumb", "1") 128 | v.Add("extra", "1") 129 | query := v.Encode() 130 | 131 | requestUrl := conf.OpenApiDomain + MetasUri + "&" + query 132 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 133 | if err != nil { 134 | log.Println("httpclient.Get failed, err:", err) 135 | return ret, err 136 | } 137 | 138 | if resp.StatusCode != 200 { 139 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 140 | } 141 | 142 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 143 | return ret, err 144 | } 145 | 146 | if ret.ErrorCode != 0 {//错误码不为0 147 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 148 | } 149 | 150 | ret.RequestID, err = strconv.Atoi(ret.RequestIDStr) 151 | if err != nil { 152 | return ret, err 153 | } 154 | 155 | return ret, nil 156 | } 157 | 158 | // 获取音视频在线播放地址,转码类型有M3U8_AUTO_480=>视频ts、M3U8_FLV_264_480=>视频flv、M3U8_MP3_128=>音频mp3、M3U8_HLS_MP3_128=>音频ts 159 | func (f *File) Streaming(path string, transcodingType string) (string, error) { 160 | ret := "" 161 | 162 | v := url.Values{} 163 | v.Add("access_token", f.AccessToken) 164 | v.Add("path", path) 165 | v.Add("type", transcodingType) 166 | query := v.Encode() 167 | 168 | requestUrl := conf.OpenApiDomain + StreamingUri + "&" + query 169 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 170 | if err != nil { 171 | log.Println("httpclient.Get failed, err:", err) 172 | return ret, err 173 | } 174 | 175 | if resp.StatusCode != 200 { 176 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 177 | } 178 | 179 | return string(resp.Body), nil 180 | } -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | //百度授权相关,wiki地址 https://openauth.baidu.com/doc/doc.html 2 | package auth 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/jsyzchen/pan/conf" 9 | "github.com/jsyzchen/pan/utils/httpclient" 10 | "log" 11 | "net/url" 12 | ) 13 | 14 | type Auth struct { 15 | ClientID string 16 | ClientSecret string 17 | } 18 | 19 | type AccessTokenResponse struct { 20 | AccessToken string `json:"access_token"` 21 | ExpiresIn int `json:"expires_in"` 22 | RefreshToken string `json:"refresh_token"` 23 | Scope string `json:"scope"` 24 | SessionKey string `json:"session_key"` 25 | SessionSecret string `json:"session_secret"` 26 | Error string `json:"error"` 27 | ErrorDescription string `json:"error_description"` 28 | } 29 | 30 | type RefreshTokenResponse struct { 31 | AccessToken string `json:"access_token"` 32 | ExpiresIn int `json:"expires_in"` 33 | RefreshToken string `json:"refresh_token"` 34 | Scope string `json:"scope"` 35 | SessionKey string `json:"session_key"` 36 | SessionSecret string `json:"session_secret"` 37 | Error string `json:"error"` 38 | ErrorDescription string `json:"error_description"` 39 | } 40 | 41 | type UserInfoResponse struct { 42 | OpenID string `json:"openid"` 43 | UnionID string `json:"unionid"` // 百度用户统一标识,对当前开发者帐号唯一 44 | UserID string `json:"userid"` // 老版百度用户的唯一标识,后续不在返回该字段,user_id字段对应account.UserInfo方法返回的uk 45 | UserName string `json:"username"` 46 | SecureMobile int `json:"securemobile"` // 当前用户绑定手机号,需要向百度开放平台单独申请权限 47 | Portrait string `json:"portrait"` 48 | UserDetail string `json:"userdetail"` 49 | Birthday string `json:"birthday"` 50 | Marriage string `json:"marriage"` 51 | Sex string `json:"sex"` 52 | Blood string `json:"blood"` 53 | IsBindMobile string `json:"is_bind_mobile"` 54 | IsRealName string `json:"is_realname"` 55 | ErrorCode int `json:"errno"` 56 | ErrorMsg string `json:"errmsg"` 57 | } 58 | 59 | const OAuthUri = "/oauth/2.0/authorize" 60 | const OAuthTokenUri = "/oauth/2.0/token" 61 | const UserInfoUri = "/rest/2.0/passport/users/getInfo" 62 | 63 | func NewAuthClient(clientID string, clientSecret string) *Auth { 64 | return &Auth{ 65 | ClientID: clientID, 66 | ClientSecret: clientSecret, 67 | } 68 | } 69 | 70 | // 获取授权页网址 71 | func (a *Auth) OAuthUrl(redirectUri string) string { 72 | oAuthUrl := "" 73 | 74 | v := url.Values{} 75 | v.Add("response_type", "code") 76 | v.Add("client_id", a.ClientID) 77 | v.Add("redirect_uri", redirectUri) 78 | v.Add("scope", "basic,netdisk") 79 | v.Add("state", "STATE") 80 | query := v.Encode() 81 | 82 | oAuthUrl = conf.BaiduOpenApiDomain + OAuthUri + "?" + query 83 | 84 | return oAuthUrl 85 | } 86 | 87 | // 获取AccessToken 88 | func (a *Auth) AccessToken(code, redirectUri string) (AccessTokenResponse, error) { 89 | ret := AccessTokenResponse{} 90 | 91 | v := url.Values{} 92 | v.Add("grant_type", "authorization_code") 93 | v.Add("code", code) 94 | v.Add("client_id", a.ClientID) 95 | v.Add("client_secret", a.ClientSecret) 96 | v.Add("redirect_uri", redirectUri) 97 | query := v.Encode() 98 | 99 | requestUrl := conf.BaiduOpenApiDomain + OAuthTokenUri + "?" + query 100 | 101 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 102 | if err != nil { 103 | log.Println("httpclient.Get failed, err:", err) 104 | return ret, err 105 | } 106 | 107 | if resp.StatusCode != 200 { 108 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, resp.Body)) 109 | } 110 | 111 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 112 | return ret, err 113 | } 114 | 115 | if ret.Error != "" {//有错误 116 | return ret, errors.New(ret.ErrorDescription) 117 | } 118 | 119 | return ret, nil 120 | } 121 | 122 | // 刷新AccessToken 123 | func (a *Auth) RefreshToken(refreshToken string) (RefreshTokenResponse, error) { 124 | ret := RefreshTokenResponse{} 125 | 126 | v := url.Values{} 127 | v.Add("grant_type", "refresh_token") 128 | v.Add("refresh_token", refreshToken) 129 | v.Add("client_id", a.ClientID) 130 | v.Add("client_secret", a.ClientSecret) 131 | query := v.Encode() 132 | 133 | requestUrl := conf.BaiduOpenApiDomain + OAuthTokenUri + "?" + query 134 | 135 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 136 | if err != nil { 137 | log.Println("httpclient.Get failed, err:", err) 138 | return ret, err 139 | } 140 | 141 | if resp.StatusCode != 200 { 142 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 143 | } 144 | 145 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 146 | return ret, err 147 | } 148 | 149 | if ret.Error != "" {//有错误 150 | return ret, errors.New(ret.ErrorDescription) 151 | } 152 | 153 | return ret, nil 154 | } 155 | 156 | // 获取授权用户的百度账号信息,可以通过unionid字段来识别多个百度产品授权的是否是同一用户 157 | // 注:获取网盘账号信息请使用account.UserInfo方法 158 | func (a *Auth) UserInfo(accessToken string) (UserInfoResponse, error) { 159 | ret := UserInfoResponse{} 160 | 161 | v := url.Values{} 162 | v.Add("access_token", accessToken) 163 | v.Add("get_unionid", "1")//需要获取unionid时,传递get_unionid = 1 164 | query := v.Encode() 165 | 166 | requestUrl := conf.BaiduOpenApiDomain + UserInfoUri + "?" + query 167 | 168 | resp, err := httpclient.Get(requestUrl, map[string]string{}) 169 | if err != nil { 170 | log.Println("httpclient.Get failed, err:", err) 171 | return ret, err 172 | } 173 | 174 | if resp.StatusCode != 200 { 175 | return ret, errors.New(fmt.Sprintf("HttpStatusCode is not equal to 200, httpStatusCode[%d], respBody[%s]", resp.StatusCode, string(resp.Body))) 176 | } 177 | 178 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 179 | return ret, err 180 | } 181 | 182 | if ret.ErrorCode != 0 {//有错误 183 | return ret, errors.New(ret.ErrorMsg) 184 | } 185 | 186 | return ret, nil 187 | } -------------------------------------------------------------------------------- /utils/file/download.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "math" 10 | "net/http" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "strconv" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | //FileDownloader 文件下载器 20 | type Downloader struct { 21 | FileSize int 22 | Link string 23 | FilePath string 24 | TotalPart int //下载线程 25 | DoneFilePart []Part 26 | PartSize int 27 | PartCoroutineNum int //分片下载协程数 28 | } 29 | 30 | //filePart 文件分片 31 | type Part struct { 32 | Index int //文件分片的序号 33 | From int //开始byte 34 | To int //解决byte 35 | Data []byte //http下载得到的文件内容 36 | FilePath string //下载到本地的分片文件路径 37 | } 38 | 39 | //NewFileDownloader . 40 | func NewFileDownloader(downloadLink, filePath string) *Downloader { 41 | return &Downloader{ 42 | FileSize: 0, 43 | Link: downloadLink, 44 | FilePath: filePath, 45 | PartSize: 10485760,// 10M 46 | PartCoroutineNum: 1, 47 | } 48 | } 49 | 50 | func (d *Downloader) SetTotalPart(totalPart int) { 51 | d.TotalPart = totalPart 52 | } 53 | 54 | func (d *Downloader) SetPartSize(partSize int) { 55 | d.PartSize = partSize 56 | } 57 | 58 | func (d *Downloader) SetCoroutineNum(partCoroutineNum int) { 59 | d.PartCoroutineNum = partCoroutineNum 60 | } 61 | 62 | //Run 开始下载任务 63 | func (d *Downloader) Download() error { 64 | if d.TotalPart == 1 { 65 | err := d.downloadWhole() 66 | return err 67 | } 68 | isSupportRange, err := d.head() 69 | if err != nil { 70 | return err 71 | } 72 | log.Println("isSupportRange:", isSupportRange) 73 | 74 | fileTotalSize := d.FileSize 75 | if d.PartSize == 0 { 76 | d.PartSize = 10485760 // 10M 77 | } 78 | 79 | log.Println("fileTotalSize:", fileTotalSize) 80 | 81 | if isSupportRange == false || fileTotalSize <= d.PartSize {//不支持Range下载或者文件比较小,直接下载文件 82 | err := d.downloadWhole() 83 | return err 84 | } 85 | 86 | log.Println("downloadPart") 87 | 88 | if d.TotalPart == 0 || fileTotalSize / d.PartSize < d.TotalPart {//减少range请求次数 89 | d.TotalPart = int(math.Ceil(float64(fileTotalSize) / float64(d.PartSize))) 90 | } 91 | maxTotalPart := 100 92 | if d.TotalPart > maxTotalPart {//限制分片数量 93 | d.TotalPart = maxTotalPart 94 | } 95 | 96 | log.Println("TotalPart:", d.TotalPart) 97 | 98 | d.DoneFilePart = make([]Part, d.TotalPart) 99 | jobs := make([]Part, d.TotalPart) 100 | eachSize := fileTotalSize / d.TotalPart 101 | 102 | log.Println("eachSize:", eachSize) 103 | 104 | for i := range jobs { 105 | jobs[i].Index = i 106 | if i == 0 { 107 | jobs[i].From = 0 108 | } else { 109 | jobs[i].From = jobs[i-1].To + 1 110 | } 111 | if i < d.TotalPart - 1 { 112 | jobs[i].To = jobs[i].From + eachSize 113 | } else { 114 | //the last filePart 115 | jobs[i].To = fileTotalSize - 1 116 | } 117 | } 118 | 119 | // 删除临时文件 120 | defer d.removePartFiles() 121 | 122 | var wg sync.WaitGroup 123 | isFailed := false 124 | partCoroutineNum := d.PartCoroutineNum 125 | if len(jobs) < partCoroutineNum { 126 | partCoroutineNum = len(jobs) 127 | } 128 | sem := make(chan int, partCoroutineNum) //限制并发数,以防大文件下载导致占用服务器大量网络宽带和磁盘io 129 | for _, job := range jobs { 130 | wg.Add(1) 131 | sem <- 1 //当通道已满的时候将被阻塞 132 | go func(job Part) { 133 | defer wg.Done() 134 | err := d.downloadPart(job) 135 | if err != nil { 136 | log.Println("下载文件失败:", err, job) 137 | isFailed = true //TODO 可能会有问题 138 | } 139 | <-sem 140 | }(job) 141 | } 142 | wg.Wait() 143 | if isFailed == true { 144 | log.Println("下载文件失败") 145 | return errors.New("downloadPart failed") 146 | } 147 | 148 | return d.mergeFileParts() 149 | } 150 | 151 | //head 获取要下载的文件的基本信息(header) 使用HTTP Method Head 152 | func (d *Downloader) head() (bool, error) { 153 | isSupportRange := false 154 | r, err := d.getNewRequest("HEAD") 155 | if err != nil { 156 | return isSupportRange, err 157 | } 158 | resp, err := http.DefaultClient.Do(r) 159 | if err != nil { 160 | return isSupportRange, err 161 | } 162 | if resp.StatusCode > 299 { 163 | return isSupportRange, errors.New(fmt.Sprintf("Can't process, response is %v", resp)) 164 | } 165 | //检查是否支持 断点续传 166 | if resp.Header.Get("Accept-Ranges") == "bytes" { 167 | isSupportRange = true 168 | } 169 | 170 | //获取文件大小 171 | contentLength, err := strconv.Atoi(resp.Header.Get("Content-Length")) 172 | if err != nil { 173 | return isSupportRange, err 174 | } 175 | d.FileSize = contentLength 176 | 177 | return isSupportRange, nil 178 | } 179 | 180 | //下载分片 181 | func (d *Downloader) downloadPart(c Part) error { 182 | r, err := d.getNewRequest("GET") 183 | if err != nil { 184 | return err 185 | } 186 | log.Printf("开始[%d]下载from:%d to:%d\n", c.Index, c.From, c.To) 187 | r.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", c.From, c.To)) 188 | resp, err := http.DefaultClient.Do(r) 189 | if err != nil { 190 | return err 191 | } 192 | defer resp.Body.Close() 193 | bs, err := ioutil.ReadAll(resp.Body) 194 | if resp.StatusCode > 299 { 195 | log.Println(fmt.Sprintf("服务器错误,状态码: %v, msg:%s", resp.StatusCode, string(bs))) 196 | return errors.New(fmt.Sprintf("服务器错误,状态码: %v, msg:%s", resp.StatusCode, string(bs))) 197 | } 198 | 199 | if err != nil { 200 | if err != io.EOF && err != io.ErrUnexpectedEOF {//unexpected EOF 处理 201 | log.Println("ioutil.ReadAll error :", err) 202 | return err 203 | } 204 | } 205 | 206 | if len(bs) != (c.To - c.From + 1) { 207 | return errors.New(fmt.Sprintf("下载文件分片长度错误, len bs:%d", len(bs))) 208 | } 209 | //c.Data = bs 210 | 211 | //分片文件写入到本地临时目录 212 | fileName := path.Base(d.FilePath) 213 | fileNamePrefix := fileName[0:len(path.Base(d.FilePath)) - len(path.Ext(d.FilePath))] 214 | nowTime := time.Now().UnixNano() / 1e6 215 | partFilePath := path.Join(os.TempDir(), fileNamePrefix + "_" + strconv.Itoa(c.Index) + "_" + strconv.FormatInt(nowTime, 10)) 216 | 217 | log.Printf("partFilePath[%d]:%s", c.Index, partFilePath) 218 | 219 | f, err := os.Create(partFilePath) 220 | if err != nil { 221 | log.Println("open file error :", err) 222 | return err 223 | } 224 | 225 | // 关闭文件 226 | defer f.Close() 227 | // 字节方式写入 228 | _, err = f.Write(bs) 229 | if err != nil { 230 | log.Println(err) 231 | return err 232 | } 233 | 234 | c.FilePath = partFilePath 235 | 236 | d.DoneFilePart[c.Index] = c 237 | 238 | log.Printf("结束[%d]下载from:%d to:%d\n", c.Index, c.From, c.To) 239 | return nil 240 | } 241 | 242 | //mergeFileParts 合并下载的文件 243 | func (d *Downloader) mergeFileParts() error { 244 | log.Println("开始合并文件") 245 | 246 | //存储文件夹不存在的话先创建文件夹 247 | fileDir := filepath.Dir(d.FilePath) 248 | _, err := os.Stat(fileDir) 249 | if err != nil { 250 | if os.IsNotExist(err){ 251 | //递归创建文件夹 252 | err := os.MkdirAll(fileDir, os.ModePerm) 253 | if err != nil{ 254 | log.Println("MkdirAll failed:", err) 255 | return err 256 | } 257 | } 258 | } 259 | 260 | mergedFile, err := os.Create(d.FilePath) 261 | if err != nil { 262 | return err 263 | } 264 | defer mergedFile.Close() 265 | totalSize := 0 266 | for _, s := range d.DoneFilePart { 267 | data, err := ioutil.ReadFile(s.FilePath) 268 | if err != nil { 269 | log.Println("ioutil.ReadFile err:", err) 270 | return err 271 | } 272 | 273 | mergedFile.Write(data) 274 | totalSize += len(data) 275 | } 276 | if totalSize != d.FileSize { 277 | return errors.New("文件不完整") 278 | } 279 | return nil 280 | } 281 | 282 | // 删除临时文件 283 | func (d *Downloader) removePartFiles() { 284 | var wg sync.WaitGroup 285 | for _, s := range d.DoneFilePart { 286 | if s.FilePath != "" { 287 | wg.Add(1) 288 | go func (filePath string) { 289 | defer wg.Done() 290 | if err := os.Remove(filePath); err != nil { 291 | log.Println(filePath, "remove failed, err:", err) 292 | } 293 | }(s.FilePath) 294 | } 295 | } 296 | wg.Wait() 297 | } 298 | 299 | //直接下载整个文件 300 | func (d *Downloader) downloadWhole() error { 301 | log.Println("downloadWhole") 302 | 303 | // Get the data 304 | r, err := d.getNewRequest("GET") 305 | if err != nil { 306 | return err 307 | } 308 | resp, err := http.DefaultClient.Do(r) 309 | if err != nil { 310 | return err 311 | } 312 | defer resp.Body.Close() 313 | 314 | // 创建一个文件用于保存 315 | out, err := os.Create(d.FilePath) 316 | if err != nil { 317 | return err 318 | } 319 | defer out.Close() 320 | 321 | // 然后将响应流和文件流对接起来 322 | _, err = io.Copy(out, resp.Body) 323 | if err != nil { 324 | return err 325 | } 326 | 327 | return nil 328 | } 329 | 330 | // getNewRequest 创建一个request 331 | func (d *Downloader) getNewRequest(method string) (*http.Request, error) { 332 | r, err := http.NewRequest( 333 | method, 334 | d.Link, 335 | nil, 336 | ) 337 | if err != nil { 338 | return nil, err 339 | } 340 | 341 | r.Header.Set("User-Agent", "pan.baidu.com") 342 | return r, nil 343 | } -------------------------------------------------------------------------------- /file/upload.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/bitly/go-simplejson" 10 | "github.com/jsyzchen/pan/account" 11 | "github.com/jsyzchen/pan/conf" 12 | fileUtil "github.com/jsyzchen/pan/utils/file" 13 | "github.com/jsyzchen/pan/utils/httpclient" 14 | "github.com/syyongx/php2go" 15 | "io" 16 | "log" 17 | "math" 18 | "net/url" 19 | "os" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | type UploadResponse struct { 25 | conf.CloudDiskResponseBase 26 | Path string `json:"path"` 27 | Size int `json:"size"` 28 | Ctime int `json:"ctime"` 29 | Mtime int `json:"mtime"` 30 | Md5 string `json:"md5"` 31 | FsID uint64 `json:"fs_id"` 32 | IsDir int `json:"isdir"` 33 | } 34 | 35 | type PreCreateResponse struct { 36 | conf.CloudDiskResponseBase 37 | UploadID string `json:"uploadid"` 38 | Path string `json:"path"` 39 | ReturnType int `json:"return_type"` 40 | BlockList []int `json:"block_list"` 41 | Info UploadResponse `json:"info"` 42 | } 43 | 44 | type SuperFile2UploadResponse struct { 45 | conf.PcsResponseBase 46 | Md5 string `json:"md5"` 47 | UploadID string `json:"uploadid"` 48 | PartSeq string `json:"partseq"`//pcsapi PHP版本返回的是int类型,Go版本返回的是string类型 49 | } 50 | 51 | type LocalFileInfo struct { 52 | Md5 string 53 | Size int64 54 | } 55 | 56 | type Uploader struct { 57 | AccessToken string 58 | Path string 59 | LocalFilePath string 60 | } 61 | 62 | const ( 63 | PreCreateUri = "/rest/2.0/xpan/file?method=precreate" 64 | CreateUri = "/rest/2.0/xpan/file?method=create" 65 | Superfile2UploadUri = "/rest/2.0/pcs/superfile2?method=upload" 66 | ) 67 | 68 | func NewUploader(accessToken, path, localFilePath string) *Uploader { 69 | return &Uploader{ 70 | AccessToken: accessToken, 71 | Path: handleSpecialChar(path),// 处理特殊字符 72 | LocalFilePath: localFilePath, 73 | } 74 | } 75 | 76 | // 上传文件到网盘,包括预创建、分片上传、创建3个步骤 77 | func (u *Uploader) Upload() (UploadResponse, error) { 78 | var ret UploadResponse 79 | 80 | //1. file precreate 81 | preCreateRes, err := u.PreCreate() 82 | if err != nil { 83 | log.Println("PreCreate failed, err:", err) 84 | ret.ErrorCode = preCreateRes.ErrorCode 85 | ret.ErrorMsg = preCreateRes.ErrorMsg 86 | ret.RequestID = preCreateRes.RequestID 87 | return ret, err 88 | } 89 | 90 | if preCreateRes.ReturnType == 2 {//云端已存在相同文件,直接上传成功,无需请求后面的分片上传和创建文件接口 91 | preCreateRes.Info.ErrorCode = preCreateRes.ErrorCode 92 | preCreateRes.Info.ErrorMsg = preCreateRes.ErrorMsg 93 | preCreateRes.Info.RequestID = preCreateRes.RequestID 94 | return preCreateRes.Info, nil 95 | } 96 | 97 | uploadID := preCreateRes.UploadID 98 | 99 | //2. superfile2 upload 100 | fileInfo, err := u.getFileInfo() 101 | if err != nil { 102 | log.Println("getFileInfo failed, err:", err) 103 | return ret, err 104 | } 105 | fileSize := fileInfo.Size 106 | 107 | sliceSize, err := u.getSliceSize(fileSize) 108 | if err != nil { 109 | log.Println("getSliceSize failed, err:", err) 110 | return ret, err 111 | } 112 | 113 | sliceNum := int(math.Ceil(float64(fileSize) / float64(sliceSize))) 114 | 115 | //TODO 断点续传 116 | file, err := os.Open(u.LocalFilePath) 117 | if err != nil { 118 | return ret, err 119 | } 120 | defer file.Close() 121 | uploadRespChan := make(chan SuperFile2UploadResponse, sliceNum) 122 | sem := make(chan int, 10) //限制并发数,以防大文件上传导致占用服务器大量内存 123 | for i := 0; i < sliceNum; i++ { 124 | buffer := make([]byte, sliceSize) 125 | n, err := file.Read(buffer[:]) 126 | if err != nil && err != io.EOF { 127 | log.Println("file.Read failed, err:", err) 128 | return ret, err 129 | } 130 | if n == 0 { //文件已读取结束 131 | break 132 | } 133 | 134 | sem <- 1 //当通道已满的时候将被阻塞 135 | go func(partSeq int, partByte []byte) { 136 | uploadResp, err := u.SuperFile2Upload(uploadID, partSeq, partByte) 137 | uploadRespChan <- uploadResp 138 | if err != nil { 139 | log.Printf("SuperFile2UploadFailed, partseq[%d] err[%v]", partSeq, err) 140 | } 141 | <-sem 142 | }(i, buffer[0:n]) 143 | } 144 | 145 | blockList := make([]string, sliceNum) 146 | for i := 0; i < sliceNum; i++ { 147 | uploadResp := <-uploadRespChan 148 | if uploadResp.ErrorCode != 0 {//有部分文件上传失败 149 | log.Print("superfile2 upload part failed") 150 | ret.ErrorCode = uploadResp.ErrorCode 151 | ret.ErrorMsg = uploadResp.ErrorMsg 152 | ret.RequestID = uploadResp.RequestID 153 | return ret, errors.New("superfile2 upload part failed") 154 | } 155 | 156 | partSeq, err := strconv.Atoi(uploadResp.PartSeq) 157 | if err != nil { 158 | log.Println("strconv.Atoi failed, err:", err) 159 | return ret, err 160 | } 161 | 162 | blockList[partSeq] = uploadResp.Md5 163 | } 164 | 165 | //3. file create 166 | superFile2CommitRes, err := u.Create(uploadID, blockList) 167 | if err != nil { 168 | log.Println("SuperFile2Commit failed, err:", err) 169 | return superFile2CommitRes, err 170 | } 171 | 172 | return superFile2CommitRes, err 173 | } 174 | 175 | // preCreate 176 | func (u *Uploader) PreCreate() (PreCreateResponse, error) { 177 | ret := PreCreateResponse{} 178 | 179 | fileInfo, err := u.getFileInfo() 180 | if err != nil { 181 | log.Println("getFileInfo failed, err:", err) 182 | return ret, err 183 | } 184 | fileSize := fileInfo.Size 185 | fileMd5 := fileInfo.Md5 186 | 187 | sliceMd5, err := u.getSliceMd5() 188 | if err != nil { 189 | log.Println("getSliceMd5 failed, err:", err) 190 | return ret, err 191 | } 192 | 193 | blockList, err := u.getBlockList() 194 | if err != nil { 195 | log.Println("getBlockList failed, err:", err) 196 | return ret, err 197 | } 198 | blockListByte, err := json.Marshal(blockList) 199 | if err != nil { 200 | return ret, err 201 | } 202 | blockListStr := string(blockListByte) 203 | 204 | // path urlencode 205 | v := url.Values{} 206 | v.Add("path", u.Path) 207 | v.Add("size", strconv.FormatInt(fileSize, 10)) 208 | v.Add("isdir", "0") 209 | v.Add("autoinit", "1")// 固定值1 210 | v.Add("rtype", "1")// 1 为只要path冲突即重命名 211 | v.Add("block_list", blockListStr) 212 | v.Add("content-md5", fileMd5) 213 | v.Add("slice-md5", sliceMd5) 214 | body := v.Encode() 215 | 216 | requestUrl := conf.OpenApiDomain + PreCreateUri + "&access_token=" + u.AccessToken 217 | headers := make(map[string]string) 218 | resp, err := httpclient.Post(requestUrl, headers, body) 219 | if err != nil { 220 | log.Println("httpclient.Post failed, err:", err) 221 | return ret, err 222 | } 223 | 224 | respBody := resp.Body 225 | if js, err := simplejson.NewJson(respBody); err == nil { 226 | if info, isExist := js.CheckGet("info"); isExist {//秒传返回的request_id有可能是科学计数法,这里将它统一转成uint64 227 | //{"return_type":2,"errno":0,"info":{"size":16877488,"category":4,"fs_id":714504460793248,"request_id":1.821160071156e+17,"path":"\/apps\/\u4e66\u68af\/easy_20210726_163824.pptx","isdir":0,"mtime":1627288705,"ctime":1627288705,"md5":"44090321ds594263c8818d7c398e5017"},"request_id":182116007115598010} 228 | info.Set("request_id", uint64(info.Get("request_id").MustFloat64())) 229 | if respBody, err = js.Encode(); err != nil { 230 | log.Println("simplejson Encode failed, err:", err) 231 | return ret, err 232 | } 233 | } 234 | } 235 | 236 | if err := json.Unmarshal(respBody, &ret); err != nil { 237 | log.Println("json.Unmarshal failed, err:", err) 238 | return ret, err 239 | } 240 | 241 | if ret.ErrorCode != 0 {//错误码不为0 242 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 243 | } 244 | 245 | return ret, nil 246 | } 247 | 248 | //superfile2 upload 249 | func (u *Uploader) SuperFile2Upload(uploadID string, partSeq int, partByte []byte) (SuperFile2UploadResponse, error) { 250 | ret := SuperFile2UploadResponse{} 251 | 252 | path := u.Path 253 | localFilePath := u.LocalFilePath 254 | 255 | // path urlencode 256 | v := url.Values{} 257 | v.Add("access_token", u.AccessToken) 258 | v.Add("path", path) 259 | v.Add("type", "tmpfile") 260 | v.Add("uploadid", uploadID) 261 | v.Add("partseq", strconv.Itoa(partSeq)) 262 | queryParams := v.Encode() 263 | 264 | uploadUrl := conf.PcsDataDomain + Superfile2UploadUri + "&" + queryParams 265 | 266 | fileUploader := fileUtil.NewFileUploader(uploadUrl, localFilePath) 267 | resp, err := fileUploader.UploadByByte(partByte) 268 | if err != nil { 269 | log.Print("fileUploader.UploadByByte failed") 270 | return ret, err 271 | } 272 | 273 | if err := json.Unmarshal(resp, &ret); err != nil { 274 | log.Printf("upload failed, path[%s] response[%s]", path, string(resp)) 275 | return ret, err 276 | } 277 | 278 | if ret.ErrorCode != 0 {//错误码不为0 279 | log.Printf("upload failed, path[%s] response[%s]", path, string(resp)) 280 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 281 | } 282 | 283 | return ret, nil 284 | } 285 | 286 | // file create 287 | func (u *Uploader) Create(uploadID string, blockList []string) (UploadResponse, error){ 288 | ret := UploadResponse{} 289 | 290 | fileInfo, err := u.getFileInfo() 291 | if err != nil { 292 | log.Println("getFileInfo failed, err:", err) 293 | return ret, err 294 | } 295 | 296 | blockListByte, err := json.Marshal(blockList) 297 | if err != nil { 298 | return ret, err 299 | } 300 | blockListStr := string(blockListByte) 301 | 302 | // path urlencode 303 | v := url.Values{} 304 | v.Add("path", u.Path) 305 | v.Add("uploadid", uploadID) 306 | v.Add("block_list", blockListStr) 307 | v.Add("size", strconv.FormatInt(fileInfo.Size, 10)) 308 | v.Add("isdir", "0") 309 | v.Add("rtype", "1")//1 为只要path冲突即重命名 310 | body := v.Encode() 311 | 312 | requestUrl := conf.OpenApiDomain + CreateUri + "&access_token=" + u.AccessToken 313 | 314 | headers := make(map[string]string) 315 | resp, err := httpclient.Post(requestUrl, headers, body) 316 | if err != nil { 317 | log.Println("httpclient.Post failed, err:", err) 318 | return ret, err 319 | } 320 | 321 | if err := json.Unmarshal(resp.Body, &ret); err != nil { 322 | log.Printf("json.Unmarshal failed, resp[%s], err[%v]", string(resp.Body), err) 323 | return ret, err 324 | } 325 | 326 | if ret.ErrorCode != 0 {//错误码不为0 327 | log.Println("file create failed, resp:", string(resp.Body)) 328 | return ret, errors.New(fmt.Sprintf("error_code:%d, error_msg:%s", ret.ErrorCode, ret.ErrorMsg)) 329 | } 330 | 331 | return ret, nil 332 | } 333 | 334 | // 获取分片的大小 335 | func (u *Uploader) getSliceSize(fileSize int64) (int64, error) { 336 | var sliceSize int64 337 | 338 | /* 339 | 限制: 340 | 普通用户单个分片大小固定为4MB(文件大小如果小于4MB,无需切片,直接上传即可),单文件总大小上限为4G。 341 | 普通会员用户单个分片大小上限为16MB,单文件总大小上限为10G。 342 | 超级会员用户单个分片大小上限为32MB,单文件总大小上限为20G。 343 | */ 344 | //切割文件,单个分片大小暂时先固定为4M,TODO 普通会员和超级会员单个分片可以更大,需判断用户的身份 345 | sliceSize = 4194304//4M 346 | accountClient := account.NewAccountClient(u.AccessToken) 347 | userInfo, err := accountClient.UserInfo() 348 | if err != nil {//获取失败直接用4M 349 | log.Println("account.UserInfo failed, err:", err) 350 | return sliceSize, nil 351 | } 352 | if userInfo.VipType == 1 {//普通会员 353 | sliceSize = 16777216//16M 354 | } else if userInfo.VipType == 2 {//超级会员 355 | sliceSize = 33554432//32M 356 | } 357 | 358 | if fileSize <= sliceSize {//无须切片 359 | sliceSize = fileSize 360 | } 361 | 362 | return sliceSize, nil 363 | } 364 | 365 | // 获取block_list 366 | func (u *Uploader) getBlockList() ([]string, error) { 367 | blockList := []string{} 368 | 369 | filePath := u.LocalFilePath 370 | 371 | fileInfo, err := u.getFileInfo() 372 | if err != nil { 373 | log.Println("getFileInfo failed, err:", err) 374 | return blockList, err 375 | } 376 | fileSize := fileInfo.Size 377 | fileMd5 := fileInfo.Md5 378 | 379 | sliceSize, err := u.getSliceSize(fileSize) 380 | if err != nil { 381 | log.Println("getSliceSize failed, err:", err) 382 | return blockList, err 383 | } 384 | 385 | if sliceSize == fileSize {//只有一个分片 386 | blockList = append(blockList, fileMd5) 387 | return blockList, nil 388 | } 389 | 390 | buffer := make([]byte, sliceSize) 391 | file, err := os.Open(filePath) 392 | if err != nil { 393 | return blockList, err 394 | } 395 | defer file.Close() 396 | 397 | for { 398 | n, err := file.Read(buffer[:]) 399 | if err != nil && err != io.EOF { 400 | log.Println("file.Read failed, err:", err) 401 | return blockList, err 402 | } 403 | if n == 0 { 404 | break 405 | } 406 | partBuffer := buffer[0:n] 407 | hash := md5.New() 408 | hash.Write(partBuffer) 409 | sliceMd5 := hex.EncodeToString(hash.Sum(nil)) 410 | blockList = append(blockList, sliceMd5) 411 | } 412 | 413 | return blockList, nil 414 | } 415 | 416 | // 获取文件信息 417 | func (u *Uploader) getFileInfo() (LocalFileInfo, error) { 418 | info := LocalFileInfo{} 419 | 420 | filePath := u.LocalFilePath 421 | file, err := os.Open(filePath) 422 | if err != nil { 423 | return info, err 424 | } 425 | defer file.Close() 426 | fileInfo, err := file.Stat() 427 | if err != nil { 428 | return info, err 429 | } 430 | 431 | info.Size= fileInfo.Size() 432 | 433 | fileMd5, err := php2go.Md5File(filePath) 434 | if err != nil { 435 | log.Println("php2go.Md5File failed, err:", err) 436 | return info, err 437 | } 438 | 439 | info.Md5 = fileMd5 440 | 441 | return info, nil 442 | } 443 | 444 | // 特殊字符处理,文件名里有特殊字符时无法上传到网盘,特殊字符有'\\', '?', '|', '"', '>', '<', ':', '*',"\t","\n","\r","\0","\x0B" 445 | func handleSpecialChar(char string) string { 446 | specialChars := []string{"\\\\", "?", "|", "\"", ">", "<", ":", "*","\t","\n","\r","\\0","\\x0B"} 447 | 448 | newChar := char 449 | for _, specialChar := range specialChars { 450 | newChar = strings.Replace(newChar, specialChar, "", -1) 451 | } 452 | 453 | if newChar != char { 454 | fmt.Printf("char has handle, origin[%s] handled[%s]", char, newChar) 455 | } 456 | 457 | return newChar 458 | } 459 | 460 | // 获取分片的md5值 461 | func (u *Uploader) getSliceMd5() (string, error) { 462 | var sliceMd5 string 463 | var sliceSize int64 464 | sliceSize = 262144//切割的块大小,固定为256KB 465 | 466 | filePath := u.LocalFilePath 467 | fileInfo, err := u.getFileInfo() 468 | if err != nil { 469 | log.Println("getFileInfo failed, err:", err) 470 | return sliceMd5, err 471 | } 472 | 473 | fileSize := fileInfo.Size 474 | fileMd5 := fileInfo.Md5 475 | 476 | if fileSize <= sliceSize { 477 | sliceMd5 = fileMd5 478 | } else { 479 | file, err := os.Open(filePath) 480 | if err != nil { 481 | return sliceMd5, err 482 | } 483 | defer file.Close() 484 | 485 | partBuffer := make([]byte, sliceSize) 486 | if _, err := file.Read(partBuffer); err == nil { 487 | hash := md5.New() 488 | hash.Write(partBuffer) 489 | sliceMd5 = hex.EncodeToString(hash.Sum(nil)) 490 | } 491 | } 492 | 493 | return sliceMd5, nil 494 | } --------------------------------------------------------------------------------