├── .gitignore ├── LICENSE ├── README.md ├── aliyunpan ├── album_entity.go ├── apierror │ ├── api_error.go │ ├── api_error_test.go │ ├── api_utils.go │ └── error_resp.go ├── apiutil │ ├── utils.go │ └── utils_test.go ├── file_api.go ├── file_download_entity.go ├── file_entity.go ├── file_share_entity.go ├── file_upload_entity.go └── user_api.go ├── aliyunpan_open ├── api_error.go ├── file_album.go ├── file_copy.go ├── file_delete.go ├── file_directory.go ├── file_download.go ├── file_mkdir.go ├── file_move.go ├── file_rename.go ├── file_share.go ├── file_upload.go ├── file_video.go ├── open_pan_client.go ├── openapi │ ├── ali_api_error.go │ ├── ali_pan_client.go │ ├── api_constant.go │ ├── aynsc_task_api.go │ ├── file_album_api.go │ ├── file_api.go │ ├── file_share.go │ ├── file_upload_api.go │ ├── user_api.go │ └── video_api.go ├── user_info.go └── util.go ├── aliyunpan_web ├── api_constant.go ├── app_login.go ├── async_task.go ├── batch_task.go ├── file_album.go ├── file_copy.go ├── file_delete.go ├── file_directory.go ├── file_download.go ├── file_move.go ├── file_recycle.go ├── file_rename.go ├── file_save.go ├── file_share.go ├── file_starred.go ├── file_upload.go ├── file_video.go ├── login.go ├── logout.go ├── mkdir.go ├── signature.go ├── user_info.go ├── util.go └── web_pan_client.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | out/ 13 | *.dl 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | 18 | # Others 19 | .DS_Store 20 | *.proc 21 | *.txt 22 | *.log 23 | *.gz 24 | captcha.png 25 | aliyunpan_config.json 26 | aliyunpan_command_history.txt 27 | aliyunpan_uploading.json 28 | test/ 29 | download/ 30 | *-downloading 31 | 32 | # GoLand 33 | .idea/ 34 | 35 | demos/ 36 | userpw.txt 37 | test_test.go 38 | git_push.sh 39 | git_pull.sh 40 | my_test.go 41 | open_client_test.go 42 | open_test.go -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aliyunpan-api 2 | GO语言封装的 aliyunpan 阿里云盘官方OpenAPI接口和网页端Web接口。你可以基于该接口库实现对阿里云盘的二次开发。 3 | 两种接口都可以实现对阿里云盘的文件访问,但是由于阿里OpenAPI目前接口开放有限,有部分功能只有web端接口具备,例如:分享、相册等。你可以根据需要自行选择,也可以两者融合使用。 4 | 5 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/tickstep/aliyunpan-api?tab=doc) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/modern-go/concurrent/master/LICENSE) 7 | 8 | # 关于登录Token 9 | 阿里官方OpenAPI的登录和网页版Web登录token是不一样的,方式也不一样,本仓库并没有这部分代码,你需要自行实现。 10 | 1. 阿里官方OpenAPI的登录token,可以参考官方文档:https://www.yuque.com/aliyundrive/zpfszx/btw0tw 11 | 2. 网页版Web的登录token,可以通过使用浏览器获取到的RefreshToken进行获取,样例如下: 12 | ``` 13 | // get access token 14 | refreshToken := "f34b54eba1...706f389" 15 | webToken, err := aliyunpan_web.GetAccessTokenFromRefreshToken(refreshToken) 16 | if err != nil { 17 | fmt.Println("get web acccess token error") 18 | return 19 | } 20 | ``` 21 | 22 | # 快速使用教程 23 | ## 阿里官方OpenAPI接口 24 | 导入包 25 | ``` 26 | import "github.com/tickstep/aliyunpan-api/aliyunpan" 27 | import "github.com/tickstep/aliyunpan-api/aliyunpan_open" 28 | ``` 29 | 30 | 使用授权登录后得到的AccessToken创建OpenPanClient实例 31 | ``` 32 | openPanClient := aliyunpan_open.NewOpenPanClient(openapi.ApiConfig{ 33 | TicketId: "", 34 | UserId: "", 35 | ClientId: "", 36 | ClientSecret: "", 37 | }, openapi.ApiToken{ 38 | AccessToken: "eyJraWQiOiJLcU8iLC...jIUeqP9mZGZDrFLN--h1utcyVc", 39 | ExpiredAt: 1709527182, 40 | }, nil) 41 | ``` 42 | 43 | 调用OpenPanClient相关方法可以实现对阿里云盘的相关操作 44 | ``` 45 | // get user info 46 | ui, err := openPanClient.GetUserInfo() 47 | if err != nil { 48 | fmt.Println("get user info error") 49 | return 50 | } 51 | fmt.Println("当前登录用户:" + ui.Nickname) 52 | 53 | // do some file operation 54 | fi, _ := openPanClient.FileInfoByPath(ui.FileDriveId, "/我的文档") 55 | fmt.Println("\n我的文档 信息:") 56 | fmt.Println(fi) 57 | ``` 58 | 59 | ## 网页版Web端接口 60 | 导入包 61 | ``` 62 | import "github.com/tickstep/aliyunpan-api/aliyunpan" 63 | import "github.com/tickstep/aliyunpan-api/aliyunpan_web" 64 | ``` 65 | 66 | 使用浏览器获取到的RefreshToken创建WebPanClient实例 67 | ``` 68 | // get access token 69 | refreshToken := "f34b54eba1...706f389" 70 | webToken, err := aliyunpan_web.GetAccessTokenFromRefreshToken(refreshToken) 71 | if err != nil { 72 | fmt.Println("get acccess token error") 73 | return 74 | } 75 | 76 | // web pan client 77 | appConfig := aliyunpan_web.AppConfig{ 78 | AppId: "25dzX3vbYqktVxyX", 79 | DeviceId: "T6ZJyY7JqX6EN2cDzLCxMVYZ", 80 | UserId: "", 81 | Nonce: 0, 82 | PublicKey: "", 83 | } 84 | webPanClient := aliyunpan_web.NewWebPanClient(*webToken, aliyunpan_web.AppLoginToken{}, appConfig, aliyunpan_web.SessionConfig{ 85 | DeviceName: "Chrome浏览器", 86 | ModelName: "Windows网页版", 87 | }) 88 | 89 | // create session 90 | webPanClient.CreateSession(&aliyunpan_web.CreateSessionParam{ 91 | DeviceName: "Chrome浏览器", 92 | ModelName: "Windows网页版", 93 | }) 94 | 95 | ``` 96 | 97 | 调用WebPanClient相关方法可以实现对阿里云盘的相关操作 98 | ``` 99 | // get user info 100 | ui, err := webPanClient.GetUserInfo() 101 | if err != nil { 102 | fmt.Println("get user info error") 103 | return 104 | } 105 | fmt.Println("当前登录用户:" + ui.Nickname) 106 | 107 | // do some file operation 108 | fi, _ := webPanClient.FileInfoByPath(ui.FileDriveId, "/我的文档") 109 | fmt.Println("\n我的文档 信息:") 110 | fmt.Println(fi) 111 | ``` 112 | 113 | # 链接 114 | > [tickstep/aliyunpan](https://github.com/tickstep/aliyunpan) 115 | > [阿里OpenAPI文档](https://www.yuque.com/aliyundrive/zpfszx/btw0tw) -------------------------------------------------------------------------------- /aliyunpan/album_entity.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | import "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 4 | 5 | type ( 6 | // AlbumEntity 相薄实体 7 | AlbumEntity struct { 8 | Owner string `json:"owner"` 9 | Name string `json:"name"` 10 | Description string `json:"description"` 11 | AlbumId string `json:"album_id"` 12 | FileCount int `json:"file_count"` 13 | ImageCount int `json:"image_count"` 14 | VideoCount int `json:"video_count"` 15 | CreatedAt int64 `json:"created_at"` 16 | UpdatedAt int64 `json:"updated_at"` 17 | IsSharing bool `json:"is_sharing"` 18 | } 19 | 20 | ShareAlbumList []*AlbumEntity 21 | 22 | // ShareAlbumListFileParam 获取共享相册文件列表参数 23 | ShareAlbumListFileParam struct { 24 | // AlbumId 共享相册唯一ID 25 | AlbumId string `json:"albumId"` 26 | // Marker 分页标记 27 | Marker string `json:"marker"` 28 | // Limit 返回文件数量,默认50 29 | Limit int `json:"limit"` 30 | // ImageThumbnailWidth 生成的图片缩略图宽度,默认480px 31 | ImageThumbnailWidth int `json:"image_thumbnail_width"` 32 | } 33 | 34 | // ShareAlbumGetFileUrlParam 获取共享相册下文件下载地址参数 35 | ShareAlbumGetFileUrlParam struct { 36 | // AlbumId 共享相册唯一ID 37 | AlbumId string `json:"albumId"` 38 | // DriveId 文件所属drive 39 | DriveId string `json:"drive_id"` 40 | // FileId 文件id 41 | FileId string `json:"file_id"` 42 | } 43 | // ShareAlbumGetFileUrlResult 获取共享相册下文件下载地址返回值 44 | ShareAlbumGetFileUrlResult struct { 45 | // Url 下载地址,如果文件是livp时为空 46 | Url string `json:"url"` 47 | // StreamsUrl 文件是livp时有值 48 | StreamsUrl *ShareAlbumFileStreamUrlItem `json:"streams_url"` 49 | // Expiration 下载地址有效时间 50 | Expiration string `json:"expiration"` 51 | // Size 文件大小 52 | Size int64 `json:"size"` 53 | // ContentHash 文件哈希 54 | ContentHash string `json:"content_hash"` 55 | } 56 | // ShareAlbumFileStreamUrlItem livp文件下载流地址 57 | ShareAlbumFileStreamUrlItem struct { 58 | // Heic livp图片,与 jpeg 不会同时返回。 59 | Heic string `json:"heic"` 60 | // Jpeg jpeg图片,与 heic 不会同时返回。 61 | Jpeg string `json:"jpeg"` 62 | // Mov livp动画 63 | Mov string `json:"mov"` 64 | } 65 | ) 66 | 67 | func (a *AlbumEntity) CreatedAtStr() string { 68 | return apiutil.UnixTime2LocalFormat(a.CreatedAt) 69 | } 70 | func (a *AlbumEntity) UpdatedAtStr() string { 71 | return apiutil.UnixTime2LocalFormat(a.UpdatedAt) 72 | } 73 | -------------------------------------------------------------------------------- /aliyunpan/apierror/api_error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apierror 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io/ioutil" 21 | "net/http" 22 | "strings" 23 | ) 24 | 25 | const ( 26 | /* ------------------------------- 默认错误码 -------------------------------*/ 27 | 28 | // ApiCodeOk 成功 29 | ApiCodeOk ApiCode = 0 30 | // ApiCodeFailed 失败 31 | ApiCodeFailed ApiCode = 999 32 | 33 | /* ------------------------------- 系统错误码(800-899) -------------------------------*/ 34 | 35 | // ApiCodeNetError 网络错误 36 | ApiCodeNetError ApiCode = 800 37 | 38 | /* ------------------------------- 阿里云盘错误码(10-799) -------------------------------*/ 39 | 40 | // ApiCodeNeedCaptchaCode 验证码 41 | ApiCodeNeedCaptchaCode ApiCode = 10 42 | // ApiCodeTokenExpiredCode 会话/Token已过期 43 | ApiCodeTokenExpiredCode ApiCode = 11 44 | // ApiCodeFileNotFoundCode 文件不存在 NotFound.File / NotFound.FileId 45 | ApiCodeFileNotFoundCode ApiCode = 12 46 | // ApiCodeUploadFileStatusVerifyFailed 上传文件失败 47 | ApiCodeUploadFileStatusVerifyFailed = 13 48 | // ApiCodeUploadOffsetVerifyFailed 上传文件数据偏移值校验失败 49 | ApiCodeUploadOffsetVerifyFailed = 14 50 | // ApiCodeUploadFileNotFound 服务器上传文件不存在 51 | ApiCodeUploadFileNotFound = 15 52 | // ApiCodeFileAlreadyExisted 文件已存在 AlreadyExist.File 53 | ApiCodeFileAlreadyExisted = 16 54 | // ApiCodeUserDayFlowOverLimited 上传达到日数量上限 55 | ApiCodeUserDayFlowOverLimited = 17 56 | // ApiCodeAccessTokenInvalid Token无效或者已过期 AccessTokenInvalid,合并到ApiCodeTokenExpiredCode错误 57 | ApiCodeAccessTokenInvalid = 18 58 | // ApiCodeForbidden 被禁止 Forbidden 59 | ApiCodeForbidden = 19 60 | // ApiCodeRefreshTokenExpiredCode RefreshToken已过期 61 | ApiCodeRefreshTokenExpiredCode ApiCode = 20 62 | // ApiCodeFileShareNotAllowed 文件不允许分享 63 | ApiCodeFileShareNotAllowed ApiCode = 21 64 | // ApiCodeInvalidRapidProof 文件上传水印码错误 65 | ApiCodeInvalidRapidProof ApiCode = 22 66 | // ApiCodeNotFoundView 资源不存在 67 | ApiCodeNotFoundView ApiCode = 23 68 | // ApiCodeBadRequest 请求非法 69 | ApiCodeBadRequest ApiCode = 24 70 | // ApiCodeInvalidResource 请求无效资源 71 | ApiCodeInvalidResource ApiCode = 25 72 | // ApiCodeVideoPreviewInfoNotFound 视频预览信息不存在 73 | ApiCodeVideoPreviewInfoNotFound ApiCode = 26 74 | // ApiCodeFeatureTemporaryDisabled 功能维护中 75 | ApiCodeFeatureTemporaryDisabled ApiCode = 27 76 | // ApiCodeForbiddenFileInTheRecycleBin 文件已经被删除 77 | ApiCodeForbiddenFileInTheRecycleBin ApiCode = 28 78 | // ApiCodeBadGateway 502网关错误,一般代表请求被限流了 79 | ApiCodeBadGateway ApiCode = 29 80 | // ApiCodeTooManyRequests 429 Too Many Requests错误,一般代表请求被限流了 81 | ApiCodeTooManyRequests ApiCode = 30 82 | // ApiCodeUserDeviceOffline 客户端离线,阿里云盘单账户最多只允许同时登录 10 台设备 83 | ApiCodeUserDeviceOffline ApiCode = 31 84 | // ApiCodeDeviceSessionSignatureInvalid 签名过期,需要更新签名密钥 85 | ApiCodeDeviceSessionSignatureInvalid ApiCode = 32 86 | // ApiCodePermissionDenied 用户已取消授权,或权限已失效,或 token 无效。需要重新发起授权 87 | ApiCodePermissionDenied ApiCode = 33 88 | // ApiCodeUserNotAllowedAccessDrive 用户没有授权应用访问当前drive 89 | ApiCodeUserNotAllowedAccessDrive ApiCode = 34 90 | // ApiCodeUploadIdNotFound 上传文件ID不存在,意味着上传任务已经失效 91 | ApiCodeUploadIdNotFound ApiCode = 35 92 | // ApiCodeUploadPayloadTooLarge 上传文件大小超过限制 93 | ApiCodeUploadPayloadTooLarge ApiCode = 36 94 | ) 95 | 96 | type ApiCode int 97 | 98 | type ApiError struct { 99 | Code ApiCode 100 | Err string 101 | } 102 | 103 | func NewApiError(code ApiCode, err string) *ApiError { 104 | return &ApiError{ 105 | code, 106 | err, 107 | } 108 | } 109 | 110 | func NewApiErrorWithError(err error) *ApiError { 111 | if err == nil { 112 | return NewApiError(ApiCodeOk, "") 113 | } else { 114 | if IsNetErr(err) { 115 | return NewApiError(ApiCodeNetError, err.Error()) 116 | } 117 | return NewApiError(ApiCodeFailed, err.Error()) 118 | } 119 | } 120 | 121 | func NewOkApiError() *ApiError { 122 | return NewApiError(ApiCodeOk, "") 123 | } 124 | 125 | func NewFailedApiError(err string) *ApiError { 126 | return NewApiError(ApiCodeFailed, err) 127 | } 128 | 129 | func (a *ApiError) SetErr(code ApiCode, err string) { 130 | a.Code = code 131 | a.Err = err 132 | } 133 | 134 | func (a *ApiError) Error() string { 135 | if a == nil { 136 | return "" 137 | } 138 | return a.Err 139 | } 140 | 141 | func (a *ApiError) ErrCode() ApiCode { 142 | return a.Code 143 | } 144 | 145 | func (a *ApiError) String() string { 146 | sb := &strings.Builder{} 147 | fmt.Fprintf(sb, "Code=%d, Err=%s", a.Code, a.Err) 148 | return sb.String() 149 | } 150 | 151 | // ParseCommonApiError 解析阿里云盘错误,如果没有错误则返回nil 152 | func ParseCommonApiError(data []byte) *ApiError { 153 | if string(data) == "Bad Gateway" { 154 | // HTTP/1.1 502 Bad Gateway 155 | return NewApiError(ApiCodeBadGateway, "网关错误,你的请求可能被临时限流了") 156 | } 157 | errResp := &ErrorResp{} 158 | if err := json.Unmarshal(data, errResp); err == nil { 159 | if errResp.ErrorCode != "" { 160 | if "AccessTokenInvalid" == errResp.ErrorCode { 161 | return NewApiError(ApiCodeTokenExpiredCode, errResp.GetErrorMsg()) 162 | } else if "NotFound.File" == errResp.ErrorCode || "NotFound.FileId" == errResp.ErrorCode { 163 | return NewApiError(ApiCodeFileNotFoundCode, errResp.GetErrorMsg()) 164 | } else if "AlreadyExist.File" == errResp.ErrorCode { 165 | return NewApiError(ApiCodeFileAlreadyExisted, errResp.GetErrorMsg()) 166 | } else if "BadRequest" == errResp.ErrorCode { 167 | return NewApiError(ApiCodeFailed, errResp.GetErrorMsg()) 168 | } else if "InvalidParameter.RefreshToken" == errResp.ErrorCode { 169 | return NewApiError(ApiCodeRefreshTokenExpiredCode, errResp.GetErrorMsg()) 170 | } else if "FileShareNotAllowed" == errResp.ErrorCode { 171 | return NewApiError(ApiCodeFileShareNotAllowed, errResp.GetErrorMsg()) 172 | } else if "InvalidRapidProof" == errResp.ErrorCode { 173 | return NewApiError(ApiCodeInvalidRapidProof, errResp.GetErrorMsg()) 174 | } else if "NotFound.View" == errResp.ErrorCode { 175 | return NewApiError(ApiCodeNotFoundView, errResp.GetErrorMsg()) 176 | } else if "BadRequest" == errResp.ErrorCode { 177 | return NewApiError(ApiCodeBadRequest, errResp.GetErrorMsg()) 178 | } else if "InvalidResource.FileTypeFolder" == errResp.ErrorCode { 179 | return NewApiError(ApiCodeInvalidResource, errResp.GetErrorMsg()) 180 | } else if "NotFound.VideoPreviewInfo" == errResp.ErrorCode { 181 | return NewApiError(ApiCodeVideoPreviewInfoNotFound, errResp.GetErrorMsg()) 182 | } else if "FeatureTemporaryDisabled" == errResp.ErrorCode { 183 | return NewApiError(ApiCodeFeatureTemporaryDisabled, errResp.GetErrorMsg()) 184 | } else if "ForbiddenFileInTheRecycleBin" == errResp.ErrorCode { 185 | return NewApiError(ApiCodeForbiddenFileInTheRecycleBin, errResp.GetErrorMsg()) 186 | } else if "UserDeviceOffline" == errResp.ErrorCode { 187 | return NewApiError(ApiCodeUserDeviceOffline, "你账号已超出最大登录设备数量,请先下线一台设备,然后重启本应用,才可以继续使用") 188 | } else if "DeviceSessionSignatureInvalid" == errResp.ErrorCode { 189 | return NewApiError(ApiCodeDeviceSessionSignatureInvalid, "签名过期,需要更新签名密钥") 190 | } 191 | return NewFailedApiError(errResp.GetErrorMsg()) 192 | } 193 | } 194 | return nil 195 | } 196 | 197 | // ParseCommonResponseApiError 解析阿里云盘错误,如果没有错误则返回nil 198 | func ParseCommonResponseApiError(resp *http.Response) ([]byte, *ApiError) { 199 | if resp == nil { 200 | return nil, nil 201 | } 202 | 203 | switch resp.StatusCode { 204 | case 502: 205 | return nil, NewApiError(ApiCodeBadGateway, "网关错误,可能是请求的参数有误") 206 | case 429: 207 | return nil, NewApiError(ApiCodeTooManyRequests, "请求太频繁,已被阿里云盘临时限流") 208 | } 209 | data, e := ioutil.ReadAll(resp.Body) 210 | if e != nil { 211 | return nil, NewFailedApiError(e.Error()) 212 | } 213 | return data, ParseCommonApiError(data) 214 | } 215 | -------------------------------------------------------------------------------- /aliyunpan/apierror/api_error_test.go: -------------------------------------------------------------------------------- 1 | package apierror 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestError(t *testing.T) { 9 | //ers := "{\"code\":\"FeatureTemporaryDisabled\",\"message\":\"feature upgrading\",\"requestId\":\"0bc13b0116660546641984077e4b93\",\"resultCode\":\"FeatureTemporaryDisabled\",\"display_message\":\"功能维护中,预计10月底前维护完成\"}" 10 | ers := "{\"code\":\"FeatureTemporaryDisabled\",\"message\":\"feature upgrading\",\"requestId\":\"0bc13b0116660546641984077e4b93\"}" 11 | 12 | e := ParseCommonApiError([]byte(ers)) 13 | fmt.Println(e) 14 | } 15 | -------------------------------------------------------------------------------- /aliyunpan/apierror/api_utils.go: -------------------------------------------------------------------------------- 1 | package apierror 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | // IsNetErr 是否是网络错误 11 | func IsNetErr(err error) bool { 12 | b := underlyingErrorIs(err, syscall.ECONNREFUSED) 13 | if !b { 14 | b = underlyingErrorIs(err, syscall.ECONNABORTED) 15 | } 16 | return b 17 | } 18 | 19 | // underlyingErrorIs 递归调用底层错误类型,判断是否是目标类型 20 | func underlyingErrorIs(err, target error) bool { 21 | err = underlyingError(err) 22 | if err == target { 23 | return true 24 | } else if err == nil { 25 | return false 26 | } else { 27 | return underlyingErrorIs(err, target) 28 | } 29 | } 30 | 31 | // underlyingError returns the underlying error for known os error types. 32 | func underlyingError(err error) error { 33 | switch err := err.(type) { 34 | case *url.Error: 35 | return err.Err 36 | case *net.OpError: 37 | return err.Err 38 | case *os.SyscallError: 39 | return err.Err 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /aliyunpan/apierror/error_resp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apierror 16 | 17 | import "encoding/xml" 18 | 19 | // ErrorResp 默认的错误信息 20 | type ErrorResp struct { 21 | ErrorCode string `json:"code"` 22 | ErrorMsg string `json:"message"` 23 | ErrorDisplayMsg string `json:"display_message"` 24 | } 25 | 26 | type ErrorXmlResp struct { 27 | XMLName xml.Name `xml:"Error"` 28 | Code string `xml:"Code"` 29 | Message string `xml:"Message"` 30 | } 31 | 32 | func (e *ErrorResp) GetErrorMsg() string { 33 | if e.ErrorDisplayMsg != "" { 34 | return e.ErrorDisplayMsg 35 | } 36 | return e.ErrorMsg 37 | } 38 | -------------------------------------------------------------------------------- /aliyunpan/apiutil/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiutil 16 | 17 | import ( 18 | "fmt" 19 | jsoniter "github.com/json-iterator/go" 20 | uuid "github.com/satori/go.uuid" 21 | "math/rand" 22 | "net/http" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | const ( 28 | FileNameSpecialChars = "\\/:*?\"<>|" 29 | ) 30 | 31 | func init() { 32 | rand.Seed(time.Now().UnixNano()) 33 | } 34 | 35 | func Timestamp() int { 36 | // millisecond 37 | return int(time.Now().UTC().UnixNano() / 1e6) 38 | } 39 | 40 | func Rand() string { 41 | randStr := &strings.Builder{} 42 | fmt.Fprintf(randStr, "%d_%d", rand.Int63n(1e5), rand.Int63n(1e10)) 43 | return randStr.String() 44 | } 45 | 46 | func DateOfGmtStr() string { 47 | return time.Now().UTC().Format(http.TimeFormat) 48 | } 49 | 50 | func XRequestId() string { 51 | u4 := uuid.NewV4() 52 | return strings.ToUpper(u4.String()) 53 | } 54 | 55 | func Uuid() string { 56 | u4 := uuid.NewV4() 57 | return u4.String() 58 | } 59 | 60 | // CheckFileNameValid 检测文件名是否有效,包含特殊字符则无效 61 | func CheckFileNameValid(name string) bool { 62 | if name == "" { 63 | return true 64 | } 65 | return !strings.ContainsAny(name, FileNameSpecialChars) 66 | } 67 | 68 | // UtcTime2LocalFormat UTC时间转换为本地时间 69 | func UtcTime2LocalFormat(timeStr string) string { 70 | if timeStr == "" { 71 | return "" 72 | } 73 | t, _ := time.Parse(time.RFC3339, timeStr) 74 | timeUint := t.In(time.Local).Unix() 75 | return time.Unix(timeUint, 0).Format("2006-01-02 15:04:05") 76 | } 77 | 78 | // LocalTime2UtcFormat 本地时间转换为UTC时间 79 | func LocalTime2UtcFormat(utcTimeStr string) string { 80 | if utcTimeStr == "" { 81 | return "" 82 | } 83 | t, _ := time.ParseInLocation("2006-01-02 15:04:05", utcTimeStr, time.Local) 84 | return t.UTC().Format("2006-01-02T15:04:05.000Z07:00") 85 | } 86 | 87 | // UnixTime2LocalFormat 时间戳转换为本地时间字符串 88 | func UnixTime2LocalFormat(unixTime int64) string { 89 | return time.Unix(unixTime/1000, 0).Format("2006-01-02 15:04:05") 90 | } 91 | 92 | // AddCommonHeader 增加公共header 93 | func AddCommonHeader(headers map[string]string) map[string]string { 94 | commonHeaders := map[string]string{ 95 | "accept": "application/json, text/plain, */*", 96 | "referer": "https://www.aliyundrive.com/", 97 | "origin": "https://www.aliyundrive.com", 98 | "content-type": "application/json;charset=UTF-8", 99 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 100 | } 101 | if headers == nil { 102 | return commonHeaders 103 | } 104 | 105 | // merge 106 | for k, v := range headers { 107 | commonHeaders[strings.ToLower(k)] = v 108 | } 109 | return commonHeaders 110 | } 111 | 112 | func GetMapSet(param interface{}) map[string]interface{} { 113 | if param == nil { 114 | return nil 115 | } 116 | 117 | r, _ := jsoniter.MarshalToString(param) 118 | m := map[string]interface{}{} 119 | jsoniter.Unmarshal([]byte(r), &m) 120 | return m 121 | } 122 | -------------------------------------------------------------------------------- /aliyunpan/apiutil/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiutil 16 | 17 | import ( 18 | "fmt" 19 | "github.com/stretchr/testify/assert" 20 | "testing" 21 | ) 22 | 23 | func TestRand(t *testing.T) { 24 | r := Rand() 25 | fmt.Println(r) 26 | assert.Equal(t, 16, len(r)) 27 | } 28 | 29 | func TestDateOfGmtStr(t *testing.T) { 30 | r := DateOfGmtStr() 31 | fmt.Println(r) 32 | } 33 | 34 | func TestUtcTime2LocalFormat(t *testing.T) { 35 | r := UtcTime2LocalFormat("2021-07-29T23:18:07.000Z") 36 | fmt.Println(r) // 2021-07-30 07:18:07 37 | } 38 | 39 | func TestLocalTime2UtcFormat(t *testing.T) { 40 | r := LocalTime2UtcFormat("2021-07-30 07:18:07") 41 | fmt.Println(r) // 2021-07-29T23:18:07.000Z 42 | } 43 | 44 | func TestUnixTime2LocalFormat(t *testing.T) { 45 | r := UnixTime2LocalFormat(1650793433058) 46 | fmt.Println(r) // 2022-04-24 17:43:53 47 | } 48 | -------------------------------------------------------------------------------- /aliyunpan/file_api.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // DefaultRootParentFileId 网盘根目录默认ID 10 | DefaultRootParentFileId string = "root" 11 | 12 | FileOrderByName FileOrderBy = "name" 13 | FileOrderByCreatedAt FileOrderBy = "created_at" 14 | FileOrderByUpdatedAt FileOrderBy = "updated_at" 15 | FileOrderBySize FileOrderBy = "size" 16 | 17 | // FileOrderDirectionDesc 降序 18 | FileOrderDirectionDesc FileOrderDirection = "DESC" 19 | // FileOrderDirectionAsc 升序 20 | FileOrderDirectionAsc FileOrderDirection = "ASC" 21 | 22 | // MaxRequestRetryCount 最大重试次数(应对请求频繁的错误限制) 23 | MaxRequestRetryCount = int64(10) 24 | 25 | // IllegalDownloadUrlPrefix 资源被屏蔽,提示资源非法链接 26 | IllegalDownloadUrlPrefix = "https://pds-system-file.oss-cn-beijing.aliyuncs.com/illegal" 27 | 28 | // DefaultChunkSize 默认分片大小,512KB 29 | DefaultChunkSize = int64(524288) 30 | 31 | // MaxPartNum 最大分片数量大小 32 | MaxPartNum = 10000 33 | 34 | // DefaultZeroSizeFileContentHash 0KB文件默认的SHA1哈希值 35 | DefaultZeroSizeFileContentHash = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709" 36 | 37 | // ShellPatternCharacters 文件名\文件路径通配符字符串 38 | ShellPatternCharacters = "*?[]" 39 | 40 | // PathSeparator 路径分隔符 41 | PathSeparator = "/" 42 | ) 43 | 44 | type ( 45 | // FileCopyParam 文件复制参数 46 | FileCopyParam struct { 47 | // DriveId 网盘id 48 | DriveId string `json:"drive_id"` 49 | // FileId 文件ID 50 | FileId string `json:"file_id"` 51 | // ToParentFileId 目标目录ID、根目录为 root 52 | ToParentFileId string `json:"to_parent_file_id"` 53 | } 54 | 55 | // FileAsyncTaskResult 文件异步操作返回值 56 | FileAsyncTaskResult struct { 57 | // DriveId 网盘id 58 | DriveId string `json:"drive_id"` 59 | // FileId 文件ID 60 | FileId string `json:"file_id"` 61 | // AsyncTaskId 异步任务id。 如果返回为空字符串,表示直接移动成功。 如果返回非空字符串,表示需要经过异步处理。 62 | AsyncTaskId string `json:"async_task_id"` 63 | } 64 | 65 | // FileBatchActionParam 文件批量操作参数 66 | FileBatchActionParam struct { 67 | // 网盘ID 68 | DriveId string `json:"drive_id"` 69 | // 文件ID 70 | FileId string `json:"file_id"` 71 | } 72 | 73 | // FileBatchActionResult 文件批量操作返回值 74 | FileBatchActionResult struct { 75 | // 文件ID 76 | FileId string 77 | // 是否成功 78 | Success bool 79 | } 80 | 81 | // HandleFileDirectoryFunc 处理文件或目录的元信息, 返回值控制是否退出递归 82 | HandleFileDirectoryFunc func(depth int, fdPath string, fd *FileEntity, apierr *apierror.ApiError) bool 83 | 84 | // FileListParam 文件列表参数 85 | FileListParam struct { 86 | OrderBy FileOrderBy `json:"order_by"` 87 | OrderDirection FileOrderDirection `json:"order_direction"` 88 | DriveId string `json:"drive_id"` 89 | ParentFileId string `json:"parent_file_id"` 90 | Limit int `json:"limit"` 91 | // Marker 下一页参数 92 | Marker string `json:"marker"` 93 | } 94 | 95 | // FileListResult 文件列表返回值 96 | FileListResult struct { 97 | FileList FileList `json:"file_list"` 98 | // NextMarker 不为空代表还有下一页 99 | NextMarker string `json:"next_marker"` 100 | } 101 | 102 | // FileGetPathResult 文件路径详情信息结果 103 | FileGetPathResult struct { 104 | // 每一个item对应一个目录,最顶层的目录是root放在最后 105 | // 例如路径:/myphoto/photo2022/photo01,则对应顺序为item[0]={"photo01"}, item[1]={"photo2022"}, item[2]={"myphoto"}, item[3]={"root"}(只有root目录下的子文件夹才会有) 106 | Items []struct { 107 | Trashed bool `json:"trashed"` 108 | DriveId string `json:"drive_id"` 109 | FileId string `json:"file_id"` 110 | CreatedAt time.Time `json:"created_at"` 111 | DomainId string `json:"domain_id"` 112 | EncryptMode string `json:"encrypt_mode"` 113 | Hidden bool `json:"hidden"` 114 | Name string `json:"name"` 115 | ParentFileId string `json:"parent_file_id"` 116 | Starred bool `json:"starred"` 117 | Status string `json:"status"` 118 | Type string `json:"type"` 119 | UpdatedAt string `json:"updated_at"` 120 | UserMeta string `json:"user_meta"` 121 | } `json:"items"` 122 | } 123 | 124 | // FileMoveParam 文件移动参数 125 | FileMoveParam struct { 126 | // 源网盘ID 127 | DriveId string `json:"drive_id"` 128 | // 源文件ID 129 | FileId string `json:"file_id"` 130 | // 目标网盘ID 131 | ToDriveId string `json:"to_drive_id"` 132 | // 目标文件夹ID 133 | ToParentFileId string `json:"to_parent_file_id"` 134 | } 135 | 136 | // FileMoveResult 文件移动返回值 137 | FileMoveResult struct { 138 | // 文件ID 139 | FileId string 140 | // 是否成功 141 | Success bool 142 | } 143 | 144 | // VideoGetPreviewPlayInfoParam 视频信息参数 145 | VideoGetPreviewPlayInfoParam struct { 146 | DriveId string `json:"drive_id"` 147 | // FileId 视频文件ID 148 | FileId string `json:"file_id"` 149 | } 150 | 151 | // VideoGetPreviewPlayInfoResult 视频信息返回值 152 | VideoGetPreviewPlayInfoResult struct { 153 | DomainId string `json:"domain_id"` 154 | DriveId string `json:"drive_id"` 155 | FileId string `json:"file_id"` 156 | VideoPreviewPlayInfo struct { 157 | Category string `json:"category"` 158 | Meta struct { 159 | Duration float64 `json:"duration"` 160 | Width int `json:"width"` 161 | Height int `json:"height"` 162 | LiveTranscodingMeta struct { 163 | TsSegment int `json:"ts_segment"` 164 | TsTotalCount int `json:"ts_total_count"` 165 | TsPreCount int `json:"ts_pre_count"` 166 | } `json:"live_transcoding_meta"` 167 | } `json:"meta"` 168 | LiveTranscodingTaskList []struct { 169 | TemplateId string `json:"template_id"` 170 | TemplateName string `json:"template_name"` 171 | TemplateWidth int `json:"template_width"` 172 | TemplateHeight int `json:"template_height"` 173 | Status string `json:"status"` 174 | Stage string `json:"stage"` 175 | URL string `json:"url"` 176 | } `json:"live_transcoding_task_list"` 177 | } `json:"video_preview_play_info"` 178 | } 179 | 180 | // MkdirResult 创建文件夹返回值 181 | MkdirResult struct { 182 | ParentFileId string `json:"parent_file_id"` 183 | Type string `json:"type"` 184 | FileId string `json:"file_id"` 185 | DomainId string `json:"domain_id"` 186 | DriveId string `json:"drive_id"` 187 | FileName string `json:"file_name"` 188 | EncryptMode string `json:"encrypt_mode"` 189 | } 190 | ) 191 | 192 | // NewFileEntityForRootDir 创建根目录"/"的默认文件信息 193 | func NewFileEntityForRootDir() *FileEntity { 194 | return &FileEntity{ 195 | FileId: DefaultRootParentFileId, 196 | FileType: "folder", 197 | FileName: "/", 198 | ParentFileId: "", 199 | Path: "/", 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /aliyunpan/file_download_entity.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | import "net/http" 4 | 5 | type ( 6 | DownloadFuncCallback func(httpMethod, fullUrl string, headers map[string]string) (resp *http.Response, err error) 7 | 8 | // FileDownloadRange 分片。0-100,101-200,201-300... 9 | FileDownloadRange struct { 10 | // 起始值,包含 11 | Offset int64 12 | // 结束值,包含 13 | End int64 14 | } 15 | 16 | // GetFileDownloadUrlParam 获取文件下载链接 17 | GetFileDownloadUrlParam struct { 18 | DriveId string `json:"drive_id"` 19 | FileId string `json:"file_id"` 20 | ExpireSec int `json:"expire_sec"` 21 | } 22 | 23 | // GetFileDownloadUrlResult 获取文件下载链接返回值 24 | GetFileDownloadUrlResult struct { 25 | Method string `json:"method"` 26 | Url string `json:"url"` 27 | InternalUrl string `json:"internal_url"` 28 | CdnUrl string `json:"cdn_url"` 29 | Expiration string `json:"expiration"` 30 | Size int64 `json:"size"` 31 | Ratelimit struct { 32 | PartSpeed int64 `json:"part_speed"` 33 | PartSize int64 `json:"part_size"` 34 | } `json:"ratelimit"` 35 | Description string `json:"description"` 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /aliyunpan/file_entity.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | import "strings" 4 | 5 | type ( 6 | // FileList 文件列表 7 | FileList []*FileEntity 8 | 9 | // FileEntity 文件/文件夹信息 10 | FileEntity struct { 11 | // 网盘ID 12 | DriveId string `json:"driveId"` 13 | // 域ID 14 | DomainId string `json:"domainId"` 15 | // FileId 文件ID 16 | FileId string `json:"fileId"` 17 | // FileName 文件名 18 | FileName string `json:"fileName"` 19 | // FileSize 文件大小 20 | FileSize int64 `json:"fileSize"` 21 | // 文件类别 folder / file 22 | FileType string `json:"fileType"` 23 | // 创建时间 24 | CreatedAt string `json:"createdAt"` 25 | // 最后修改时间 26 | UpdatedAt string `json:"updatedAt"` 27 | // 后缀名,例如:dmg 28 | FileExtension string `json:"fileExtension"` 29 | // 文件上传ID 30 | UploadId string `json:"uploadId"` 31 | // 父文件夹ID 32 | ParentFileId string `json:"parentFileId"` 33 | // 内容CRC64校验值,只有文件才会有 34 | Crc64Hash string `json:"crc64Hash"` 35 | // 内容Hash值,只有文件才会有 36 | ContentHash string `json:"contentHash"` 37 | // 内容Hash计算方法,只有文件才会有,默认为:sha1 38 | ContentHashName string `json:"contentHashName"` 39 | // FilePath 文件的完整路径 40 | Path string `json:"path"` 41 | // Category 文件分类,例如:image/video/doc/others 42 | Category string `json:"category"` 43 | // SyncFlag 同步盘标记,该文件夹是否是同步盘的文件 44 | SyncFlag bool `json:"syncFlag"` 45 | // SyncMeta 如果是同步盘的文件夹,则这里会记录该文件对应的同步机器和目录等信息 46 | SyncMeta string `json:"syncMeta"` 47 | // Thumbnail 缩略图URL地址,只有相册文件才有 48 | Thumbnail string `json:"thumbnail"` 49 | // AlbumId 所属相册ID,只有相册文件才有 50 | AlbumId string `json:"albumId"` 51 | } 52 | 53 | FileOrderBy string 54 | FileOrderDirection string 55 | ) 56 | 57 | // IsFolder 是否是文件夹 58 | func (f *FileEntity) IsFolder() bool { 59 | return f.FileType == "folder" 60 | } 61 | 62 | // IsFile 是否是文件 63 | func (f *FileEntity) IsFile() bool { 64 | return f.FileType == "file" 65 | } 66 | 67 | // IsDriveRootFolder 是否是网盘根目录 68 | func (f *FileEntity) IsDriveRootFolder() bool { 69 | return f.FileId == DefaultRootParentFileId 70 | } 71 | 72 | // 文件展示信息 73 | func (f *FileEntity) String() string { 74 | builder := &strings.Builder{} 75 | builder.WriteString("文件ID: " + f.FileId + "\n") 76 | builder.WriteString("文件名: " + f.FileName + "\n") 77 | if f.IsFolder() { 78 | builder.WriteString("文件类型: 目录\n") 79 | } else { 80 | builder.WriteString("文件类型: 文件\n") 81 | } 82 | builder.WriteString("文件路径: " + f.Path + "\n") 83 | return builder.String() 84 | } 85 | 86 | // IsAlbumFile 是否是相册文件 87 | func (f *FileEntity) IsAlbumFile() bool { 88 | return f.AlbumId != "" 89 | } 90 | 91 | // IsAlbumLivePhotoFile 是否是相册实况图片文件 92 | func (f *FileEntity) IsAlbumLivePhotoFile() bool { 93 | return f.IsAlbumFile() && strings.HasSuffix(strings.ToLower(f.FileName), ".livp") 94 | } 95 | 96 | // TotalSize 获取目录下文件的总大小 97 | func (fl FileList) TotalSize() int64 { 98 | var size int64 99 | for k := range fl { 100 | if fl[k] == nil { 101 | continue 102 | } 103 | 104 | size += fl[k].FileSize 105 | } 106 | return size 107 | } 108 | 109 | // Count 获取文件总数和目录总数 110 | func (fl FileList) Count() (fileN, directoryN int64) { 111 | for k := range fl { 112 | if fl[k] == nil { 113 | continue 114 | } 115 | 116 | if fl[k].IsFolder() { 117 | directoryN++ 118 | } else { 119 | fileN++ 120 | } 121 | } 122 | return 123 | } 124 | 125 | // ItemCount 文件数量 126 | func (fl FileList) ItemCount() int { 127 | return len(fl) 128 | } 129 | 130 | // Item 文件项 131 | func (fl FileList) Item(index int) *FileEntity { 132 | return fl[index] 133 | } 134 | -------------------------------------------------------------------------------- /aliyunpan/file_share_entity.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | type ( 4 | // ShareCreateParam 创建分享 5 | ShareCreateParam struct { 6 | DriveId string `json:"drive_id"` 7 | // 分享密码,4个字符,为空代码公开分享 8 | SharePwd string `json:"share_pwd"` 9 | // 过期时间,为空代表永不过期。时间格式必须是这种:2021-07-23 09:22:19 10 | Expiration string `json:"expiration"` 11 | FileIdList []string `json:"file_id_list"` 12 | } 13 | ShareEntity struct { 14 | Creator string `json:"creator"` 15 | DriveId string `json:"drive_id"` 16 | ShareId string `json:"share_id"` 17 | ShareName string `json:"share_name"` 18 | // SharePwd 密码,为空代表没有密码 19 | SharePwd string `json:"share_pwd"` 20 | ShareUrl string `json:"share_url"` 21 | FileIdList []string `json:"file_id_list"` 22 | SaveCount int `json:"save_count"` 23 | // Expiration 过期时间,为空代表永不过期 24 | Expiration string `json:"expiration"` 25 | UpdatedAt string `json:"updated_at"` 26 | CreatedAt string `json:"created_at"` 27 | // forbidden-已违规,enabled-正常 28 | Status string `json:"status"` 29 | FirstFile *FileEntity `json:"first_file"` 30 | } 31 | 32 | // FastShareCreateParam 创建快传分享 33 | FastShareCreateParam struct { 34 | DriveId string `json:"drive_id"` 35 | FileIdList []string `json:"file_id_list"` 36 | } 37 | FastShareFileItem struct { 38 | DriveId string `json:"drive_id"` 39 | FileId string `json:"file_id"` 40 | } 41 | // FastShareCreateResult 创建快传返回值 42 | FastShareCreateResult struct { 43 | Expiration string `json:"expiration"` 44 | Thumbnail string `json:"thumbnail"` 45 | ShareName string `json:"share_name"` 46 | ShareId string `json:"share_id"` 47 | ShareUrl string `json:"share_url"` 48 | DriveFileList []FastShareFileItem `json:"drive_file_list"` 49 | FullShareMsg string `json:"full_share_msg"` 50 | ShareTitle string `json:"share_title"` 51 | ShareSubtitle string `json:"share_subtitle"` 52 | Expired bool `json:"expired"` 53 | } 54 | ) 55 | -------------------------------------------------------------------------------- /aliyunpan/file_upload_entity.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "github.com/tickstep/library-go/requester/rio" 8 | "io" 9 | "math" 10 | "math/big" 11 | "net/http" 12 | ) 13 | 14 | type ( 15 | // UploadFunc 上传文件处理函数 16 | UploadFunc func(httpMethod, fullUrl string, headers map[string]string) (resp *http.Response, err error) 17 | 18 | // FileUploadPartInfoParam 上传文件分片参数。从1开始,最大为 10000 19 | FileUploadPartInfoParam struct { 20 | PartNumber int `json:"part_number"` 21 | } 22 | 23 | // FileUploadCheckPreHashParam 文件PreHash检测参数 24 | FileUploadCheckPreHashParam struct { 25 | // DriveId 网盘ID 26 | DriveId string `json:"drive_id"` 27 | // ParentFileId 父目录id,上传到根目录时填写 root 28 | ParentFileId string `json:"parent_file_id"` 29 | // Name 文件名称,按照 utf8 编码最长 1024 字节,不能以 / 结尾 30 | Name string `json:"name"` 31 | // Size 文件大小,单位为 byte。秒传必须 32 | Size int64 `json:"size"` 33 | // PreHash 针对大文件sha1计算非常耗时的情况, 可以先在读取文件的前1k的sha1, 如果前1k的sha1没有匹配的, 那么说明文件无法做秒传, 如果1ksha1有匹配再计算文件sha1进行秒传,这样有效边避免无效的sha1计算。 34 | PreHash string `json:"pre_hash"` 35 | } 36 | 37 | // CreateFileUploadParam 创建上传文件参数 38 | CreateFileUploadParam struct { 39 | Name string `json:"name"` 40 | DriveId string `json:"drive_id"` 41 | ParentFileId string `json:"parent_file_id"` 42 | Size int64 `json:"size"` 43 | // 上传文件分片参数,最大为 10000 44 | PartInfoList []FileUploadPartInfoParam `json:"part_info_list"` 45 | ContentHash string `json:"content_hash"` 46 | // 默认为 sha1。可选:sha1,none 47 | ContentHashName string `json:"content_hash_name"` 48 | // 默认为 file 49 | Type string `json:"type"` 50 | // 默认为 auto_rename。可选:overwrite-覆盖网盘同名文件,auto_rename-自动重命名,refuse-无需检测 51 | CheckNameMode string `json:"check_name_mode"` 52 | 53 | ProofCode string `json:"proof_code"` 54 | ProofVersion string `json:"proof_version"` 55 | 56 | // 分片大小 57 | // 不进行json序列化 58 | BlockSize int64 `json:"-"` 59 | // LocalCreatedAt 本地创建时间,只对文件有效,格式yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 60 | LocalCreatedAt string `json:"-"` 61 | // LocalModifiedAt 本地修改时间,只对文件有效,格式yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 62 | LocalModifiedAt string `json:"-"` 63 | } 64 | 65 | FileUploadPartInfoResult struct { 66 | PartNumber int `json:"part_number"` 67 | UploadURL string `json:"upload_url"` 68 | InternalUploadURL string `json:"internal_upload_url"` 69 | ContentType string `json:"content_type"` 70 | } 71 | 72 | // CreateFileUploadResult 创建上传文件返回值 73 | CreateFileUploadResult struct { 74 | ParentFileId string `json:"parent_file_id"` 75 | PartInfoList []FileUploadPartInfoResult `json:"part_info_list"` 76 | UploadId string `json:"upload_id"` 77 | // RapidUpload 是否秒传。true-已秒传,false-没有秒传,需要手动上传 78 | RapidUpload bool `json:"rapid_upload"` 79 | Type string `json:"type"` 80 | FileId string `json:"file_id"` 81 | DomainId string `json:"domain_id"` 82 | DriveId string `json:"drive_id"` 83 | // FileName 保存在网盘的名称,因为网盘会自动重命名同名的文件 84 | FileName string `json:"file_name"` 85 | EncryptMode string `json:"encrypt_mode"` 86 | Location string `json:"location"` 87 | } 88 | 89 | // GetUploadUrlParam 获取上传数据链接参数 90 | GetUploadUrlParam struct { 91 | DriveId string `json:"drive_id"` 92 | FileId string `json:"file_id"` 93 | PartInfoList []FileUploadPartInfoParam `json:"part_info_list"` 94 | UploadId string `json:"upload_id"` 95 | } 96 | 97 | // GetUploadUrlResult 获取上传数据链接返回值 98 | GetUploadUrlResult struct { 99 | DomainId string `json:"domain_id"` 100 | DriveId string `json:"drive_id"` 101 | FileId string `json:"file_id"` 102 | PartInfoList []FileUploadPartInfoResult `json:"part_info_list"` 103 | UploadId string `json:"upload_id"` 104 | CreateAt string `json:"create_at"` 105 | } 106 | 107 | FileUploadRange struct { 108 | // 起始值,包含 109 | Offset int64 110 | // 总上传长度 111 | Len int64 112 | } 113 | 114 | // FileUploadChunkData 文件上传数据块 115 | FileUploadChunkData struct { 116 | Reader io.Reader 117 | ChunkSize int64 118 | hasReadCount int64 119 | } 120 | 121 | // CompleteUploadFileParam 提交上传文件传输完成参数 122 | CompleteUploadFileParam struct { 123 | DriveId string `json:"drive_id"` 124 | FileId string `json:"file_id"` 125 | UploadId string `json:"upload_id"` 126 | } 127 | 128 | CompleteUploadFileResult struct { 129 | DriveId string `json:"drive_id"` 130 | DomainId string `json:"domain_id"` 131 | FileId string `json:"file_id"` 132 | Name string `json:"name"` 133 | Type string `json:"type"` 134 | Size int64 `json:"size"` 135 | UploadId string `json:"upload_id"` 136 | ParentFileId string `json:"parent_file_id"` 137 | Crc64Hash string `json:"crc64_hash"` 138 | ContentHash string `json:"content_hash"` 139 | ContentHashName string `json:"content_hash_name"` 140 | CreatedAt string `json:"created_at"` 141 | } 142 | 143 | // GetUploadedPartItem 上传分片详情 144 | GetUploadedPartItem struct { 145 | // Etag 在上传分片结束后,服务端会返回这个分片的Etag,在complete的时候可以在uploadInfo指定分片的Etag,服务端会在合并时对每个分片Etag做校验 146 | Etag string `json:"etag"` 147 | // PartNumber 分片序列号,从 1 开始。单个文件分片最大限制5GB,最小限制100KB 148 | PartNumber int `json:"part_number"` 149 | // PartSize 分片大小 150 | PartSize int64 `json:"part_size"` 151 | } 152 | // GetUploadedPartsParam 列举已上传分片参数 153 | GetUploadedPartsParam struct { 154 | // DriveId 网盘ID 155 | DriveId string `json:"drive_id"` 156 | // FileId 157 | FileId string `json:"file_id"` 158 | // UploadId 文件创建获取的upload_id 159 | UploadId string `json:"upload_id"` 160 | // PartNumberMarker 分页标记 161 | PartNumberMarker string `json:"part_number_marker"` 162 | } 163 | // GetUploadedPartsResult 列举已上传分片返回值 164 | GetUploadedPartsResult struct { 165 | // DriveId 网盘ID 166 | DriveId string `json:"drive_id"` 167 | // UploadId 文件创建获取的upload_id 168 | UploadId string `json:"upload_id"` 169 | // ParallelUpload 是否并行上传 170 | ParallelUpload bool `json:"parallelUpload"` 171 | // UploadedParts 已经上传分片列表 172 | UploadedParts []*GetUploadedPartItem `json:"uploaded_parts"` 173 | // NextPartNumberMarker 下一页起始资源标识符, 最后一页该值为空。 174 | NextPartNumberMarker string `json:"next_part_number_marker"` 175 | } 176 | ) 177 | 178 | func (d *FileUploadChunkData) Read(p []byte) (n int, err error) { 179 | realReadCount := int64(0) 180 | var buf []byte = p 181 | needCopy := false 182 | if (d.hasReadCount + int64(len(p))) > d.ChunkSize { 183 | realReadCount = d.ChunkSize - d.hasReadCount 184 | buf = make([]byte, realReadCount) 185 | needCopy = true 186 | } 187 | 188 | n, err = d.Reader.Read(buf) 189 | if needCopy { 190 | copy(p, buf) 191 | } 192 | d.hasReadCount += int64(n) 193 | return n, err 194 | } 195 | 196 | func (d *FileUploadChunkData) Len() int64 { 197 | return d.ChunkSize 198 | } 199 | 200 | // CalcProofCode 计算文件上传防伪码 201 | func CalcProofCode(accessToken string, reader rio.ReaderAtLen64, fileSize int64) string { 202 | if fileSize == 0 { // empty file 203 | return "" 204 | } 205 | 206 | md5w := md5.New() 207 | md5w.Write([]byte(accessToken)) 208 | md5bytes := md5w.Sum(nil) 209 | hashCode := hex.EncodeToString(md5bytes)[0:16] 210 | hashInteger, _ := new(big.Int).SetString(hashCode, 16) 211 | 212 | z := big.NewInt(0) 213 | startPosInteger := big.NewInt(0) 214 | z.Div(hashInteger, big.NewInt(fileSize)) 215 | startPosInteger.Sub(hashInteger, big.NewInt(z.Int64()*fileSize)) 216 | startPos := startPosInteger.Int64() 217 | 218 | endPos := startPos + 8 219 | if endPos > fileSize { 220 | endPos = fileSize 221 | } 222 | 223 | // read byte from file 224 | readCount := endPos - startPos 225 | proofBytes := make([]byte, readCount) 226 | reader.ReadAt(proofBytes, startPos) 227 | 228 | // calc the base64 string for read bytes 229 | return base64.StdEncoding.EncodeToString(proofBytes) 230 | } 231 | 232 | // GenerateFileUploadPartInfoList 根据文件大小自动生成分片 233 | func GenerateFileUploadPartInfoList(size int64) []FileUploadPartInfoParam { 234 | return GenerateFileUploadPartInfoListWithChunkSize(size, DefaultChunkSize) 235 | } 236 | 237 | // GenerateFileUploadPartInfoListWithChunkSize 根据文件大小和指定的分片大小自动生成分片 238 | func GenerateFileUploadPartInfoListWithChunkSize(size, chunkSize int64) []FileUploadPartInfoParam { 239 | r := []FileUploadPartInfoParam{} 240 | if size <= chunkSize { 241 | r = append(r, FileUploadPartInfoParam{ 242 | PartNumber: 1, 243 | }) 244 | } else { 245 | pageSize := int(math.Ceil(float64(size) / float64(chunkSize))) 246 | for i := 1; i <= pageSize; i++ { 247 | r = append(r, FileUploadPartInfoParam{ 248 | PartNumber: i, 249 | }) 250 | } 251 | } 252 | return r 253 | } 254 | -------------------------------------------------------------------------------- /aliyunpan/user_api.go: -------------------------------------------------------------------------------- 1 | package aliyunpan 2 | 3 | const ( 4 | User UserRole = "user" 5 | UnknownRole UserRole = "unknown" 6 | 7 | Enabled UserStatus = "enable" 8 | UnknownStatus UserStatus = "unknown" 9 | ) 10 | 11 | type ( 12 | UserRole string 13 | UserStatus string 14 | 15 | // UserInfo 用户信息 16 | UserInfo struct { 17 | // DomainId 域ID 18 | DomainId string `json:"domainId"` 19 | // FileDriveId 备份(文件)网盘ID 20 | FileDriveId string `json:"fileDriveId"` 21 | // SafeBoxDriveId 保险箱网盘ID 22 | SafeBoxDriveId string `json:"safeBoxDriveId"` 23 | // AlbumDriveId 相册网盘ID 24 | AlbumDriveId string `json:"albumDriveId"` 25 | // ResourceDriveId 资源库网盘ID 26 | ResourceDriveId string `json:"resourceDriveId"` 27 | // 用户UID 28 | UserId string `json:"userId"` 29 | // UserName 用户名 30 | UserName string `json:"userName"` 31 | // CreatedAt 创建时间 32 | CreatedAt string `json:"createdAt"` 33 | // Email 邮箱 34 | Email string `json:"email"` 35 | // Phone 手机 36 | Phone string `json:"phone"` 37 | // Role 角色,默认是user 38 | Role UserRole `json:"role"` 39 | // Status 是否被禁用,enable / disable 40 | Status UserStatus `json:"status"` 41 | // Nickname 昵称,如果没有设置则为空 42 | Nickname string `json:"nickname"` 43 | // TotalSize 网盘空间总大小 44 | TotalSize uint64 `json:"totalSize"` 45 | // UsedSize 网盘已使用空间大小 46 | UsedSize uint64 `json:"usedSize"` 47 | // ThirdPartyVip “三方权益包”是否生效 48 | ThirdPartyVip bool `json:"thirdPartyVip"` 49 | // ThirdPartyVipExpire “三方权益包”过期时间 50 | ThirdPartyVipExpire string `json:"thirdPartyVipExpire"` 51 | } 52 | ) 53 | -------------------------------------------------------------------------------- /aliyunpan_open/api_error.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 6 | "github.com/tickstep/library-go/logger" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | type ( 12 | ApiErrorHandleResp struct { 13 | // NeedRetry 是否需要重试 14 | NeedRetry bool 15 | // ApiErr 错误 16 | ApiErr *apierror.ApiError 17 | } 18 | ) 19 | 20 | func NewApiErrorHandleResp(needRetry bool, apiErr *apierror.ApiError) *ApiErrorHandleResp { 21 | return &ApiErrorHandleResp{ 22 | NeedRetry: needRetry, 23 | ApiErr: apiErr, 24 | } 25 | } 26 | 27 | // ParseAliApiError 解析阿里接口返回错误,封装成本地的统一实体 28 | func (p *OpenPanClient) ParseAliApiError(respErr *openapi.AliApiErrResult) *apierror.ApiError { 29 | if respErr == nil { 30 | return nil 31 | } 32 | 33 | switch respErr.HttpStatusCode { 34 | case 200: 35 | return apierror.NewFailedApiError(respErr.Message) 36 | case 400: 37 | if respErr.Code == "NotFound.File" { 38 | return apierror.NewApiError(apierror.ApiCodeFileNotFoundCode, respErr.Message) 39 | } else if respErr.Code == "NotFound.UploadId" { 40 | return apierror.NewApiError(apierror.ApiCodeUploadIdNotFound, respErr.Message) 41 | } 42 | case 401: 43 | if respErr.Code == "AccessTokenExpired" { 44 | return apierror.NewApiError(apierror.ApiCodeTokenExpiredCode, respErr.Message) 45 | } else if respErr.Code == "RefreshTokenExpired" { 46 | return apierror.NewApiError(apierror.ApiCodeRefreshTokenExpiredCode, respErr.Message) 47 | } 48 | case 403: 49 | if respErr.Code == "PermissionDenied" { 50 | return apierror.NewApiError(apierror.ApiCodePermissionDenied, respErr.Message) 51 | } else if respErr.Code == "UserNotAllowedAccessDrive" { 52 | return apierror.NewApiError(apierror.ApiCodeUserNotAllowedAccessDrive, respErr.Message) 53 | } else if respErr.Code == "FileShareNotAllowed" { 54 | return apierror.NewApiError(apierror.ApiCodeFileShareNotAllowed, respErr.Message) 55 | } 56 | case 404: 57 | if respErr.Code == "NotFound.FileId" { 58 | return apierror.NewApiError(apierror.ApiCodeFileNotFoundCode, respErr.Message) 59 | } else if respErr.Code == "Not Found" { 60 | return apierror.NewApiError(apierror.ApiCodeFailed, respErr.Message) 61 | } 62 | case 409: 63 | case 429: 64 | if respErr.Code == "TooManyRequests" { 65 | return apierror.NewApiError(apierror.ApiCodeTooManyRequests, respErr.Message) 66 | } 67 | case 413: 68 | if respErr.Code == "Payload Too Large" { 69 | return apierror.NewApiError(apierror.ApiCodeUploadPayloadTooLarge, respErr.Message) 70 | } 71 | } 72 | return apierror.NewFailedApiError(respErr.Message) 73 | } 74 | 75 | // HandleAliApiError 处理公共错误 76 | func (p *OpenPanClient) HandleAliApiError(respErr *openapi.AliApiErrResult, retryTime *int) *ApiErrorHandleResp { 77 | // handle error, retry, token refresh 78 | myApiErr := p.ParseAliApiError(respErr) 79 | if myApiErr.Code == apierror.ApiCodeTokenExpiredCode { 80 | // get new access token 81 | time.Sleep(time.Duration(1) * time.Second) 82 | if tokenErr := p.RefreshNewAccessToken(); tokenErr != nil { 83 | logger.Verboseln("get new access token from server error: ", tokenErr) 84 | return NewApiErrorHandleResp(false, myApiErr) 85 | } 86 | // retry check 87 | if *retryTime < ApiRetryMaxTimes { 88 | *retryTime++ 89 | return NewApiErrorHandleResp(true, myApiErr) 90 | } 91 | } else if myApiErr.Code == apierror.ApiCodeTooManyRequests { 92 | // sleep 93 | // 可以根据429和 x-retry-after 头部来判断等待重试的时间 94 | if retryMillisecond := respErr.GetExtra("x-retry-after"); retryMillisecond != nil { 95 | num, err := strconv.Atoi(retryMillisecond.(string)) 96 | if err == nil { 97 | // 比官方要的延迟时间多1s 98 | time.Sleep(time.Duration(int64(num)+1000) * time.Millisecond) 99 | } 100 | } else { 101 | time.Sleep(time.Duration(int64(*retryTime+1)*2) * time.Second) 102 | } 103 | 104 | // retry check 105 | if *retryTime < ApiRetryMaxTimes { 106 | *retryTime++ 107 | return NewApiErrorHandleResp(true, myApiErr) 108 | } 109 | } 110 | return NewApiErrorHandleResp(false, myApiErr) 111 | } 112 | -------------------------------------------------------------------------------- /aliyunpan_open/file_album.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 7 | ) 8 | 9 | // ShareAlbumListGetAll 获取共享相册列表 10 | func (p *OpenPanClient) ShareAlbumListGetAll() (aliyunpan.ShareAlbumList, *apierror.ApiError) { 11 | retryTime := 0 12 | 13 | RetryBegin: 14 | opParam := &openapi.ShareAlbumListParam{} 15 | if result, err := p.apiClient.ShareAlbumList(opParam); err == nil { 16 | shareAlbumList := aliyunpan.ShareAlbumList{} 17 | for _, item := range result.Items { 18 | shareAlbumList = append(shareAlbumList, &aliyunpan.AlbumEntity{ 19 | AlbumId: item.SharedAlbumId, 20 | Name: item.Name, 21 | Description: item.Description, 22 | CreatedAt: item.CreatedAt, 23 | UpdatedAt: item.UpdatedAt, 24 | FileCount: 0, 25 | ImageCount: 0, 26 | VideoCount: 0, 27 | IsSharing: false, 28 | Owner: "", 29 | }) 30 | } 31 | return shareAlbumList, nil 32 | } else { 33 | // handle common error 34 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 35 | goto RetryBegin 36 | } else { 37 | return nil, apiErrorHandleResp.ApiErr 38 | } 39 | } 40 | } 41 | 42 | // ShareAlbumListFileGetAll 获取指定相簿下的所有文件列表 43 | func (p *OpenPanClient) ShareAlbumListFileGetAll(param *aliyunpan.ShareAlbumListFileParam) (aliyunpan.FileList, *apierror.ApiError) { 44 | internalParam := &aliyunpan.ShareAlbumListFileParam{ 45 | AlbumId: param.AlbumId, 46 | Marker: param.Marker, 47 | Limit: param.Limit, 48 | ImageThumbnailWidth: param.ImageThumbnailWidth, 49 | } 50 | 51 | fileList := aliyunpan.FileList{} 52 | result, err := p.ShareAlbumListFile(internalParam) 53 | if err != nil || result == nil { 54 | return nil, err 55 | } 56 | fileList = append(fileList, result.FileList...) 57 | 58 | // more page? 59 | for len(result.NextMarker) > 0 { 60 | internalParam.Marker = result.NextMarker 61 | result, err = p.ShareAlbumListFile(internalParam) 62 | if err == nil && result != nil { 63 | fileList = append(fileList, result.FileList...) 64 | } else { 65 | break 66 | } 67 | } 68 | return fileList, nil 69 | } 70 | 71 | // ShareAlbumListFile 获取共享相册文件列表 72 | func (p *OpenPanClient) ShareAlbumListFile(param *aliyunpan.ShareAlbumListFileParam) (*aliyunpan.FileListResult, *apierror.ApiError) { 73 | retryTime := 0 74 | 75 | RetryBegin: 76 | opParam := &openapi.ShareAlbumListFileParam{ 77 | AlbumId: param.AlbumId, 78 | OrderBy: "joined_at", 79 | OrderDirection: "DESC", 80 | Marker: param.Marker, 81 | Limit: param.Limit, 82 | ImageThumbnailWidth: param.ImageThumbnailWidth, 83 | } 84 | if opParam.Limit <= 0 { 85 | opParam.Limit = 50 86 | } 87 | if opParam.ImageThumbnailWidth <= 0 { 88 | opParam.ImageThumbnailWidth = 480 89 | } 90 | if result, err := p.apiClient.ShareAlbumListFile(opParam); err == nil { 91 | shareAlbumFileList := aliyunpan.FileList{} 92 | for _, item := range result.Items { 93 | shareAlbumFileList = append(shareAlbumFileList, &aliyunpan.FileEntity{ 94 | DriveId: item.DriveId, 95 | DomainId: "", 96 | FileId: item.FileId, 97 | FileName: item.Name, 98 | FileSize: item.Size, 99 | FileType: item.Type, 100 | CreatedAt: item.CreatedAt, 101 | UpdatedAt: item.UpdatedAt, 102 | FileExtension: item.FileExtension, 103 | UploadId: "", 104 | ParentFileId: item.ParentFileId, 105 | Crc64Hash: "", 106 | ContentHash: item.ContentHash, 107 | ContentHashName: item.ContentHashName, 108 | Path: "", 109 | Category: item.Category, 110 | SyncFlag: false, 111 | SyncMeta: "", 112 | Thumbnail: item.Thumbnail, 113 | AlbumId: item.AlbumId, 114 | }) 115 | } 116 | return &aliyunpan.FileListResult{ 117 | FileList: shareAlbumFileList, 118 | NextMarker: result.NextMarker, 119 | }, nil 120 | } else { 121 | // handle common error 122 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 123 | goto RetryBegin 124 | } else { 125 | return nil, apiErrorHandleResp.ApiErr 126 | } 127 | } 128 | } 129 | 130 | // ShareAlbumGetFileDownloadUrl 获取共享相册下文件下载地址 131 | func (p *OpenPanClient) ShareAlbumGetFileDownloadUrl(param *aliyunpan.ShareAlbumGetFileUrlParam) (*aliyunpan.ShareAlbumGetFileUrlResult, *apierror.ApiError) { 132 | retryTime := 0 133 | 134 | RetryBegin: 135 | opParam := &openapi.ShareAlbumGetFileUrlParam{ 136 | AlbumId: param.AlbumId, 137 | DriveId: param.DriveId, 138 | FileId: param.FileId, 139 | } 140 | if result, err := p.apiClient.ShareAlbumGetFileDownloadUrl(opParam); err == nil { 141 | return result, nil 142 | } else { 143 | // handle common error 144 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 145 | goto RetryBegin 146 | } else { 147 | return nil, apiErrorHandleResp.ApiErr 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /aliyunpan_open/file_copy.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 7 | ) 8 | 9 | // FileCopy 同网盘内复制文件或文件夹 10 | func (p *OpenPanClient) FileCopy(param *aliyunpan.FileCopyParam) (*aliyunpan.FileAsyncTaskResult, *apierror.ApiError) { 11 | retryTime := 0 12 | 13 | RetryBegin: 14 | opParam := &openapi.FileCopyParam{ 15 | DriveId: param.DriveId, 16 | FileId: param.FileId, 17 | ToDriveId: param.DriveId, 18 | ToParentFileId: param.ToParentFileId, 19 | AutoRename: true, 20 | } 21 | if result, err := p.apiClient.FileCopy(opParam); err == nil { 22 | return &aliyunpan.FileAsyncTaskResult{ 23 | DriveId: result.DriveId, 24 | FileId: result.FileId, 25 | AsyncTaskId: result.AsyncTaskId, 26 | }, nil 27 | } else { 28 | // handle common error 29 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 30 | goto RetryBegin 31 | } else { 32 | return nil, apiErrorHandleResp.ApiErr 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /aliyunpan_open/file_delete.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 7 | ) 8 | 9 | // FileDelete 删除文件到回收站 10 | func (p *OpenPanClient) FileDelete(param *aliyunpan.FileBatchActionParam) (*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 11 | retryTime := 0 12 | 13 | RetryBegin: 14 | opParam := &openapi.FileIdentityPair{ 15 | DriveId: param.DriveId, 16 | FileId: param.FileId, 17 | } 18 | if result, err := p.apiClient.FileTrash(opParam); err == nil { 19 | return &aliyunpan.FileBatchActionResult{ 20 | FileId: result.FileId, 21 | Success: true, 22 | }, nil 23 | } else { 24 | // handle common error 25 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 26 | goto RetryBegin 27 | } else { 28 | return nil, apiErrorHandleResp.ApiErr 29 | } 30 | } 31 | } 32 | 33 | // FileDeleteCompletely 彻底删除文件,不经回收站直接永久删除文件 34 | func (p *OpenPanClient) FileDeleteCompletely(param *aliyunpan.FileBatchActionParam) (*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 35 | retryTime := 0 36 | 37 | RetryBegin: 38 | opParam := &openapi.FileIdentityPair{ 39 | DriveId: param.DriveId, 40 | FileId: param.FileId, 41 | } 42 | if result, err := p.apiClient.FileDelete(opParam); err == nil { 43 | return &aliyunpan.FileBatchActionResult{ 44 | FileId: result.FileId, 45 | Success: true, 46 | }, nil 47 | } else { 48 | // handle common error 49 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 50 | goto RetryBegin 51 | } else { 52 | return nil, apiErrorHandleResp.ApiErr 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /aliyunpan_open/file_directory.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 8 | "path" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func createFileEntity(f *openapi.FileItem) *aliyunpan.FileEntity { 14 | if f == nil { 15 | return nil 16 | } 17 | return &aliyunpan.FileEntity{ 18 | DriveId: f.DriveId, 19 | DomainId: f.DomainId, 20 | FileId: f.FileId, 21 | FileName: f.Name, 22 | FileSize: f.Size, 23 | FileType: f.Type, 24 | CreatedAt: apiutil.UtcTime2LocalFormat(f.CreatedAt), 25 | UpdatedAt: apiutil.UtcTime2LocalFormat(f.UpdatedAt), 26 | FileExtension: f.FileExtension, 27 | ParentFileId: f.ParentFileId, 28 | ContentHash: f.ContentHash, 29 | ContentHashName: f.ContentHashName, 30 | Path: "", 31 | Category: f.Category, 32 | } 33 | } 34 | 35 | // FileList 获取文件列表 36 | func (p *OpenPanClient) FileList(param *aliyunpan.FileListParam) (*aliyunpan.FileListResult, *apierror.ApiError) { 37 | retryTime := 0 38 | 39 | RetryBegin: 40 | result := &aliyunpan.FileListResult{ 41 | FileList: aliyunpan.FileList{}, 42 | NextMarker: "", 43 | } 44 | 45 | opParam := &openapi.FileListParam{ 46 | DriveId: param.DriveId, 47 | ParentFileId: param.ParentFileId, 48 | Limit: param.Limit, 49 | Marker: param.Marker, 50 | OrderBy: string(param.OrderBy), 51 | OrderDirection: string(param.OrderDirection), 52 | Type: "all", 53 | Fields: "*", 54 | } 55 | if flr, err := p.apiClient.FileList(opParam); err == nil { 56 | for k := range flr.Items { 57 | if flr.Items[k] == nil { 58 | continue 59 | } 60 | result.FileList = append(result.FileList, createFileEntity(flr.Items[k])) 61 | } 62 | result.NextMarker = flr.NextMarker 63 | } else { 64 | // handle common error 65 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 66 | goto RetryBegin 67 | } else { 68 | return nil, apiErrorHandleResp.ApiErr 69 | } 70 | } 71 | 72 | return result, nil 73 | } 74 | 75 | // FileListGetAll 获取指定目录下的所有文件列表 76 | func (p *OpenPanClient) FileListGetAll(param *aliyunpan.FileListParam, delayMilliseconds int) (aliyunpan.FileList, *apierror.ApiError) { 77 | internalParam := &aliyunpan.FileListParam{ 78 | OrderBy: param.OrderBy, 79 | OrderDirection: param.OrderDirection, 80 | DriveId: param.DriveId, 81 | ParentFileId: param.ParentFileId, 82 | Limit: param.Limit, 83 | Marker: param.Marker, 84 | } 85 | if internalParam.Limit <= 0 { 86 | internalParam.Limit = 100 87 | } 88 | 89 | fileList := aliyunpan.FileList{} 90 | result, err := p.FileList(internalParam) 91 | if err != nil || result == nil { 92 | return nil, err 93 | } 94 | fileList = append(fileList, result.FileList...) 95 | 96 | // more page? 97 | for len(result.NextMarker) > 0 { 98 | if delayMilliseconds > 0 { 99 | time.Sleep(time.Duration(delayMilliseconds) * time.Millisecond) 100 | } 101 | internalParam.Marker = result.NextMarker 102 | result, err = p.FileList(internalParam) 103 | if err == nil && result != nil { 104 | fileList = append(fileList, result.FileList...) 105 | } else { 106 | return nil, err 107 | } 108 | } 109 | return fileList, nil 110 | } 111 | 112 | // FileInfoById 通过FileId获取文件信息 113 | func (p *OpenPanClient) FileInfoById(driveId, fileId string) (*aliyunpan.FileEntity, *apierror.ApiError) { 114 | retryTime := 0 115 | 116 | RetryBegin: 117 | opParam := &openapi.FileIdentityPair{ 118 | DriveId: driveId, 119 | FileId: fileId, 120 | } 121 | if result, err := p.apiClient.FileGetDetailInfo(opParam); err == nil { 122 | return createFileEntity(result), nil 123 | } else { 124 | // handle common error 125 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 126 | goto RetryBegin 127 | } else { 128 | return nil, apiErrorHandleResp.ApiErr 129 | } 130 | } 131 | } 132 | 133 | // FileInfoByPath 通过路径获取文件详情,pathStr是绝对路径 134 | func (p *OpenPanClient) FileInfoByPath(driveId string, pathStr string) (fileInfo *aliyunpan.FileEntity, error *apierror.ApiError) { 135 | retryTime := 0 136 | 137 | if pathStr == "" { 138 | pathStr = "/" 139 | } 140 | //pathStr = path.Clean(pathStr) 141 | pathStr = strings.ReplaceAll(pathStr, "\\", "/") 142 | if !path.IsAbs(pathStr) { 143 | return nil, apierror.NewFailedApiError("pathStr必须是绝对路径") 144 | } 145 | if len(pathStr) > 1 { 146 | pathStr = path.Clean(pathStr) 147 | } 148 | // 根目录 149 | if pathStr == "/" { 150 | return aliyunpan.NewFileEntityForRootDir(), nil 151 | } 152 | 153 | // try cache 154 | if v := p.loadFilePathFromCache(driveId, pathStr); v != nil { 155 | return v, nil 156 | } 157 | 158 | RetryBegin: 159 | opParam := &openapi.FilePathPair{ 160 | DriveId: driveId, 161 | FilePath: pathStr, 162 | } 163 | if result, err := p.apiClient.FileGetDetailInfoByPath(opParam); err == nil { 164 | fileInfo = createFileEntity(result) 165 | fileInfo.Path = pathStr 166 | p.storeFilePathToCache(driveId, pathStr, fileInfo) 167 | return fileInfo, nil 168 | } else { 169 | // handle common error 170 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 171 | goto RetryBegin 172 | } else { 173 | return nil, apiErrorHandleResp.ApiErr 174 | } 175 | } 176 | } 177 | 178 | // FilesDirectoriesRecurseList 递归获取目录下的文件和目录列表 179 | func (p *OpenPanClient) FilesDirectoriesRecurseList(driveId string, path string, handleFileDirectoryFunc aliyunpan.HandleFileDirectoryFunc) aliyunpan.FileList { 180 | targetFileInfo, er := p.FileInfoByPath(driveId, path) 181 | if er != nil { 182 | if handleFileDirectoryFunc != nil { 183 | handleFileDirectoryFunc(0, path, nil, er) 184 | } 185 | return nil 186 | } 187 | if targetFileInfo.IsFolder() { 188 | // folder 189 | if handleFileDirectoryFunc != nil { 190 | handleFileDirectoryFunc(0, path, targetFileInfo, nil) 191 | } 192 | } else { 193 | // file 194 | if handleFileDirectoryFunc != nil { 195 | handleFileDirectoryFunc(0, path, targetFileInfo, nil) 196 | } 197 | return aliyunpan.FileList{targetFileInfo} 198 | } 199 | 200 | fld := &aliyunpan.FileList{} 201 | ok := p.recurseList(driveId, targetFileInfo, 1, handleFileDirectoryFunc, fld) 202 | if !ok { 203 | return nil 204 | } 205 | return *fld 206 | } 207 | 208 | func (p *OpenPanClient) recurseList(driveId string, folderInfo *aliyunpan.FileEntity, depth int, handleFileDirectoryFunc aliyunpan.HandleFileDirectoryFunc, fld *aliyunpan.FileList) bool { 209 | flp := &aliyunpan.FileListParam{ 210 | DriveId: driveId, 211 | ParentFileId: folderInfo.FileId, 212 | } 213 | r, apiError := p.FileListGetAll(flp, 0) 214 | if apiError != nil { 215 | if handleFileDirectoryFunc != nil { 216 | handleFileDirectoryFunc(depth, folderInfo.Path, nil, apiError) 217 | } 218 | return false 219 | } 220 | ok := true 221 | for _, fi := range r { 222 | fi.Path = strings.ReplaceAll(folderInfo.Path+aliyunpan.PathSeparator+fi.FileName, "//", "/") 223 | *fld = append(*fld, fi) 224 | if fi.IsFolder() { 225 | if handleFileDirectoryFunc != nil { 226 | ok = handleFileDirectoryFunc(depth, fi.Path, fi, nil) 227 | } 228 | ok = p.recurseList(driveId, fi, depth+1, handleFileDirectoryFunc, fld) 229 | } else { 230 | if handleFileDirectoryFunc != nil { 231 | ok = handleFileDirectoryFunc(depth, fi.Path, fi, nil) 232 | } 233 | } 234 | if !ok { 235 | return false 236 | } 237 | } 238 | return true 239 | } 240 | -------------------------------------------------------------------------------- /aliyunpan_open/file_download.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 9 | "github.com/tickstep/library-go/logger" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // GetFileDownloadUrl 获取文件下载URL路径 15 | func (p *OpenPanClient) GetFileDownloadUrl(param *aliyunpan.GetFileDownloadUrlParam) (*aliyunpan.GetFileDownloadUrlResult, *apierror.ApiError) { 16 | retryTime := 0 17 | 18 | RetryBegin: 19 | opParam := &openapi.FileDownloadUrlParam{ 20 | DriveId: param.DriveId, 21 | FileId: param.FileId, 22 | ExpireSec: param.ExpireSec, 23 | } 24 | if result, err := p.apiClient.FileGetDownloadUrl(opParam); err == nil { 25 | return &aliyunpan.GetFileDownloadUrlResult{ 26 | Method: result.Method, 27 | Url: result.Url, 28 | InternalUrl: "", 29 | CdnUrl: "", 30 | Expiration: apiutil.UtcTime2LocalFormat(result.Expiration), 31 | Size: result.Size, 32 | Description: result.Description, 33 | }, nil 34 | } else { 35 | // handle common error 36 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 37 | goto RetryBegin 38 | } else { 39 | return nil, apiErrorHandleResp.ApiErr 40 | } 41 | } 42 | } 43 | 44 | // DownloadFileData 下载文件内容 45 | func (p *OpenPanClient) DownloadFileData(downloadFileUrl string, fileRange aliyunpan.FileDownloadRange, downloadFunc aliyunpan.DownloadFuncCallback) *apierror.ApiError { 46 | // url 47 | fullUrl := &strings.Builder{} 48 | fmt.Fprintf(fullUrl, "%s", downloadFileUrl) 49 | logger.Verboseln("do request url: " + fullUrl.String()) 50 | 51 | // header 52 | headers := map[string]string{ 53 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 54 | "referer": "https://www.aliyundrive.com/", 55 | } 56 | 57 | // download data resume 58 | if fileRange.Offset != 0 || fileRange.End != 0 { 59 | rangeStr := "bytes=" + strconv.FormatInt(fileRange.Offset, 10) + "-" 60 | if fileRange.End != 0 { 61 | rangeStr += strconv.FormatInt(fileRange.End, 10) 62 | } 63 | headers["range"] = rangeStr 64 | } 65 | logger.Verboseln("do request url: " + fullUrl.String()) 66 | 67 | // request callback 68 | _, err := downloadFunc("GET", fullUrl.String(), headers) 69 | //resp, err := p.client.Req("GET", fullUrl.String(), nil, headers) 70 | 71 | if err != nil { 72 | logger.Verboseln("download file data response failed") 73 | return apierror.NewApiErrorWithError(err) 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /aliyunpan_open/file_mkdir.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 8 | "strings" 9 | ) 10 | 11 | // Mkdir 创建文件夹 12 | func (p *OpenPanClient) Mkdir(driveId, parentFileId, dirName string) (*aliyunpan.MkdirResult, *apierror.ApiError) { 13 | retryTime := 0 14 | if !apiutil.CheckFileNameValid(dirName) { 15 | return nil, apierror.NewFailedApiError("文件夹名不能包含特殊字符:" + apiutil.FileNameSpecialChars) 16 | } 17 | 18 | RetryBegin: 19 | opParam := &openapi.FileUploadCreateParam{ 20 | DriveId: driveId, 21 | ParentFileId: parentFileId, 22 | Name: dirName, 23 | Type: "folder", 24 | CheckNameMode: "auto_rename", 25 | } 26 | if result, err := p.apiClient.FileUploadCreate(opParam); err == nil { 27 | return &aliyunpan.MkdirResult{ 28 | ParentFileId: result.ParentFileId, 29 | Type: "folder", 30 | FileId: result.FileId, 31 | DriveId: result.DriveId, 32 | FileName: result.FileName, 33 | }, nil 34 | } else { 35 | // handle common error 36 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 37 | goto RetryBegin 38 | } else { 39 | return nil, apiErrorHandleResp.ApiErr 40 | } 41 | } 42 | } 43 | 44 | // MkdirByFullPath 通过绝对路径创建文件夹 45 | func (p *OpenPanClient) MkdirByFullPath(driveId, fullPath string) (*aliyunpan.MkdirResult, *apierror.ApiError) { 46 | fullPath = strings.ReplaceAll(fullPath, "//", "/") 47 | fullPath = strings.Trim(fullPath, " ") 48 | if fullPath == "/" { 49 | return &aliyunpan.MkdirResult{ 50 | ParentFileId: "", 51 | Type: "folder", 52 | FileId: "root", 53 | DomainId: "", 54 | DriveId: "", 55 | FileName: "", 56 | EncryptMode: "", 57 | }, nil 58 | } 59 | pathSlice := strings.Split(fullPath, "/") 60 | return p.MkdirRecursive(driveId, "", "", 0, pathSlice) 61 | } 62 | 63 | func (p *OpenPanClient) MkdirRecursive(driveId, parentFileId string, fullPath string, index int, pathSlice []string) (*aliyunpan.MkdirResult, *apierror.ApiError) { 64 | r := &aliyunpan.MkdirResult{} 65 | if parentFileId == "" { 66 | // default root "/" entity 67 | parentFileId = "root" 68 | if index == 0 && len(pathSlice) == 1 { 69 | // root path "/" 70 | r.FileId = parentFileId 71 | return r, nil 72 | } 73 | 74 | fullPath = "" 75 | return p.MkdirRecursive(driveId, parentFileId, fullPath, index+1, pathSlice) 76 | } 77 | if index >= len(pathSlice) { 78 | r.ParentFileId = "root" 79 | r.FileId = parentFileId 80 | r.Type = "folder" 81 | r.FileName = pathSlice[index-1] 82 | return r, nil 83 | } 84 | 85 | // existed? 86 | thisDirPath := fullPath + "/" + pathSlice[index] 87 | fileEntity, e := p.FileInfoByPath(driveId, thisDirPath) 88 | if e != nil && e.Code != apierror.ApiCodeFileNotFoundCode { 89 | return nil, e 90 | } 91 | if fileEntity != nil { 92 | // existed 93 | if fileEntity.IsFile() { 94 | return nil, apierror.NewFailedApiError("the fileName is a file not a folder") 95 | } 96 | return p.MkdirRecursive(driveId, fileEntity.FileId, fullPath+"/"+pathSlice[index], index+1, pathSlice) 97 | } 98 | 99 | // not existed, mkdir dir 100 | name := pathSlice[index] 101 | if !apiutil.CheckFileNameValid(name) { 102 | r.FileId = "" 103 | return r, apierror.NewFailedApiError("文件夹名不能包含特殊字符:" + apiutil.FileNameSpecialChars) 104 | } 105 | 106 | rs, err := p.Mkdir(driveId, parentFileId, name) 107 | if err != nil { 108 | r.FileId = "" 109 | return r, err 110 | } 111 | 112 | if (index + 1) >= len(pathSlice) { 113 | return rs, nil 114 | } else { 115 | return p.MkdirRecursive(driveId, rs.FileId, fullPath+"/"+pathSlice[index], index+1, pathSlice) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /aliyunpan_open/file_move.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 7 | ) 8 | 9 | // FileMove 移动文件 10 | func (p *OpenPanClient) FileMove(param *aliyunpan.FileMoveParam) (*aliyunpan.FileMoveResult, *apierror.ApiError) { 11 | retryTime := 0 12 | 13 | RetryBegin: 14 | opParam := &openapi.FileMoveParam{ 15 | DriveId: param.DriveId, 16 | FileId: param.FileId, 17 | ToParentFileId: param.ToParentFileId, 18 | } 19 | if result, err := p.apiClient.FileMove(opParam); err == nil { 20 | return &aliyunpan.FileMoveResult{ 21 | FileId: result.FileId, 22 | Success: true, 23 | }, nil 24 | } else { 25 | // handle common error 26 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 27 | goto RetryBegin 28 | } else { 29 | return nil, apiErrorHandleResp.ApiErr 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /aliyunpan_open/file_rename.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 6 | ) 7 | 8 | // FileRename 重命名文件 9 | func (p *OpenPanClient) FileRename(driveId, renameFileId, newName string) (bool, *apierror.ApiError) { 10 | retryTime := 0 11 | 12 | RetryBegin: 13 | opParam := &openapi.FileUpdateParam{ 14 | DriveId: driveId, 15 | FileId: renameFileId, 16 | Name: newName, 17 | CheckNameMode: "refuse", 18 | } 19 | if result, err := p.apiClient.FileUpdate(opParam); err == nil { 20 | return result.Name != "", nil 21 | } else { 22 | // handle common error 23 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 24 | goto RetryBegin 25 | } else { 26 | return false, apiErrorHandleResp.ApiErr 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /aliyunpan_open/file_share.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 8 | ) 9 | 10 | // ShareLinkCreate 创建文件分享 11 | func (p *OpenPanClient) ShareLinkCreate(param aliyunpan.ShareCreateParam) (*aliyunpan.ShareEntity, *apierror.ApiError) { 12 | retryTime := 0 13 | 14 | RetryBegin: 15 | opParam := &openapi.FileShareCreateParam{ 16 | DriveId: param.DriveId, 17 | FileIdList: param.FileIdList, 18 | Expiration: param.Expiration, 19 | SharePwd: param.SharePwd, 20 | } 21 | // format time 22 | if opParam.Expiration != "" { 23 | opParam.Expiration = apiutil.LocalTime2UtcFormat(param.Expiration) 24 | } 25 | if result, err := p.apiClient.FileShareCreate(opParam); err == nil { 26 | return &aliyunpan.ShareEntity{ 27 | Creator: result.Creator, 28 | DriveId: param.DriveId, 29 | ShareId: result.ShareId, 30 | ShareName: "", 31 | SharePwd: result.SharePwd, 32 | ShareUrl: result.ShareUrl, 33 | FileIdList: nil, 34 | SaveCount: 0, 35 | Expiration: apiutil.UtcTime2LocalFormat(result.Expiration), 36 | UpdatedAt: apiutil.UtcTime2LocalFormat(result.UpdatedAt), 37 | CreatedAt: apiutil.UtcTime2LocalFormat(result.CreatedAt), 38 | Status: result.Status, 39 | FirstFile: nil, 40 | }, nil 41 | } else { 42 | // handle common error 43 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 44 | goto RetryBegin 45 | } else { 46 | return nil, apiErrorHandleResp.ApiErr 47 | } 48 | } 49 | } 50 | 51 | // FastShareLinkCreate 创建文件快传 52 | func (p *OpenPanClient) FastShareLinkCreate(param aliyunpan.FastShareCreateParam) (*aliyunpan.FastShareCreateResult, *apierror.ApiError) { 53 | retryTime := 0 54 | 55 | opParam := &openapi.FileFastShareCreateParam{ 56 | DriveFileList: []openapi.FileFastShareFileItem{}, 57 | } 58 | for _, fileId := range param.FileIdList { 59 | opParam.DriveFileList = append(opParam.DriveFileList, openapi.FileFastShareFileItem{ 60 | DriveId: param.DriveId, 61 | FileId: fileId, 62 | }) 63 | } 64 | RetryBegin: 65 | if result, err := p.apiClient.FileFastShareCreate(opParam); err == nil { 66 | driveFileList := []aliyunpan.FastShareFileItem{} 67 | for _, item := range result.DriveFileList { 68 | driveFileList = append(driveFileList, aliyunpan.FastShareFileItem{ 69 | DriveId: item.DriveId, 70 | FileId: item.FileId, 71 | }) 72 | } 73 | return &aliyunpan.FastShareCreateResult{ 74 | ShareId: result.ShareId, 75 | ShareName: "", 76 | ShareUrl: result.ShareUrl, 77 | Expiration: apiutil.UtcTime2LocalFormat(result.Expiration), 78 | DriveFileList: driveFileList, 79 | Expired: false, 80 | }, nil 81 | } else { 82 | // handle common error 83 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 84 | goto RetryBegin 85 | } else { 86 | return nil, apiErrorHandleResp.ApiErr 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /aliyunpan_open/file_upload.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 8 | "github.com/tickstep/library-go/logger" 9 | "strings" 10 | ) 11 | 12 | // CheckUploadFilePreHash 文件PreHash检测,当PreHash检查为false的文件肯定不支持秒传 13 | func (p *OpenPanClient) CheckUploadFilePreHash(param *aliyunpan.FileUploadCheckPreHashParam) (bool, *apierror.ApiError) { 14 | retryTime := 0 15 | 16 | RetryBegin: 17 | opParam := &openapi.FileUploadCheckPreHashParam{ 18 | DriveId: param.DriveId, 19 | ParentFileId: param.ParentFileId, 20 | Name: param.Name, 21 | Type: "file", 22 | CheckNameMode: "ignore", 23 | Size: param.Size, 24 | PreHash: param.PreHash, 25 | } 26 | if result, err := p.apiClient.FileUploadCheckPreHash(opParam); err == nil { 27 | return result, nil 28 | } else { 29 | // handle common error 30 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 31 | goto RetryBegin 32 | } else { 33 | return false, apiErrorHandleResp.ApiErr 34 | } 35 | } 36 | } 37 | 38 | // CreateUploadFile 创建上传文件,如果文件已经上传过可以直接秒传 39 | func (p *OpenPanClient) CreateUploadFile(param *aliyunpan.CreateFileUploadParam) (*aliyunpan.CreateFileUploadResult, *apierror.ApiError) { 40 | retryTime := 0 41 | 42 | // 计算分片数量 43 | partInfoListParam := param.PartInfoList 44 | if len(param.PartInfoList) == 0 { 45 | blockSize := aliyunpan.DefaultChunkSize 46 | if param.BlockSize > 0 { 47 | blockSize = param.BlockSize 48 | } 49 | partInfoListParam = aliyunpan.GenerateFileUploadPartInfoListWithChunkSize(param.Size, blockSize) 50 | } 51 | realPartInfoList := []*openapi.PartInfoItem{} 52 | for _, v := range partInfoListParam { 53 | realPartInfoList = append(realPartInfoList, &openapi.PartInfoItem{ 54 | PartNumber: v.PartNumber, 55 | }) 56 | } 57 | 58 | RetryBegin: 59 | opParam := &openapi.FileUploadCreateParam{ 60 | DriveId: param.DriveId, 61 | ParentFileId: param.ParentFileId, 62 | Name: param.Name, 63 | Type: "file", 64 | CheckNameMode: param.CheckNameMode, 65 | Size: param.Size, 66 | PartInfoList: realPartInfoList, 67 | ContentHash: param.ContentHash, 68 | ContentHashName: param.ContentHashName, 69 | ProofCode: param.ProofCode, 70 | ProofVersion: param.ProofVersion, 71 | LocalCreatedAt: param.LocalCreatedAt, 72 | LocalModifiedAt: param.LocalModifiedAt, 73 | } 74 | if opParam.ContentHashName == "" { 75 | opParam.ContentHashName = "sha1" 76 | } 77 | if opParam.ParentFileId == "" { 78 | opParam.ParentFileId = aliyunpan.DefaultRootParentFileId 79 | } 80 | if opParam.ProofVersion == "" { 81 | opParam.ProofVersion = "v1" 82 | } 83 | if opParam.CheckNameMode == "" { 84 | opParam.CheckNameMode = "auto_rename" 85 | } 86 | 87 | if result, err := p.apiClient.FileUploadCreate(opParam); err == nil { 88 | partInfoListResult := []aliyunpan.FileUploadPartInfoResult{} 89 | for _, v := range result.PartInfoList { 90 | partInfoListResult = append(partInfoListResult, aliyunpan.FileUploadPartInfoResult{ 91 | PartNumber: v.PartNumber, 92 | UploadURL: v.UploadUrl, 93 | InternalUploadURL: "", 94 | ContentType: "", 95 | }) 96 | } 97 | return &aliyunpan.CreateFileUploadResult{ 98 | ParentFileId: result.ParentFileId, 99 | PartInfoList: partInfoListResult, 100 | UploadId: result.UploadId, 101 | RapidUpload: result.RapidUpload, 102 | Type: "", 103 | FileId: result.FileId, 104 | DomainId: "", 105 | DriveId: result.DriveId, 106 | FileName: result.FileName, 107 | EncryptMode: "", 108 | Location: "", 109 | }, nil 110 | } else { 111 | // handle common error 112 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 113 | goto RetryBegin 114 | } else { 115 | return nil, apiErrorHandleResp.ApiErr 116 | } 117 | } 118 | } 119 | 120 | // GetUploadUrl 获取上传数据链接参数 121 | // 因为有些文件过大,或者暂定上传后,然后过段时间再继续上传,这时候之前的上传链接可能已经失效了,所以需要重新获取上传数据的链接 122 | func (p *OpenPanClient) GetUploadUrl(param *aliyunpan.GetUploadUrlParam) (*aliyunpan.GetUploadUrlResult, *apierror.ApiError) { 123 | retryTime := 0 124 | 125 | realPartInfoList := []*openapi.PartInfoItem{} 126 | for _, v := range param.PartInfoList { 127 | realPartInfoList = append(realPartInfoList, &openapi.PartInfoItem{ 128 | PartNumber: v.PartNumber, 129 | }) 130 | } 131 | RetryBegin: 132 | opParam := &openapi.FileUploadGetUploadUrlParam{ 133 | DriveId: param.DriveId, 134 | FileId: param.FileId, 135 | UploadId: param.UploadId, 136 | PartInfoList: realPartInfoList, 137 | } 138 | if result, err := p.apiClient.FileUploadGetUploadUrl(opParam); err == nil { 139 | partInfoListResult := []aliyunpan.FileUploadPartInfoResult{} 140 | for _, v := range result.PartInfoList { 141 | partInfoListResult = append(partInfoListResult, aliyunpan.FileUploadPartInfoResult{ 142 | PartNumber: v.PartNumber, 143 | UploadURL: v.UploadUrl, 144 | InternalUploadURL: "", 145 | ContentType: "", 146 | }) 147 | } 148 | return &aliyunpan.GetUploadUrlResult{ 149 | DomainId: "", 150 | DriveId: result.DriveId, 151 | FileId: result.FileId, 152 | PartInfoList: partInfoListResult, 153 | UploadId: result.UploadId, 154 | CreateAt: result.CreatedAt, 155 | }, nil 156 | } else { 157 | // handle common error 158 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 159 | goto RetryBegin 160 | } else { 161 | return nil, apiErrorHandleResp.ApiErr 162 | } 163 | } 164 | } 165 | 166 | // UploadFileData 上传文件数据 167 | func (p *OpenPanClient) UploadFileData(uploadUrl string, uploadFunc aliyunpan.UploadFunc) *apierror.ApiError { 168 | // header 169 | header := map[string]string{ 170 | "referer": "https://www.aliyundrive.com/", 171 | } 172 | 173 | // url 174 | fullUrl := &strings.Builder{} 175 | fmt.Fprintf(fullUrl, "%s", uploadUrl) 176 | logger.Verboseln("do request url: " + fullUrl.String()) 177 | 178 | // request 179 | if uploadFunc != nil { 180 | resp, err := uploadFunc("PUT", fullUrl.String(), header) 181 | if err != nil || (resp != nil && resp.StatusCode != 200) { 182 | logger.Verboseln("upload file data chunk error ", err) 183 | return apierror.NewFailedApiError("update data error") 184 | } 185 | } 186 | return nil 187 | } 188 | 189 | // GetUploadedPartInfo 获取指定文件已经上传的分片信息(可能会有分页) 190 | func (p *OpenPanClient) GetUploadedPartInfo(param *aliyunpan.GetUploadedPartsParam) (*aliyunpan.GetUploadedPartsResult, *apierror.ApiError) { 191 | retryTime := 0 192 | 193 | RetryBegin: 194 | opParam := &openapi.FileUploadListUploadedPartsParam{ 195 | DriveId: param.DriveId, 196 | FileId: param.FileId, 197 | UploadId: param.UploadId, 198 | PartNumberMarker: param.PartNumberMarker, 199 | } 200 | if result, err := p.apiClient.FileUploadListUploadedParts(opParam); err == nil { 201 | uploadedParts := []*aliyunpan.GetUploadedPartItem{} 202 | if result.UploadedParts != nil { 203 | for _, v := range result.UploadedParts { 204 | uploadedParts = append(uploadedParts, &aliyunpan.GetUploadedPartItem{ 205 | Etag: v.Etag, 206 | PartNumber: v.PartNumber, 207 | PartSize: v.PartSize, 208 | }) 209 | } 210 | } 211 | return &aliyunpan.GetUploadedPartsResult{ 212 | DriveId: result.DriveId, 213 | UploadId: result.UploadId, 214 | ParallelUpload: result.ParallelUpload, 215 | UploadedParts: uploadedParts, 216 | NextPartNumberMarker: result.NextPartNumberMarker, 217 | }, nil 218 | } else { 219 | // handle common error 220 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 221 | goto RetryBegin 222 | } else { 223 | return nil, apiErrorHandleResp.ApiErr 224 | } 225 | } 226 | } 227 | 228 | // GetUploadedPartInfoAllItem 获取指定文件已经上传的所有分片信息 229 | func (p *OpenPanClient) GetUploadedPartInfoAllItem(param *aliyunpan.GetUploadedPartsParam) (*aliyunpan.GetUploadedPartsResult, *apierror.ApiError) { 230 | result, err := p.GetUploadedPartInfo(param) 231 | if err != nil || result == nil { 232 | return nil, err 233 | } 234 | param.PartNumberMarker = result.NextPartNumberMarker 235 | for len(param.PartNumberMarker) > 0 { 236 | r, er := p.GetUploadedPartInfo(param) 237 | if er != nil || r == nil { 238 | return result, err 239 | } 240 | result.UploadedParts = append(result.UploadedParts, r.UploadedParts...) 241 | param.PartNumberMarker = r.NextPartNumberMarker 242 | } 243 | return result, nil 244 | } 245 | 246 | // CompleteUploadFile 完成文件上传确认。完成文件数据上传后,需要调用该接口文件才会显示再网盘中 247 | func (p *OpenPanClient) CompleteUploadFile(param *aliyunpan.CompleteUploadFileParam) (*aliyunpan.CompleteUploadFileResult, *apierror.ApiError) { 248 | retryTime := 0 249 | 250 | RetryBegin: 251 | opParam := &openapi.FileUploadCompleteParam{ 252 | DriveId: param.DriveId, 253 | FileId: param.FileId, 254 | UploadId: param.UploadId, 255 | } 256 | if result, err := p.apiClient.FileUploadComplete(opParam); err == nil { 257 | return &aliyunpan.CompleteUploadFileResult{ 258 | DriveId: result.DriveId, 259 | DomainId: "", 260 | FileId: result.FileId, 261 | Name: result.Name, 262 | Type: result.Type, 263 | Size: result.Size, 264 | UploadId: param.UploadId, 265 | ParentFileId: result.ParentFileId, 266 | Crc64Hash: "", 267 | ContentHash: result.ContentHash, 268 | ContentHashName: result.ContentHashName, 269 | CreatedAt: result.LocalCreatedAt, 270 | }, nil 271 | } else { 272 | // handle common error 273 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 274 | goto RetryBegin 275 | } else { 276 | return nil, apiErrorHandleResp.ApiErr 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /aliyunpan_open/file_video.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 6 | ) 7 | 8 | // VideoGetPreviewPlayInfo 获取视频预览信息,调用该接口会触发视频云端转码 9 | func (p *OpenPanClient) VideoGetPreviewPlayInfo(param *aliyunpan.VideoGetPreviewPlayInfoParam) (*aliyunpan.VideoGetPreviewPlayInfoResult, error) { 10 | retryTime := 0 11 | 12 | RetryBegin: 13 | opParam := &openapi.VideoGetPreviewPlayInfoParam{ 14 | DriveId: param.DriveId, 15 | FileId: param.FileId, 16 | Category: "live_transcoding", 17 | } 18 | if result, err := p.apiClient.VideoGetPreviewPlayInfo(opParam); err == nil { 19 | return &aliyunpan.VideoGetPreviewPlayInfoResult{ 20 | DriveId: result.DriveId, 21 | FileId: result.FileId, 22 | }, nil 23 | } else { 24 | // handle common error 25 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 26 | goto RetryBegin 27 | } else { 28 | return nil, apiErrorHandleResp.ApiErr 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /aliyunpan_open/open_pan_client.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi" 9 | "github.com/tickstep/library-go/logger" 10 | "github.com/tickstep/library-go/requester" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // ApiRetryMaxTimes API失败重试次数 18 | ApiRetryMaxTimes int = 3 19 | ) 20 | 21 | type ( 22 | // AccessTokenRefreshCallback Token刷新回调 23 | AccessTokenRefreshCallback func(userId string, newToken openapi.ApiToken) error 24 | 25 | // OpenPanClient 开放接口客户端 26 | OpenPanClient struct { 27 | httpClient *requester.HTTPClient // http 客户端 28 | apiClient *openapi.AliPanClient 29 | 30 | accessTokenRefreshCallback AccessTokenRefreshCallback 31 | 32 | // 缓存 33 | cacheMutex *sync.Mutex 34 | useCache bool 35 | // 网盘文件绝对路径到网盘文件信息实体映射缓存,避免FileInfoByPath频繁访问服务器触发风控 36 | filePathCacheMap sync.Map 37 | } 38 | ) 39 | 40 | // NewOpenPanClient 创建开放接口客户端 41 | func NewOpenPanClient(apiConfig openapi.ApiConfig, apiToken openapi.ApiToken, tokenCallback AccessTokenRefreshCallback) *OpenPanClient { 42 | myclient := requester.NewHTTPClient() 43 | 44 | return &OpenPanClient{ 45 | httpClient: myclient, 46 | apiClient: openapi.NewAliPanClient(apiToken, apiConfig), 47 | accessTokenRefreshCallback: tokenCallback, 48 | cacheMutex: &sync.Mutex{}, 49 | useCache: false, 50 | filePathCacheMap: sync.Map{}, 51 | } 52 | } 53 | 54 | // SetAccessTokenRefreshCallback 设置 Token 回调 55 | func (p *OpenPanClient) SetAccessTokenRefreshCallback(tokenCallback AccessTokenRefreshCallback) { 56 | p.accessTokenRefreshCallback = tokenCallback 57 | } 58 | 59 | // SetTimeout 设置 http 请求超时时间 60 | func (p *OpenPanClient) SetTimeout(t time.Duration) { 61 | if p.apiClient != nil { 62 | p.apiClient.SetTimeout(t) 63 | } 64 | 65 | if p.httpClient != nil { 66 | p.httpClient.Timeout = t 67 | } 68 | } 69 | 70 | // GetAccessToken 获取AccessToken鉴权字符串 71 | func (p *OpenPanClient) GetAccessToken() string { 72 | return p.apiClient.GetAccessToken() 73 | } 74 | 75 | // RefreshNewAccessToken 获取新的AccessToken 76 | func (p *OpenPanClient) RefreshNewAccessToken() error { 77 | if p.apiClient.GetApiConfig().TicketId == "" { 78 | return errors.New("not support refresh token automatically") 79 | } 80 | fullUrl := &strings.Builder{} 81 | fmt.Fprintf(fullUrl, "https://api.tickstep.com/auth/tickstep/aliyunpan/token/openapi/%s/refresh?userId=%s", 82 | p.apiClient.GetApiConfig().TicketId, p.apiClient.GetApiConfig().UserId) 83 | logger.Verboseln("do request url: " + fullUrl.String()) 84 | 85 | // request 86 | h := p.apiClient.Headers() 87 | h["old-token"] = p.GetAccessToken() 88 | data, err := p.httpClient.Fetch("GET", fullUrl.String(), nil, h) 89 | if err != nil { 90 | logger.Verboseln("get new access token error ", err) 91 | return err 92 | } 93 | 94 | // parse result 95 | type respEntity struct { 96 | Code int `json:"code"` 97 | Data *openapi.ApiToken `json:"data"` 98 | Msg string `json:"msg"` 99 | } 100 | r := &respEntity{} 101 | if err2 := json.Unmarshal(data, r); err2 != nil { 102 | logger.Verboseln("parse access token result json error ", err2) 103 | return err2 104 | } 105 | if r.Code != 0 { 106 | return errors.New(r.Msg) 107 | } 108 | token := *r.Data 109 | p.apiClient.UpdateToken(token) 110 | if p.accessTokenRefreshCallback != nil { 111 | p.accessTokenRefreshCallback(p.apiClient.GetApiConfig().UserId, token) 112 | } 113 | return nil 114 | } 115 | 116 | // UpdateUserId 更新用户ID 117 | func (p *OpenPanClient) UpdateUserId(userId string) { 118 | c := p.apiClient.GetApiConfig() 119 | c.UserId = userId 120 | p.apiClient.UpdateApiConfig(c) 121 | } 122 | 123 | // EnableCache 启用缓存 124 | func (p *OpenPanClient) EnableCache() { 125 | p.cacheMutex.Lock() 126 | p.cacheMutex.Unlock() 127 | p.useCache = true 128 | } 129 | 130 | // ClearCache 清除已经缓存的数据 131 | func (p *OpenPanClient) ClearCache() { 132 | p.cacheMutex.Lock() 133 | p.cacheMutex.Unlock() 134 | p.filePathCacheMap = sync.Map{} 135 | } 136 | 137 | // DisableCache 禁用缓存 138 | func (p *OpenPanClient) DisableCache() { 139 | p.cacheMutex.Lock() 140 | p.cacheMutex.Unlock() 141 | p.useCache = false 142 | } 143 | 144 | // storeFilePathToCache 存储文件信息到缓存 145 | func (p *OpenPanClient) storeFilePathToCache(driveId, pathStr string, fileEntity *aliyunpan.FileEntity) { 146 | p.cacheMutex.Lock() 147 | p.cacheMutex.Unlock() 148 | if !p.useCache { 149 | return 150 | } 151 | pathStr = formatPathStyle(pathStr) 152 | cache, _ := p.filePathCacheMap.LoadOrStore(driveId, &sync.Map{}) 153 | cache.(*sync.Map).Store(pathStr, fileEntity) 154 | } 155 | 156 | // loadFilePathFromCache 从缓存获取文件信息 157 | func (p *OpenPanClient) loadFilePathFromCache(driveId, pathStr string) *aliyunpan.FileEntity { 158 | p.cacheMutex.Lock() 159 | p.cacheMutex.Unlock() 160 | if !p.useCache { 161 | return nil 162 | } 163 | pathStr = formatPathStyle(pathStr) 164 | cache, _ := p.filePathCacheMap.LoadOrStore(driveId, &sync.Map{}) 165 | s := cache.(*sync.Map) 166 | if v, ok := s.Load(pathStr); ok { 167 | logger.Verboseln("file path cache hit: ", pathStr) 168 | return v.(*aliyunpan.FileEntity) 169 | } 170 | return nil 171 | } 172 | 173 | func formatPathStyle(pathStr string) string { 174 | pathStr = strings.ReplaceAll(pathStr, "\\", "/") 175 | if pathStr != "/" { 176 | pathStr = strings.TrimSuffix(pathStr, "/") 177 | } 178 | return pathStr 179 | } 180 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/ali_api_error.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // AliApiErrResult openapi错误响应 12 | AliApiErrResult struct { 13 | HttpStatusCode int `json:"http_status_code"` 14 | Code string `json:"code"` 15 | Message string `json:"message"` 16 | extra map[string]interface{} `json:"-"` 17 | } 18 | 19 | // AliApiDefaultErrResult openapi默认错误响应,例如404错误 20 | AliApiDefaultErrResult struct { 21 | Timestamp string `json:"timestamp"` 22 | Status int64 `json:"status"` 23 | Error string `json:"error"` 24 | Path string `json:"path"` 25 | } 26 | ) 27 | 28 | func NewAliApiError(httpStatusCode int, code, msg string) *AliApiErrResult { 29 | return &AliApiErrResult{ 30 | HttpStatusCode: httpStatusCode, 31 | Code: code, 32 | Message: msg, 33 | } 34 | } 35 | func NewAliApiHttpError(msg string) *AliApiErrResult { 36 | return &AliApiErrResult{ 37 | HttpStatusCode: 200, 38 | Code: "TS.HttpError", 39 | Message: msg, 40 | } 41 | } 42 | func NewAliApiAppError(msg string) *AliApiErrResult { 43 | return &AliApiErrResult{ 44 | HttpStatusCode: 200, 45 | Code: "TS.AppError", 46 | Message: msg, 47 | } 48 | } 49 | 50 | func (a *AliApiErrResult) PutExtra(key string, value interface{}) *AliApiErrResult { 51 | if a.extra == nil { 52 | a.extra = map[string]interface{}{} 53 | } 54 | a.extra[key] = value 55 | return a 56 | } 57 | func (a *AliApiErrResult) GetExtra(key string) interface{} { 58 | if a.extra == nil { 59 | return nil 60 | } 61 | if v, ok := a.extra[key]; ok { 62 | return v 63 | } 64 | return nil 65 | } 66 | 67 | // ParseCommonOpenApiError 解析阿里云盘API错误,如果没有错误则返回nil 68 | func ParseCommonOpenApiError(resp *http.Response) ([]byte, *AliApiErrResult) { 69 | if resp == nil { 70 | return nil, nil 71 | } 72 | 73 | // read response text 74 | data, e := ioutil.ReadAll(resp.Body) 75 | if e != nil { 76 | return nil, NewAliApiError(resp.StatusCode, "TS.ReadError", e.Error()) 77 | } 78 | 79 | // 非json错误 80 | plainText := string(data) 81 | if !strings.HasPrefix(plainText, "{") { 82 | formatStatusCode := int64(resp.StatusCode / 100.0 * 100) 83 | if formatStatusCode != 200 { 84 | return nil, &AliApiErrResult{ 85 | HttpStatusCode: resp.StatusCode, 86 | Code: plainText, 87 | Message: plainText, 88 | } 89 | } 90 | } 91 | 92 | // 默认错误 93 | errDefaultResult := &AliApiDefaultErrResult{} 94 | if err := json.Unmarshal(data, errDefaultResult); err == nil { 95 | if errDefaultResult.Error != "" && errDefaultResult.Status != 0 { 96 | errResult := &AliApiErrResult{ 97 | HttpStatusCode: resp.StatusCode, 98 | Code: errDefaultResult.Error, 99 | Message: errDefaultResult.Error, 100 | } 101 | return nil, errResult 102 | } 103 | } 104 | 105 | // 业务错误 106 | errResult := &AliApiErrResult{} 107 | if err := json.Unmarshal(data, errResult); err == nil { 108 | if errResult.Code != "" { 109 | errResult.HttpStatusCode = resp.StatusCode 110 | // headers 111 | if hv := resp.Header.Get("x-retry-after"); hv != "" { 112 | errResult.PutExtra("x-retry-after", hv) 113 | } 114 | return nil, errResult 115 | } 116 | } 117 | return data, nil 118 | } 119 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/ali_pan_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstea. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package openapi 16 | 17 | import ( 18 | "github.com/tickstep/library-go/requester" 19 | "strings" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | const ( 25 | // PathSeparator 路径分隔符 26 | PathSeparator = "/" 27 | ) 28 | 29 | type ( 30 | // ApiToken 登录Token 31 | ApiToken struct { 32 | AccessToken string `json:"accessToken"` 33 | ExpiredAt int64 `json:"expired"` 34 | } 35 | 36 | // ApiConfig 存储客户端相关配置参数 37 | ApiConfig struct { 38 | TicketId string `json:"ticket_id"` 39 | UserId string `json:"user_id"` 40 | ClientId string `json:"clientId"` 41 | ClientSecret string `json:"clientSecret"` 42 | } 43 | 44 | AliPanClient struct { 45 | httpclient *requester.HTTPClient // http 客户端 46 | token ApiToken 47 | apiConfig ApiConfig 48 | 49 | cacheMutex *sync.Mutex 50 | useCache bool 51 | // 网盘文件绝对路径到网盘文件信息实体映射缓存,避免FileInfoByPath频繁访问服务器触发风控 52 | filePathCacheMap sync.Map 53 | } 54 | ) 55 | 56 | func NewAliPanClient(token ApiToken, apiConfig ApiConfig) *AliPanClient { 57 | myclient := requester.NewHTTPClient() 58 | 59 | return &AliPanClient{ 60 | httpclient: myclient, 61 | token: token, 62 | apiConfig: apiConfig, 63 | 64 | cacheMutex: &sync.Mutex{}, 65 | useCache: false, 66 | filePathCacheMap: sync.Map{}, 67 | } 68 | } 69 | 70 | func (a ApiToken) GetAuthorizationStr() string { 71 | return "Bearer " + a.AccessToken 72 | } 73 | 74 | func (a *AliPanClient) UpdateToken(token ApiToken) { 75 | a.token = token 76 | } 77 | 78 | func (a *AliPanClient) UpdateApiConfig(apiConfig ApiConfig) { 79 | a.apiConfig = apiConfig 80 | } 81 | 82 | func (a *AliPanClient) GetAccessToken() string { 83 | return a.token.AccessToken 84 | } 85 | 86 | func (a *AliPanClient) Headers() map[string]string { 87 | return map[string]string{ 88 | "content-type": "application/json", 89 | "authorization": a.token.GetAuthorizationStr(), 90 | //"X-Canary": "label=gray", // 标记灰度测试header 91 | } 92 | } 93 | 94 | func (a *AliPanClient) GetApiConfig() ApiConfig { 95 | return a.apiConfig 96 | } 97 | 98 | // EnableCache 启用缓存 99 | func (a *AliPanClient) EnableCache() { 100 | a.cacheMutex.Lock() 101 | a.cacheMutex.Unlock() 102 | a.useCache = true 103 | } 104 | 105 | // ClearCache 清除已经缓存的数据 106 | func (a *AliPanClient) ClearCache() { 107 | a.cacheMutex.Lock() 108 | a.cacheMutex.Unlock() 109 | a.filePathCacheMap = sync.Map{} 110 | } 111 | 112 | // DisableCache 禁用缓存 113 | func (a *AliPanClient) DisableCache() { 114 | a.cacheMutex.Lock() 115 | a.cacheMutex.Unlock() 116 | a.useCache = false 117 | } 118 | 119 | //func (a *AliPanClient) storeFilePathToCache(driveId, pathStr string, fileEntity *FileEntity) { 120 | // a.cacheMutex.Lock() 121 | // a.cacheMutex.Unlock() 122 | // if !a.useCache { 123 | // return 124 | // } 125 | // pathStr = formatPathStyle(pathStr) 126 | // cache, _ := a.filePathCacheMaa.LoadOrStore(driveId, &sync.Map{}) 127 | // cache.(*sync.Map).Store(pathStr, fileEntity) 128 | //} 129 | // 130 | //func (a *AliPanClient) loadFilePathFromCache(driveId, pathStr string) *FileEntity { 131 | // a.cacheMutex.Lock() 132 | // a.cacheMutex.Unlock() 133 | // if !a.useCache { 134 | // return nil 135 | // } 136 | // pathStr = formatPathStyle(pathStr) 137 | // cache, _ := a.filePathCacheMaa.LoadOrStore(driveId, &sync.Map{}) 138 | // s := cache.(*sync.Map) 139 | // if v, ok := s.Load(pathStr); ok { 140 | // logger.Verboseln("file path cache hit: ", pathStr) 141 | // return v.(*FileEntity) 142 | // } 143 | // return nil 144 | //} 145 | 146 | // SetTimeout 设置 http 请求超时时间 147 | func (a *AliPanClient) SetTimeout(t time.Duration) { 148 | if a.httpclient != nil { 149 | a.httpclient.Timeout = t 150 | } 151 | } 152 | 153 | func formatPathStyle(pathStr string) string { 154 | pathStr = strings.ReplaceAll(pathStr, "\\", "/") 155 | if pathStr != "/" { 156 | pathStr = strings.TrimSuffix(pathStr, "/") 157 | } 158 | return pathStr 159 | } 160 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/api_constant.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package openapi 16 | 17 | const ( 18 | OPENAPI_URL string = "https://openapi.alipan.com" 19 | ) 20 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/aynsc_task_api.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/library-go/logger" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // AsyncTaskQueryStatusParam 查询异步任务状态参数 12 | AsyncTaskQueryStatusParam struct { 13 | // AsyncTaskId 异步任务ID 14 | AsyncTaskId string `json:"async_task_id"` 15 | } 16 | // AsyncTaskQueryStatusResult 查询异步任务状态返回值 17 | AsyncTaskQueryStatusResult struct { 18 | // State Succeed 成功,Running 处理中,Failed 已失败 19 | State string `json:"state"` 20 | // AsyncTaskId 异步任务ID 21 | AsyncTaskId string `json:"async_task_id"` 22 | } 23 | ) 24 | 25 | // AsyncTaskQueryStatus 获取异步任务状态 26 | func (a *AliPanClient) AsyncTaskQueryStatus(param *AsyncTaskQueryStatusParam) (*AsyncTaskQueryStatusResult, *AliApiErrResult) { 27 | fullUrl := &strings.Builder{} 28 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/openFile/async_task/get", OPENAPI_URL) 29 | logger.Verboseln("do request url: " + fullUrl.String()) 30 | 31 | // parameters 32 | postData := param 33 | 34 | // request 35 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 36 | if err != nil { 37 | logger.Verboseln("async task status error ", err) 38 | return nil, NewAliApiHttpError(err.Error()) 39 | } 40 | 41 | // handler common error 42 | var body []byte 43 | var apiErrResult *AliApiErrResult 44 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 45 | return nil, apiErrResult 46 | } 47 | 48 | // parse result 49 | r := &AsyncTaskQueryStatusResult{} 50 | if err2 := json.Unmarshal(body, r); err2 != nil { 51 | logger.Verboseln("parse async task status result json error ", err2) 52 | return nil, NewAliApiAppError(err2.Error()) 53 | } 54 | return r, nil 55 | } 56 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/file_album_api.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan" 7 | "github.com/tickstep/library-go/logger" 8 | "strings" 9 | ) 10 | 11 | type ( 12 | // ShareAlbumListParam 获取共享相册列表参数 13 | ShareAlbumListParam struct { 14 | } 15 | // ShareAlbumListResult 获取共享相册列表返回值 16 | ShareAlbumListResult struct { 17 | // 共享相册项列表 18 | Items []*ShareAlbumItem `json:"items"` 19 | } 20 | 21 | // ShareAlbumItem 共享相册项 22 | ShareAlbumItem struct { 23 | // SharedAlbumId 共享相册唯一ID 24 | SharedAlbumId string `json:"sharedAlbumId"` 25 | // Name 共相册名称 26 | Name string `json:"name"` 27 | // Description 共享相册简介 28 | Description string `json:"description"` 29 | // CoverThumbnail 封面图地址 30 | CoverThumbnail string `json:"coverThumbnail"` 31 | // CreatedAt 分享创建时间 32 | CreatedAt int64 `json:"createdAt"` 33 | // UpdatedAt 分享更新时间 34 | UpdatedAt int64 `json:"updatedAt"` 35 | } 36 | 37 | // ShareAlbumListFileParam 获取共享相册文件列表参数 38 | ShareAlbumListFileParam struct { 39 | // AlbumId 共享相册唯一ID 40 | AlbumId string `json:"sharedAlbumId"` 41 | // OrderBy 排序字段,当前仅支持joined_at 42 | OrderBy string `json:"order_by"` 43 | // OrderDirection 排序方向,默认 DESC。ASC 升序,DESC 降序。 44 | OrderDirection string `json:"order_direction"` 45 | // Marker 分页标记 46 | Marker string `json:"marker"` 47 | // Limit 返回文件数量,默认50 48 | Limit int `json:"limit"` 49 | // ImageThumbnailWidth 生成的图片缩略图宽度,默认480px 50 | ImageThumbnailWidth int `json:"image_thumbnail_width"` 51 | } 52 | // ShareAlbumListFileResult 获取共享相册文件列表返回值 53 | ShareAlbumListFileResult struct { 54 | // Items 文件列表 55 | Items []*FileItem `json:"items"` 56 | // NextMarker 不为空代表还有下一页 57 | NextMarker string `json:"nextMarker"` 58 | } 59 | 60 | // ShareAlbumGetFileUrlParam 获取共享相册下文件下载地址参数 61 | ShareAlbumGetFileUrlParam struct { 62 | // AlbumId 共享相册唯一ID 63 | AlbumId string `json:"sharedAlbumId"` 64 | // DriveId 文件所属drive 65 | DriveId string `json:"drive_id"` 66 | // FileId 文件id 67 | FileId string `json:"file_id"` 68 | } 69 | ) 70 | 71 | // ShareAlbumList 获取共享相册列表 72 | func (a *AliPanClient) ShareAlbumList(param *ShareAlbumListParam) (*ShareAlbumListResult, *AliApiErrResult) { 73 | fullUrl := &strings.Builder{} 74 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/sharedAlbum/list", OPENAPI_URL) 75 | logger.Verboseln("do request url: " + fullUrl.String()) 76 | 77 | // parameters 78 | postData := param 79 | 80 | // request 81 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 82 | if err != nil { 83 | logger.Verboseln("list share album error ", err) 84 | return nil, NewAliApiHttpError(err.Error()) 85 | } 86 | 87 | // handler common error 88 | var body []byte 89 | var apiErrResult *AliApiErrResult 90 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 91 | return nil, apiErrResult 92 | } 93 | 94 | // parse result 95 | r := &ShareAlbumListResult{} 96 | if err2 := json.Unmarshal(body, r); err2 != nil { 97 | logger.Verboseln("parse list share album result json error ", err2) 98 | return nil, NewAliApiAppError(err2.Error()) 99 | } 100 | return r, nil 101 | } 102 | 103 | // ShareAlbumListFile 获取共享相册包含图片视频文件列表 104 | func (a *AliPanClient) ShareAlbumListFile(param *ShareAlbumListFileParam) (*ShareAlbumListFileResult, *AliApiErrResult) { 105 | fullUrl := &strings.Builder{} 106 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/sharedAlbum/listFile", OPENAPI_URL) 107 | logger.Verboseln("do request url: " + fullUrl.String()) 108 | 109 | // parameters 110 | postData := param 111 | 112 | // request 113 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 114 | if err != nil { 115 | logger.Verboseln("list file of share album error ", err) 116 | return nil, NewAliApiHttpError(err.Error()) 117 | } 118 | 119 | // handler common error 120 | var body []byte 121 | var apiErrResult *AliApiErrResult 122 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 123 | return nil, apiErrResult 124 | } 125 | 126 | // parse result 127 | r := &ShareAlbumListFileResult{} 128 | if err2 := json.Unmarshal(body, r); err2 != nil { 129 | logger.Verboseln("parse list file of share album result json error ", err2) 130 | return nil, NewAliApiAppError(err2.Error()) 131 | } 132 | // 补全 album ID 133 | if r.Items != nil { 134 | for _, item := range r.Items { 135 | item.AlbumId = param.AlbumId 136 | } 137 | } 138 | return r, nil 139 | } 140 | 141 | // ShareAlbumGetFileDownloadUrl 获取共享相册下文件下载地址 142 | func (a *AliPanClient) ShareAlbumGetFileDownloadUrl(param *ShareAlbumGetFileUrlParam) (*aliyunpan.ShareAlbumGetFileUrlResult, *AliApiErrResult) { 143 | fullUrl := &strings.Builder{} 144 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/sharedAlbum/getDownloadUrl", OPENAPI_URL) 145 | logger.Verboseln("do request url: " + fullUrl.String()) 146 | 147 | // parameters 148 | postData := param 149 | 150 | // request 151 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 152 | if err != nil { 153 | logger.Verboseln("get share album file download url error ", err) 154 | return nil, NewAliApiHttpError(err.Error()) 155 | } 156 | 157 | // handler common error 158 | var body []byte 159 | var apiErrResult *AliApiErrResult 160 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 161 | return nil, apiErrResult 162 | } 163 | 164 | // parse result 165 | r := &aliyunpan.ShareAlbumGetFileUrlResult{} 166 | if err2 := json.Unmarshal(body, r); err2 != nil { 167 | logger.Verboseln("parse download url of share album file result json error ", err2) 168 | return nil, NewAliApiAppError(err2.Error()) 169 | } 170 | return r, nil 171 | } 172 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/file_share.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/library-go/logger" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // FileShareCreateParam 创建文件分享参数 12 | FileShareCreateParam struct { 13 | // DriveId 网盘id 14 | DriveId string `json:"driveId"` 15 | // 文件ID数据,元数数量 [1, 100] 16 | FileIdList []string `json:"fileIdList"` 17 | // 分享过期时间,格式:2024-09-19T09:32:50.000Z 18 | Expiration string `json:"expiration"` 19 | // 分享提取码 20 | SharePwd string `json:"sharePwd"` 21 | } 22 | // FileShareCreateResult 创建文件分享返回值 23 | FileShareCreateResult struct { 24 | // 分享ID 25 | ShareId string `json:"share_id"` 26 | // 分享过期时间 27 | Expiration string `json:"expiration"` 28 | // 分享是否已过期 29 | Expired bool `json:"expired"` 30 | // 分享提取码 31 | SharePwd string `json:"share_pwd"` 32 | // 分享链接地址 33 | ShareUrl string `json:"share_url"` 34 | // 分享创建者ID 35 | Creator string `json:"creator"` 36 | // 分享当前状态 37 | Status string `json:"status"` 38 | // 分享创建时间,格式:2024-09-14T02:11:34.264Z 39 | CreatedAt string `json:"created_at"` 40 | // 分享更新时间,格式:2024-09-14T02:11:34.264Z 41 | UpdatedAt string `json:"update_at"` 42 | } 43 | 44 | // FileFastShareFileItem 快传文件项 45 | FileFastShareFileItem struct { 46 | // DriveId 网盘id 47 | DriveId string `json:"drive_id"` 48 | // FileId 文件ID 49 | FileId string `json:"file_id"` 50 | } 51 | // FileFastShareCreateParam 创建文件快传参数 52 | FileFastShareCreateParam struct { 53 | // DriveFileList 分享文件列表 [1,100] 54 | DriveFileList []FileFastShareFileItem `json:"drive_file_list"` 55 | } 56 | // FileFastShareCreateResult 创建文件快传返回值 57 | FileFastShareCreateResult struct { 58 | // 分享ID 59 | ShareId string `json:"share_id"` 60 | // 分享过期时间 61 | Expiration string `json:"expiration"` 62 | // 分享链接地址 63 | ShareUrl string `json:"share_url"` 64 | // 分享创建者ID 65 | CreatorId string `json:"creator_id"` 66 | // DriveFileList 分享文件列表 [1,100] 67 | DriveFileList []FileFastShareFileItem `json:"drive_file_list"` 68 | } 69 | ) 70 | 71 | // FileShareCreate 创建文件分享 72 | func (a *AliPanClient) FileShareCreate(param *FileShareCreateParam) (*FileShareCreateResult, *AliApiErrResult) { 73 | fullUrl := &strings.Builder{} 74 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/openFile/createShare", OPENAPI_URL) 75 | logger.Verboseln("do request url: " + fullUrl.String()) 76 | 77 | // parameters 78 | postData := param 79 | 80 | // request 81 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 82 | if err != nil { 83 | logger.Verboseln("create file share error ", err) 84 | return nil, NewAliApiHttpError(err.Error()) 85 | } 86 | 87 | // handler common error 88 | var body []byte 89 | var apiErrResult *AliApiErrResult 90 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 91 | return nil, apiErrResult 92 | } 93 | 94 | // parse result 95 | r := &FileShareCreateResult{} 96 | if err2 := json.Unmarshal(body, r); err2 != nil { 97 | logger.Verboseln("parse file share result json error ", err2) 98 | return nil, NewAliApiAppError(err2.Error()) 99 | } 100 | return r, nil 101 | } 102 | 103 | // FileFastShareCreate 创建文件快传 104 | func (a *AliPanClient) FileFastShareCreate(param *FileFastShareCreateParam) (*FileFastShareCreateResult, *AliApiErrResult) { 105 | fullUrl := &strings.Builder{} 106 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/openFile/createFastTransfer", OPENAPI_URL) 107 | logger.Verboseln("do request url: " + fullUrl.String()) 108 | 109 | // parameters 110 | postData := param 111 | 112 | // request 113 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 114 | if err != nil { 115 | logger.Verboseln("create file fast share error ", err) 116 | return nil, NewAliApiHttpError(err.Error()) 117 | } 118 | 119 | // handler common error 120 | var body []byte 121 | var apiErrResult *AliApiErrResult 122 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 123 | return nil, apiErrResult 124 | } 125 | 126 | // parse result 127 | r := &FileFastShareCreateResult{} 128 | if err2 := json.Unmarshal(body, r); err2 != nil { 129 | logger.Verboseln("parse file fast share result json error ", err2) 130 | return nil, NewAliApiAppError(err2.Error()) 131 | } 132 | return r, nil 133 | } 134 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/user_api.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/library-go/logger" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | DriveInfoResult struct { 12 | // UserId 用户ID,具有唯一性 13 | UserId string `json:"user_id"` 14 | // Name 昵称 15 | Name string `json:"name"` 16 | // Avatar 头像地址 17 | Avatar string `json:"avatar"` 18 | // DefaultDriveId 默认drive 19 | DefaultDriveId string `json:"default_drive_id"` 20 | // ResourceDriveId 资源库。用户选择了授权才会返回 21 | ResourceDriveId string `json:"resource_drive_id"` 22 | // BackupDriveId 备份盘。用户选择了授权才会返回 23 | BackupDriveId string `json:"backup_drive_id"` 24 | } 25 | 26 | PersonalSpaceInfoResult struct { 27 | // UsedSize 使用容量,单位bytes 28 | UsedSize int64 `json:"used_size"` 29 | // TotalSize 总容量,单位bytes 30 | TotalSize int64 `json:"total_size"` 31 | } 32 | 33 | UserVipInfoResult struct { 34 | // Identity 枚举:member, vip, svip 35 | Identity string `json:"identity"` 36 | // level 20TB、8TB 37 | Level string `json:"level"` 38 | // Expire 过期时间,时间戳,单位秒 39 | Expire int64 `json:"expire"` 40 | // ThirdPartyVip “三方权益包”是否生效 41 | ThirdPartyVip bool `json:"thirdPartyVip"` 42 | // ThirdPartyVipExpire “三方权益包”过期时间 43 | ThirdPartyVipExpire int64 `json:"thirdPartyVipExpire"` 44 | } 45 | 46 | UserScopeList []*UserScopeItem 47 | UserScopeItem struct { 48 | // Scope 权限标识 49 | Scope string `json:"scope"` 50 | } 51 | ) 52 | 53 | // UserGetDriveInfo 获取用户drive信息 54 | func (a *AliPanClient) UserGetDriveInfo() (*DriveInfoResult, *AliApiErrResult) { 55 | fullUrl := &strings.Builder{} 56 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/user/getDriveInfo", OPENAPI_URL) 57 | logger.Verboseln("do request url: " + fullUrl.String()) 58 | 59 | // request 60 | resp, err := a.httpclient.Req("POST", fullUrl.String(), nil, a.Headers()) 61 | if err != nil { 62 | logger.Verboseln("get drive info error ", err) 63 | return nil, NewAliApiHttpError(err.Error()) 64 | } 65 | 66 | // handler common error 67 | var body []byte 68 | var apiErrResult *AliApiErrResult 69 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 70 | return nil, apiErrResult 71 | } 72 | 73 | // parse result 74 | r := &DriveInfoResult{} 75 | if err2 := json.Unmarshal(body, r); err2 != nil { 76 | logger.Verboseln("parse drive info result json error ", err2) 77 | return nil, NewAliApiAppError(err2.Error()) 78 | } 79 | return r, nil 80 | } 81 | 82 | // UserGetSpaceInfo 获取用户空间信息 83 | func (a *AliPanClient) UserGetSpaceInfo() (*PersonalSpaceInfoResult, *AliApiErrResult) { 84 | fullUrl := &strings.Builder{} 85 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/user/getSpaceInfo", OPENAPI_URL) 86 | logger.Verboseln("do request url: " + fullUrl.String()) 87 | 88 | // request 89 | resp, err := a.httpclient.Req("POST", fullUrl.String(), nil, a.Headers()) 90 | if err != nil { 91 | logger.Verboseln("get space info error ", err) 92 | return nil, NewAliApiHttpError(err.Error()) 93 | } 94 | 95 | // handler common error 96 | var body []byte 97 | var apiErrResult *AliApiErrResult 98 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 99 | return nil, apiErrResult 100 | } 101 | 102 | // parse result 103 | type personalSpaceInfoData struct { 104 | Info *PersonalSpaceInfoResult `json:"personal_space_info"` 105 | } 106 | r := &personalSpaceInfoData{} 107 | if err2 := json.Unmarshal(body, r); err2 != nil { 108 | logger.Verboseln("parse space info result json error ", err2) 109 | return nil, NewAliApiAppError(err2.Error()) 110 | } 111 | return r.Info, nil 112 | } 113 | 114 | // UserGetVipInfo 获取用户vip信息 115 | func (a *AliPanClient) UserGetVipInfo() (*UserVipInfoResult, *AliApiErrResult) { 116 | fullUrl := &strings.Builder{} 117 | fmt.Fprintf(fullUrl, "%s/business/v1.0/user/getVipInfo", OPENAPI_URL) 118 | logger.Verboseln("do request url: " + fullUrl.String()) 119 | 120 | // request 121 | resp, err := a.httpclient.Req("POST", fullUrl.String(), nil, a.Headers()) 122 | if err != nil { 123 | logger.Verboseln("get vip info error ", err) 124 | return nil, NewAliApiHttpError(err.Error()) 125 | } 126 | 127 | // handler common error 128 | var body []byte 129 | var apiErrResult *AliApiErrResult 130 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 131 | return nil, apiErrResult 132 | } 133 | 134 | // parse result 135 | r := &UserVipInfoResult{} 136 | if err2 := json.Unmarshal(body, r); err2 != nil { 137 | logger.Verboseln("parse vip info result json error ", err2) 138 | return nil, NewAliApiAppError(err2.Error()) 139 | } 140 | return r, nil 141 | } 142 | 143 | // UserScopes 获取用户权限 144 | func (a *AliPanClient) UserScopes() (*UserScopeList, *AliApiErrResult) { 145 | fullUrl := &strings.Builder{} 146 | fmt.Fprintf(fullUrl, "%s/oauth/users/scopes", OPENAPI_URL) 147 | logger.Verboseln("do request url: " + fullUrl.String()) 148 | 149 | // request 150 | resp, err := a.httpclient.Req("GET", fullUrl.String(), nil, a.Headers()) 151 | if err != nil { 152 | logger.Verboseln("get user scope info error ", err) 153 | return nil, NewAliApiHttpError(err.Error()) 154 | } 155 | 156 | // handler common error 157 | var body []byte 158 | var apiErrResult *AliApiErrResult 159 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 160 | return nil, apiErrResult 161 | } 162 | 163 | // parse result 164 | type userScopeInfoData struct { 165 | Id string `json:"id"` 166 | UserScopes *UserScopeList `json:"scopes"` 167 | } 168 | r := &userScopeInfoData{} 169 | if err2 := json.Unmarshal(body, r); err2 != nil { 170 | logger.Verboseln("parse user scope info result json error ", err2) 171 | return nil, NewAliApiAppError(err2.Error()) 172 | } 173 | return r.UserScopes, nil 174 | } 175 | -------------------------------------------------------------------------------- /aliyunpan_open/openapi/video_api.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/library-go/logger" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // VideoGetPreviewPlayInfoParam 获取文件播放详情参数 12 | VideoGetPreviewPlayInfoParam struct { 13 | // DriveId 网盘id 14 | DriveId string `json:"drive_id"` 15 | // ParentFileId 根目录为root 16 | FileId string `json:"file_id"` 17 | // Category live_transcoding 边转边播 18 | Category string `json:"category"` 19 | } 20 | 21 | LiveTranscodingTask struct { 22 | TemplateId string `json:"template_id"` 23 | TemplateName string `json:"template_name"` 24 | TemplateWidth int `json:"template_width"` 25 | TemplateHeight int `json:"template_height"` 26 | // Status 状态。 枚举值如下: finished, 索引完成,可以获取到url, running, 正在索引,请稍等片刻重试, failed, 转码失败,请检查是否媒体文件,如果有疑问请联系客服 27 | Status string `json:"status"` 28 | Stage string `json:"stage"` 29 | Url string `json:"url"` 30 | } 31 | // VideoGetPreviewPlayInfoResult 获取文件播放详情返回值 32 | VideoGetPreviewPlayInfoResult struct { 33 | DriveId string `json:"drive_id"` 34 | FileId string `json:"file_id"` 35 | VideoPreviewPlayInfo struct { 36 | Category string `json:"category"` 37 | Meta struct { 38 | Duration float64 `json:"duration"` 39 | Width int `json:"width"` 40 | Height int `json:"height"` 41 | } `json:"meta"` 42 | LiveTranscodingTaskList []*LiveTranscodingTask `json:"live_transcoding_task_list"` 43 | } `json:"video_preview_play_info"` 44 | } 45 | ) 46 | 47 | // VideoGetPreviewPlayInfo 获取文件播放详情 48 | func (a *AliPanClient) VideoGetPreviewPlayInfo(param *VideoGetPreviewPlayInfoParam) (*VideoGetPreviewPlayInfoResult, *AliApiErrResult) { 49 | fullUrl := &strings.Builder{} 50 | fmt.Fprintf(fullUrl, "%s/adrive/v1.0/openFile/getVideoPreviewPlayInfo", OPENAPI_URL) 51 | logger.Verboseln("do request url: " + fullUrl.String()) 52 | 53 | // parameters 54 | postData := param 55 | 56 | // request 57 | resp, err := a.httpclient.Req("POST", fullUrl.String(), postData, a.Headers()) 58 | if err != nil { 59 | logger.Verboseln("video get preview play info error ", err) 60 | return nil, NewAliApiHttpError(err.Error()) 61 | } 62 | 63 | // handler common error 64 | var body []byte 65 | var apiErrResult *AliApiErrResult 66 | if body, apiErrResult = ParseCommonOpenApiError(resp); apiErrResult != nil { 67 | return nil, apiErrResult 68 | } 69 | 70 | // parse result 71 | r := &VideoGetPreviewPlayInfoResult{} 72 | if err2 := json.Unmarshal(body, r); err2 != nil { 73 | logger.Verboseln("parse video get preview play info result json error ", err2) 74 | return nil, NewAliApiAppError(err2.Error()) 75 | } 76 | return r, nil 77 | } 78 | -------------------------------------------------------------------------------- /aliyunpan_open/user_info.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 7 | ) 8 | 9 | // GetUserInfo 获取用户信息 10 | func (p *OpenPanClient) GetUserInfo() (*aliyunpan.UserInfo, *apierror.ApiError) { 11 | retryTime := 0 12 | returnResult := &aliyunpan.UserInfo{ 13 | DomainId: "", 14 | FileDriveId: "", 15 | SafeBoxDriveId: "", 16 | AlbumDriveId: "", 17 | ResourceDriveId: "", 18 | UserId: "", 19 | UserName: "", 20 | CreatedAt: "", 21 | Email: "", 22 | Phone: "", 23 | Role: "", 24 | Status: "", 25 | Nickname: "", 26 | TotalSize: 0, 27 | UsedSize: 0, 28 | } 29 | 30 | RetryBegin: 31 | // user basic info 32 | if result, err := p.apiClient.UserGetDriveInfo(); err == nil { 33 | returnResult = &aliyunpan.UserInfo{ 34 | DomainId: "", 35 | FileDriveId: result.BackupDriveId, 36 | SafeBoxDriveId: "", 37 | AlbumDriveId: "", 38 | ResourceDriveId: result.ResourceDriveId, 39 | UserId: result.UserId, 40 | UserName: "", 41 | CreatedAt: "", 42 | Email: "", 43 | Phone: "", 44 | Role: "", 45 | Status: "", 46 | Nickname: result.Name, 47 | TotalSize: 0, 48 | UsedSize: 0, 49 | ThirdPartyVip: false, 50 | ThirdPartyVipExpire: "", 51 | } 52 | } else { 53 | // handle common error 54 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 55 | goto RetryBegin 56 | } else { 57 | return nil, apiErrorHandleResp.ApiErr 58 | } 59 | } 60 | 61 | // user vip info 62 | if result, err := p.apiClient.UserGetVipInfo(); err == nil { 63 | returnResult.ThirdPartyVip = result.ThirdPartyVip 64 | if result.ThirdPartyVipExpire > 0 { 65 | returnResult.ThirdPartyVipExpire = apiutil.UnixTime2LocalFormat(result.ThirdPartyVipExpire*1000) 66 | } 67 | } else { 68 | // handle common error 69 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 70 | goto RetryBegin 71 | } else { 72 | return nil, apiErrorHandleResp.ApiErr 73 | } 74 | } 75 | 76 | // drive spaces 77 | if result, err := p.apiClient.UserGetSpaceInfo(); err == nil { 78 | returnResult.TotalSize = uint64(result.TotalSize) 79 | returnResult.UsedSize = uint64(result.UsedSize) 80 | } else { 81 | // handle common error 82 | if apiErrorHandleResp := p.HandleAliApiError(err, &retryTime); apiErrorHandleResp.NeedRetry { 83 | goto RetryBegin 84 | } else { 85 | return nil, apiErrorHandleResp.ApiErr 86 | } 87 | } 88 | 89 | return returnResult, nil 90 | } 91 | -------------------------------------------------------------------------------- /aliyunpan_open/util.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_open 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/library-go/escaper" 7 | "github.com/tickstep/library-go/logger" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | // ShellPatternCharacters 通配符字符串 14 | ShellPatternCharacters = "*?[]" 15 | ) 16 | 17 | func (p *OpenPanClient) recurseMatchPathByShellPattern(driveId string, index int, pathSlice *[]string, parentFileInfo *aliyunpan.FileEntity, resultList *aliyunpan.FileList) { 18 | if parentFileInfo == nil { 19 | // default root "/" entity 20 | parentFileInfo = aliyunpan.NewFileEntityForRootDir() 21 | if index == 0 && len(*pathSlice) == 1 { 22 | // root path "/" 23 | *resultList = append(*resultList, parentFileInfo) 24 | return 25 | } 26 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, parentFileInfo, resultList) 27 | return 28 | } 29 | 30 | if index >= len(*pathSlice) { 31 | // 已经是最后的路径分片了,是命中的结果 32 | *resultList = append(*resultList, parentFileInfo) 33 | return 34 | } 35 | 36 | if !strings.ContainsAny((*pathSlice)[index], ShellPatternCharacters) { 37 | // 不包含通配符,先查缓存 38 | curPathStr := path.Clean(parentFileInfo.Path + "/" + (*pathSlice)[index]) 39 | 40 | // try cache 41 | if v := p.loadFilePathFromCache(driveId, curPathStr); v != nil { 42 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, v, resultList) 43 | return 44 | } 45 | } 46 | 47 | // 遍历目录下所有文件 48 | if parentFileInfo.IsFile() { 49 | return 50 | } 51 | fileListParam := &aliyunpan.FileListParam{ 52 | DriveId: driveId, 53 | ParentFileId: parentFileInfo.FileId, 54 | } 55 | fileResult, err := p.FileListGetAll(fileListParam, 0) 56 | if err != nil { 57 | logger.Verbosef("获取目录文件列表错误") 58 | return 59 | } 60 | if fileResult == nil || len(fileResult) == 0 { 61 | // 文件目录下文件为空 62 | return 63 | } 64 | 65 | curParentPathStr := parentFileInfo.Path 66 | if curParentPathStr == "/" { 67 | curParentPathStr = "" 68 | } 69 | 70 | // 先检测是否满足文件名全量匹配 71 | for _, fileEntity := range fileResult { 72 | // cache item 73 | fileEntity.Path = curParentPathStr + "/" + fileEntity.FileName 74 | p.storeFilePathToCache(driveId, fileEntity.Path, fileEntity) 75 | 76 | // 阿里云盘文件名支持*?[]等特殊符号,先排除文件名完全一致匹配的情况,这种情况下不能开启通配符匹配 77 | if fileEntity.FileName == (*pathSlice)[index] { 78 | // 匹配一个就直接返回 79 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, fileEntity, resultList) 80 | return 81 | } 82 | } 83 | 84 | // 使用通配符匹配 85 | for _, fileEntity := range fileResult { 86 | // cache item 87 | fileEntity.Path = curParentPathStr + "/" + fileEntity.FileName 88 | p.storeFilePathToCache(driveId, fileEntity.Path, fileEntity) 89 | 90 | // 使用通配符 91 | if matched, _ := path.Match((*pathSlice)[index], fileEntity.FileName); matched { 92 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, fileEntity, resultList) 93 | } 94 | } 95 | } 96 | 97 | // MatchPathByShellPattern 通配符匹配文件路径, pattern为绝对路径,符合的路径文件存放在resultList中 98 | func (p *OpenPanClient) MatchPathByShellPattern(driveId string, pattern string) (resultList *aliyunpan.FileList, error *apierror.ApiError) { 99 | errInfo := apierror.NewApiError(apierror.ApiCodeFailed, "") 100 | resultList = &aliyunpan.FileList{} 101 | 102 | patternSlice := strings.Split(escaper.Escape(path.Clean(pattern), []rune{'['}), aliyunpan.PathSeparator) // 转义中括号 103 | if patternSlice[0] != "" { 104 | errInfo.Err = "路径不是绝对路径" 105 | return nil, errInfo 106 | } 107 | defer func() { // 捕获异常 108 | if err := recover(); err != nil { 109 | resultList = nil 110 | errInfo.Err = "查询路径异常" 111 | } 112 | }() 113 | 114 | parentFile := aliyunpan.NewFileEntityForRootDir() 115 | if path.Clean(strings.TrimSpace(pattern)) == "/" { 116 | *resultList = append(*resultList, parentFile) 117 | return resultList, nil 118 | } 119 | p.recurseMatchPathByShellPattern(driveId, 1, &patternSlice, parentFile, resultList) 120 | return resultList, nil 121 | } 122 | -------------------------------------------------------------------------------- /aliyunpan_web/api_constant.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | const ( 18 | WEB_URL string = "https://www.aliyundrive.com" 19 | AUTH_URL string = "https://auth.aliyundrive.com" 20 | API_URL string = "https://api.aliyundrive.com" 21 | USER_URL string = "https://user.aliyundrive.com" 22 | ) 23 | -------------------------------------------------------------------------------- /aliyunpan_web/app_login.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // 电脑手机客户端API,例如MAC客户端 16 | package aliyunpan_web 17 | 18 | import ( 19 | "github.com/tickstep/library-go/requester" 20 | ) 21 | 22 | type ( 23 | AppLoginToken struct { 24 | AccessToken string `json:"accessToken"` 25 | RefreshToken string `json:"refreshToken"` 26 | } 27 | ) 28 | 29 | var ( 30 | appClient = requester.NewHTTPClient() 31 | ) 32 | -------------------------------------------------------------------------------- /aliyunpan_web/async_task.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 8 | "github.com/tickstep/library-go/logger" 9 | "strings" 10 | ) 11 | 12 | type ( 13 | AsyncTaskQueryStatusParam struct { 14 | AsyncTaskId string `json:"async_task_id"` 15 | } 16 | 17 | AsyncTaskQueryStatusResult struct { 18 | AsyncTaskId string `json:"async_task_id"` 19 | State string `json:"state"` 20 | Status string `json:"status"` 21 | TotalProcess int `json:"total_process"` 22 | ConsumedProcess int `json:"consumed_process"` 23 | SkippedProcess int `json:"skipped_process"` 24 | FailedProcess int `json:"failed_process"` 25 | PunishedFileCount int `json:"punished_file_count"` 26 | } 27 | ) 28 | 29 | // AsyncTaskQueryStatus 查询异步任务进度和状态 30 | func (p *WebPanClient) AsyncTaskQueryStatus(param *AsyncTaskQueryStatusParam) (*AsyncTaskQueryStatusResult, *apierror.ApiError) { 31 | header := map[string]string{ 32 | "authorization": p.webToken.GetAuthorizationStr(), 33 | "referer": "https://www.aliyundrive.com/", 34 | "origin": "https://www.aliyundrive.com", 35 | } 36 | 37 | fullUrl := &strings.Builder{} 38 | fmt.Fprintf(fullUrl, "%s/v2/async_task/get", API_URL) 39 | logger.Verboseln("do request url: " + fullUrl.String()) 40 | 41 | postData := map[string]interface{}{ 42 | "async_task_id": param.AsyncTaskId, 43 | } 44 | // request 45 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 46 | if err != nil { 47 | logger.Verboseln("async task query status error ", err) 48 | return nil, apierror.NewFailedApiError(err.Error()) 49 | } 50 | 51 | // handler common error 52 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 53 | return nil, err1 54 | } 55 | 56 | // parse result 57 | r := &AsyncTaskQueryStatusResult{} 58 | if err2 := json.Unmarshal(body, r); err2 != nil { 59 | logger.Verboseln("parse async task query status result json error ", err2) 60 | return nil, apierror.NewFailedApiError(err2.Error()) 61 | } 62 | return r, nil 63 | } 64 | -------------------------------------------------------------------------------- /aliyunpan_web/batch_task.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 9 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 10 | "github.com/tickstep/library-go/logger" 11 | ) 12 | 13 | type ( 14 | // 请求参数 15 | BatchRequest struct { 16 | Id string `json:"id"` 17 | Method string `json:"method"` 18 | Url string `json:"url"` 19 | 20 | Headers map[string]string `json:"headers"` 21 | Body map[string]interface{} `json:"body"` 22 | } 23 | BatchRequestList []*BatchRequest 24 | BatchRequestParam struct { 25 | Requests BatchRequestList `json:"requests"` 26 | Resource string `json:"resource"` 27 | } 28 | 29 | // 响应结果 30 | BatchResponse struct { 31 | Id string `json:"id"` 32 | Status int `json:"status"` 33 | Body map[string]interface{} `json:"body"` 34 | } 35 | BatchResponseList []*BatchResponse 36 | BatchResponseResult struct { 37 | Responses BatchResponseList `json:"responses"` 38 | } 39 | ) 40 | 41 | // BatchTask 批量请求任务。多选操作基本都是批量任务 42 | func (p *WebPanClient) BatchTask(url string, param *BatchRequestParam, headers ...[2]string) (*BatchResponseResult, *apierror.ApiError) { 43 | if param == nil { 44 | return nil, apierror.NewFailedApiError("参数不能为空") 45 | } 46 | 47 | // header 48 | header := map[string]string{ 49 | "authorization": p.webToken.GetAuthorizationStr(), 50 | } 51 | for _, v := range headers { 52 | header[v[0]] = v[1] 53 | } 54 | 55 | // url 56 | fullUrl := &strings.Builder{} 57 | fmt.Fprintf(fullUrl, "%s", url) 58 | logger.Verboseln("do request url: " + fullUrl.String()) 59 | 60 | // data 61 | postData := param 62 | 63 | // request 64 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 65 | if err != nil { 66 | logger.Verboseln("batch request error ", err) 67 | return nil, apierror.NewFailedApiError(err.Error()) 68 | } 69 | 70 | // handler common error 71 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 72 | return nil, err1 73 | } 74 | 75 | // parse result 76 | r := &BatchResponseResult{} 77 | if err2 := json.Unmarshal(body, r); err2 != nil { 78 | logger.Verboseln("batch result json error ", err2) 79 | return nil, apierror.NewFailedApiError(err2.Error()) 80 | } 81 | return r, nil 82 | } 83 | -------------------------------------------------------------------------------- /aliyunpan_web/file_copy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 22 | "github.com/tickstep/library-go/logger" 23 | "strings" 24 | ) 25 | 26 | type ( 27 | FileCrossCopyParam struct { 28 | // FromDriveId 源网盘ID 29 | FromDriveId string `json:"from_drive_id"` 30 | // FromFileIds 源网盘文件列表ID 31 | FromFileIds []string `json:"from_file_ids"` 32 | // ToDriveId 目标网盘ID。必须和源网盘ID不一样,否则会报错 33 | ToDriveId string `json:"to_drive_id"` 34 | // ToParentFileId 目标网盘目录ID 35 | ToParentFileId string `json:"to_parent_fileId"` 36 | } 37 | 38 | FileCrossCopyResult struct { 39 | DriveId string `json:"drive_id"` 40 | FileId string `json:"file_id"` 41 | SourceDriveId string `json:"source_drive_id"` 42 | SourceFileId string `json:"source_file_id"` 43 | // Status 结果状态,201代表成功 44 | Status int `json:"status"` 45 | } 46 | ) 47 | 48 | // FileCrossDriveCopy 跨网盘复制文件,支持资源库和备份盘之间复制文件 49 | func (p *WebPanClient) FileCrossDriveCopy(param *FileCrossCopyParam) ([]*FileCrossCopyResult, *apierror.ApiError) { 50 | // header 51 | header := map[string]string{ 52 | "authorization": p.webToken.GetAuthorizationStr(), 53 | } 54 | 55 | // url 56 | fullUrl := &strings.Builder{} 57 | fmt.Fprintf(fullUrl, "%s/adrive/v2/file/crossDriveCopy", API_URL) 58 | logger.Verboseln("do request url: " + fullUrl.String()) 59 | 60 | // data 61 | postData := param 62 | if param.FromDriveId == param.ToDriveId { 63 | return nil, apierror.NewFailedApiError("目标网盘ID和源网盘ID必须不一样") 64 | } 65 | 66 | // request 67 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 68 | if err != nil { 69 | logger.Verboseln("do cross drive copy error ", err) 70 | return nil, apierror.NewFailedApiError(err.Error()) 71 | } 72 | logger.Verboseln("response: ", string(body)) 73 | 74 | // handler common error 75 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 76 | return nil, err1 77 | } 78 | 79 | // parse result 80 | result := struct { 81 | Items []*FileCrossCopyResult `json:"items"` 82 | }{} 83 | if err2 := json.Unmarshal(body, &result); err2 != nil { 84 | logger.Verboseln("parse cross drive copy result json error ", err2) 85 | return nil, apierror.NewFailedApiError(err2.Error()) 86 | } 87 | 88 | // parse result 89 | r := []*FileCrossCopyResult{} 90 | for _, item := range result.Items { 91 | r = append(r, item) 92 | } 93 | return r, nil 94 | } 95 | 96 | // FileCrossDriveMove 跨网盘移动文件,只支持从资源库移动到备份盘 97 | func (p *WebPanClient) FileCrossDriveMove(param *FileCrossCopyParam) ([]*FileCrossCopyResult, *apierror.ApiError) { 98 | // header 99 | header := map[string]string{ 100 | "authorization": p.webToken.GetAuthorizationStr(), 101 | } 102 | 103 | // url 104 | fullUrl := &strings.Builder{} 105 | fmt.Fprintf(fullUrl, "%s/adrive/v2/file/crossDriveMove", API_URL) 106 | logger.Verboseln("do request url: " + fullUrl.String()) 107 | 108 | // data 109 | postData := param 110 | if param.FromDriveId == param.ToDriveId { 111 | return nil, apierror.NewFailedApiError("目标网盘ID和源网盘ID必须不一样") 112 | } 113 | 114 | // request 115 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 116 | if err != nil { 117 | logger.Verboseln("do cross drive copy error ", err) 118 | return nil, apierror.NewFailedApiError(err.Error()) 119 | } 120 | logger.Verboseln("response: ", string(body)) 121 | 122 | // handler common error 123 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 124 | return nil, err1 125 | } 126 | 127 | // parse result 128 | result := struct { 129 | Items []*FileCrossCopyResult `json:"items"` 130 | }{} 131 | if err2 := json.Unmarshal(body, &result); err2 != nil { 132 | logger.Verboseln("parse cross drive copy result json error ", err2) 133 | return nil, apierror.NewFailedApiError(err2.Error()) 134 | } 135 | 136 | // parse result 137 | r := []*FileCrossCopyResult{} 138 | for _, item := range result.Items { 139 | r = append(r, item) 140 | } 141 | return r, nil 142 | } 143 | -------------------------------------------------------------------------------- /aliyunpan_web/file_delete.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "fmt" 19 | "github.com/tickstep/aliyunpan-api/aliyunpan" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 22 | "github.com/tickstep/library-go/logger" 23 | "strings" 24 | ) 25 | 26 | type () 27 | 28 | // FileDelete 删除文件到回收站 29 | func (p *WebPanClient) FileDelete(param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 30 | // url 31 | fullUrl := &strings.Builder{} 32 | fmt.Fprintf(fullUrl, "%s/adrive/v4/batch", API_URL) 33 | logger.Verboseln("do request url: " + fullUrl.String()) 34 | 35 | // process 36 | return p.doFileBatchRequest(fullUrl.String(), "/recyclebin/trash", param) 37 | } 38 | 39 | // RecycleBinFileDelete 回收站彻底删除文件 40 | func (p *WebPanClient) RecycleBinFileDelete(param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 41 | // url 42 | fullUrl := &strings.Builder{} 43 | fmt.Fprintf(fullUrl, "%s/adrive/v4/batch", API_URL) 44 | logger.Verboseln("do request url: " + fullUrl.String()) 45 | 46 | // process 47 | return p.doFileBatchRequest(fullUrl.String(), "/file/delete", param) 48 | } 49 | 50 | // RecycleBinFileRestore 回收站还原文件。还原的文件会存放会原来的地方 51 | func (p *WebPanClient) RecycleBinFileRestore(param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 52 | // url 53 | fullUrl := &strings.Builder{} 54 | fmt.Fprintf(fullUrl, "%s/adrive/v4/batch", API_URL) 55 | logger.Verboseln("do request url: " + fullUrl.String()) 56 | 57 | // process 58 | return p.doFileBatchRequest(fullUrl.String(), "/recyclebin/restore", param) 59 | } 60 | 61 | func (p *WebPanClient) doFileBatchRequest(url, actionUrl string, param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 62 | requests, e := p.getFileDeleteBatchRequestList(actionUrl, param) 63 | if e != nil { 64 | return nil, e 65 | } 66 | batchParam := BatchRequestParam{ 67 | Requests: requests, 68 | Resource: "file", 69 | } 70 | 71 | // request 72 | result, err := p.BatchTask(url, &batchParam) 73 | if err != nil { 74 | logger.Verboseln("file batch error ", err) 75 | return nil, apierror.NewFailedApiError(err.Error()) 76 | } 77 | 78 | // parse result 79 | r := []*aliyunpan.FileBatchActionResult{} 80 | for _, item := range result.Responses { 81 | r = append(r, &aliyunpan.FileBatchActionResult{ 82 | FileId: item.Id, 83 | Success: item.Status == 204 || item.Status == 202 || item.Status == 200, 84 | }) 85 | } 86 | return r, nil 87 | } 88 | 89 | func (p *WebPanClient) getFileDeleteBatchRequestList(actionUrl string, param []*aliyunpan.FileBatchActionParam) (BatchRequestList, *apierror.ApiError) { 90 | if param == nil { 91 | return nil, apierror.NewFailedApiError("参数不能为空") 92 | } 93 | 94 | r := BatchRequestList{} 95 | for _, item := range param { 96 | r = append(r, &BatchRequest{ 97 | Id: item.FileId, 98 | Method: "POST", 99 | Url: actionUrl, 100 | Headers: map[string]string{ 101 | "Content-Type": "application/json", 102 | }, 103 | Body: apiutil.GetMapSet(item), 104 | }) 105 | } 106 | return r, nil 107 | } 108 | -------------------------------------------------------------------------------- /aliyunpan_web/file_download.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 22 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 23 | "github.com/tickstep/library-go/cachepool" 24 | "github.com/tickstep/library-go/logger" 25 | "github.com/tickstep/library-go/requester" 26 | "io" 27 | "net/http" 28 | "strconv" 29 | "strings" 30 | ) 31 | 32 | type () 33 | 34 | const () 35 | 36 | // GetFileDownloadUrl 获取文件下载URL路径 37 | func (p *WebPanClient) GetFileDownloadUrl(param *aliyunpan.GetFileDownloadUrlParam) (*aliyunpan.GetFileDownloadUrlResult, *apierror.ApiError) { 38 | // header 39 | header := map[string]string{ 40 | "authorization": p.webToken.GetAuthorizationStr(), 41 | } 42 | 43 | // url 44 | fullUrl := &strings.Builder{} 45 | fmt.Fprintf(fullUrl, "%s/v2/file/get_download_url", API_URL) 46 | logger.Verboseln("do request url: " + fullUrl.String()) 47 | 48 | // data 49 | sec := param.ExpireSec 50 | if sec <= 0 { 51 | sec = 14400 52 | } 53 | postData := map[string]interface{}{ 54 | "drive_id": param.DriveId, 55 | "file_id": param.FileId, 56 | "expire_sec": sec, 57 | } 58 | 59 | // request 60 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 61 | if err != nil { 62 | logger.Verboseln("get file download url error ", err) 63 | return nil, apierror.NewFailedApiError(err.Error()) 64 | } 65 | 66 | // handler common error 67 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 68 | return nil, err1 69 | } 70 | 71 | // parse result 72 | r := &aliyunpan.GetFileDownloadUrlResult{} 73 | if err2 := json.Unmarshal(body, r); err2 != nil { 74 | logger.Verboseln("parse file download url result json error ", err2) 75 | return nil, apierror.NewFailedApiError(err2.Error()) 76 | } 77 | // time format 78 | r.Expiration = apiutil.UtcTime2LocalFormat(r.Expiration) 79 | return r, nil 80 | } 81 | 82 | // DownloadFileData 下载文件内容 83 | func (p *WebPanClient) DownloadFileData(downloadFileUrl string, fileRange aliyunpan.FileDownloadRange, downloadFunc aliyunpan.DownloadFuncCallback) *apierror.ApiError { 84 | // url 85 | fullUrl := &strings.Builder{} 86 | fmt.Fprintf(fullUrl, "%s", downloadFileUrl) 87 | logger.Verboseln("do request url: " + fullUrl.String()) 88 | 89 | // header 90 | headers := map[string]string{ 91 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 92 | "referer": "https://www.aliyundrive.com/", 93 | } 94 | 95 | // download data resume 96 | if fileRange.Offset != 0 || fileRange.End != 0 { 97 | rangeStr := "bytes=" + strconv.FormatInt(fileRange.Offset, 10) + "-" 98 | if fileRange.End != 0 { 99 | rangeStr += strconv.FormatInt(fileRange.End, 10) 100 | } 101 | headers["range"] = rangeStr 102 | } 103 | logger.Verboseln("do request url: " + fullUrl.String()) 104 | 105 | // request callback 106 | _, err := downloadFunc("GET", fullUrl.String(), headers) 107 | //resp, err := p.client.Req("GET", fullUrl.String(), nil, headers) 108 | 109 | if err != nil { 110 | logger.Verboseln("download file data response failed") 111 | return apierror.NewApiErrorWithError(err) 112 | } 113 | return nil 114 | } 115 | 116 | // DownloadFileDataAndSave 下载文件并存储到指定IO设备里面。该方法是同步阻塞的 117 | func (p *WebPanClient) DownloadFileDataAndSave(downloadFileUrl string, fileRange aliyunpan.FileDownloadRange, writerAt io.WriterAt) *apierror.ApiError { 118 | var resp *http.Response 119 | var err error 120 | var client = requester.NewHTTPClient() 121 | 122 | apierr := p.DownloadFileData( 123 | downloadFileUrl, 124 | fileRange, 125 | func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) { 126 | resp, err = client.Req(httpMethod, fullUrl, nil, headers) 127 | if err != nil { 128 | return nil, err 129 | } 130 | return resp, err 131 | }) 132 | 133 | if apierr != nil { 134 | return apierr 135 | } 136 | 137 | // close socket defer 138 | if resp != nil { 139 | defer func() { 140 | resp.Body.Close() 141 | }() 142 | } 143 | 144 | switch resp.StatusCode { 145 | case 200, 206: 146 | // do nothing, continue 147 | break 148 | case 416: //Requested Range Not Satisfiable 149 | fallthrough 150 | case 403: // Forbidden 151 | fallthrough 152 | case 406: // Not Acceptable 153 | return apierror.NewFailedApiError("") 154 | case 404: 155 | return apierror.NewFailedApiError("") 156 | case 429, 509: // Too Many Requests 157 | return apierror.NewFailedApiError("") 158 | default: 159 | return apierror.NewApiErrorWithError(fmt.Errorf("unexpected http status code, %d, %s", resp.StatusCode, resp.Status)) 160 | } 161 | 162 | // save data 163 | var ( 164 | buf = make([]byte, 4096) 165 | totalCount, readByteCount int 166 | ) 167 | defer cachepool.SyncPool.Put(buf) 168 | 169 | var readErr error 170 | totalCount = 0 171 | 172 | for true { 173 | readByteCount, readErr = resp.Body.Read(buf) 174 | logger.Verboseln("get byte piece:", readByteCount) 175 | if readErr == io.EOF && readByteCount > 0 { 176 | // the last piece 177 | writerAt.WriteAt(buf[:readByteCount], fileRange.Offset+int64(totalCount)) 178 | totalCount += readByteCount 179 | break 180 | } 181 | if readErr != nil { 182 | return apierror.NewApiErrorWithError(readErr) 183 | } 184 | 185 | // write 186 | writerAt.WriteAt(buf[:readByteCount], fileRange.Offset+int64(totalCount)) 187 | totalCount += readByteCount 188 | } 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /aliyunpan_web/file_move.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "fmt" 19 | "github.com/tickstep/aliyunpan-api/aliyunpan" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 22 | "github.com/tickstep/library-go/logger" 23 | "strings" 24 | ) 25 | 26 | type () 27 | 28 | // FileMove 移动文件 29 | func (p *WebPanClient) FileMove(param []*aliyunpan.FileMoveParam) ([]*aliyunpan.FileMoveResult, *apierror.ApiError) { 30 | // url 31 | fullUrl := &strings.Builder{} 32 | fmt.Fprintf(fullUrl, "%s/adrive/v4/batch", API_URL) 33 | logger.Verboseln("do request url: " + fullUrl.String()) 34 | 35 | // data 36 | requests, e := p.getFileMoveBatchRequestList(param) 37 | if e != nil { 38 | return nil, e 39 | } 40 | batchParam := BatchRequestParam{ 41 | Requests: requests, 42 | Resource: "file", 43 | } 44 | 45 | // request 46 | result, err := p.BatchTask(fullUrl.String(), &batchParam) 47 | if err != nil { 48 | logger.Verboseln("file move error ", err) 49 | return nil, apierror.NewFailedApiError(err.Error()) 50 | } 51 | 52 | // parse result 53 | r := []*aliyunpan.FileMoveResult{} 54 | for _, item := range result.Responses { 55 | r = append(r, &aliyunpan.FileMoveResult{ 56 | FileId: item.Id, 57 | Success: item.Status == 200, 58 | }) 59 | } 60 | return r, nil 61 | } 62 | 63 | func (p *WebPanClient) getFileMoveBatchRequestList(param []*aliyunpan.FileMoveParam) (BatchRequestList, *apierror.ApiError) { 64 | if param == nil { 65 | return nil, apierror.NewFailedApiError("参数不能为空") 66 | } 67 | 68 | r := BatchRequestList{} 69 | for _, item := range param { 70 | r = append(r, &BatchRequest{ 71 | Id: item.FileId, 72 | Method: "POST", 73 | Url: "/file/move", 74 | Headers: map[string]string{ 75 | "Content-Type": "application/json", 76 | }, 77 | Body: apiutil.GetMapSet(item), 78 | }) 79 | } 80 | return r, nil 81 | } 82 | -------------------------------------------------------------------------------- /aliyunpan_web/file_recycle.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 9 | "github.com/tickstep/library-go/logger" 10 | "strings" 11 | ) 12 | 13 | type ( 14 | RecycleBinFileListParam struct { 15 | DriveId string `json:"drive_id"` 16 | Limit int `json:"limit"` 17 | Marker string `json:"marker"` 18 | } 19 | 20 | RecycleBinFileClearParam struct { 21 | DriveId string `json:"drive_id"` 22 | } 23 | RecycleBinFileClearResult struct { 24 | DomainId string `json:"domain_id"` 25 | DriveId string `json:"drive_id"` 26 | TaskId string `json:"task_id"` 27 | AsyncTaskId string `json:"async_task_id"` 28 | } 29 | ) 30 | 31 | // RecycleBinFileList 获取回收站文件列表 32 | func (p *WebPanClient) RecycleBinFileList(param *RecycleBinFileListParam) (*aliyunpan.FileListResult, *apierror.ApiError) { 33 | result := &aliyunpan.FileListResult{ 34 | FileList: aliyunpan.FileList{}, 35 | NextMarker: "", 36 | } 37 | if flr, err := p.recycleBinFileListReq(param); err == nil { 38 | for k := range flr.Items { 39 | if flr.Items[k] == nil { 40 | continue 41 | } 42 | 43 | result.FileList = append(result.FileList, createFileEntity(flr.Items[k])) 44 | } 45 | result.NextMarker = flr.NextMarker 46 | } 47 | return result, nil 48 | } 49 | 50 | // RecycleBinFileListGetAll 获取所有列表文件 51 | func (p *WebPanClient) RecycleBinFileListGetAll(param *RecycleBinFileListParam) (aliyunpan.FileList, *apierror.ApiError) { 52 | internalParam := &RecycleBinFileListParam{ 53 | DriveId: param.DriveId, 54 | Limit: param.Limit, 55 | Marker: param.Marker, 56 | } 57 | if internalParam.Limit <= 0 { 58 | internalParam.Limit = 100 59 | } 60 | 61 | fileList := aliyunpan.FileList{} 62 | result, err := p.RecycleBinFileList(internalParam) 63 | if err != nil || result == nil { 64 | return nil, err 65 | } 66 | fileList = append(fileList, result.FileList...) 67 | 68 | // more page? 69 | for len(result.NextMarker) > 0 { 70 | internalParam.Marker = result.NextMarker 71 | result, err = p.RecycleBinFileList(internalParam) 72 | if err == nil && result != nil { 73 | fileList = append(fileList, result.FileList...) 74 | } else { 75 | break 76 | } 77 | } 78 | return fileList, nil 79 | } 80 | 81 | func (p *WebPanClient) recycleBinFileListReq(param *RecycleBinFileListParam) (*fileListResult, *apierror.ApiError) { 82 | header := map[string]string{ 83 | "authorization": p.webToken.GetAuthorizationStr(), 84 | "referer": "https://www.aliyundrive.com/", 85 | "origin": "https://www.aliyundrive.com", 86 | } 87 | 88 | fullUrl := &strings.Builder{} 89 | fmt.Fprintf(fullUrl, "%s/adrive/v2/recyclebin/list", API_URL) 90 | logger.Verboseln("do request url: " + fullUrl.String()) 91 | 92 | limit := param.Limit 93 | if limit <= 0 { 94 | limit = 100 95 | } 96 | postData := map[string]interface{}{ 97 | "drive_id": param.DriveId, 98 | "limit": limit, 99 | "image_thumbnail_process": "image/resize,w_400/format,jpeg", 100 | "video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_800", 101 | "order_by": "name", 102 | "order_direction": "DESC", 103 | } 104 | if len(param.Marker) > 0 { 105 | postData["marker"] = param.Marker 106 | } 107 | 108 | // request 109 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 110 | if err != nil { 111 | logger.Verboseln("get recycle bin file list error ", err) 112 | return nil, apierror.NewFailedApiError(err.Error()) 113 | } 114 | 115 | // handler common error 116 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 117 | return nil, err1 118 | } 119 | 120 | // parse result 121 | r := &fileListResult{} 122 | if err2 := json.Unmarshal(body, r); err2 != nil { 123 | logger.Verboseln("parse recycle bin file list result json error ", err2) 124 | return nil, apierror.NewFailedApiError(err2.Error()) 125 | } 126 | return r, nil 127 | } 128 | 129 | // RecycleBinFileClear 清空回收站 130 | func (p *WebPanClient) RecycleBinFileClear(param *RecycleBinFileClearParam) (*RecycleBinFileClearResult, *apierror.ApiError) { 131 | header := map[string]string{ 132 | "authorization": p.webToken.GetAuthorizationStr(), 133 | "referer": "https://www.aliyundrive.com/", 134 | "origin": "https://www.aliyundrive.com", 135 | } 136 | 137 | fullUrl := &strings.Builder{} 138 | fmt.Fprintf(fullUrl, "%s/v2/recyclebin/clear", API_URL) 139 | logger.Verboseln("do request url: " + fullUrl.String()) 140 | 141 | postData := map[string]interface{}{ 142 | "drive_id": param.DriveId, 143 | } 144 | // request 145 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 146 | if err != nil { 147 | logger.Verboseln("clear recycle bin file error ", err) 148 | return nil, apierror.NewFailedApiError(err.Error()) 149 | } 150 | 151 | // handler common error 152 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 153 | return nil, err1 154 | } 155 | 156 | // parse result 157 | r := &RecycleBinFileClearResult{} 158 | if err2 := json.Unmarshal(body, r); err2 != nil { 159 | logger.Verboseln("parse recycle bin file clear result json error ", err2) 160 | return nil, apierror.NewFailedApiError(err2.Error()) 161 | } 162 | return r, nil 163 | } 164 | -------------------------------------------------------------------------------- /aliyunpan_web/file_rename.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 22 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 23 | "github.com/tickstep/library-go/logger" 24 | "strings" 25 | ) 26 | 27 | // FileRename 重命名文件 28 | func (p *WebPanClient) FileRename(driveId, renameFileId, newName string) (bool, *apierror.ApiError) { 29 | if renameFileId == "" { 30 | return false, apierror.NewFailedApiError("请指定命名的文件") 31 | } 32 | // header 33 | header := map[string]string{ 34 | "authorization": p.webToken.GetAuthorizationStr(), 35 | } 36 | 37 | // url 38 | fullUrl := &strings.Builder{} 39 | fmt.Fprintf(fullUrl, "%s/adrive/v3/file/update", API_URL) 40 | logger.Verboseln("do request url: " + fullUrl.String()) 41 | 42 | // data 43 | postData := map[string]interface{}{ 44 | "drive_id": driveId, 45 | "file_id": renameFileId, 46 | "name": newName, 47 | "check_name_mode": "refuse", 48 | } 49 | 50 | // request 51 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 52 | if err != nil { 53 | logger.Verboseln("get rename error ", err) 54 | return false, apierror.NewFailedApiError(err.Error()) 55 | } 56 | 57 | // handler common error 58 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 59 | return false, err1 60 | } 61 | 62 | // parse result 63 | r := &aliyunpan.FileEntity{} 64 | if err2 := json.Unmarshal(body, r); err2 != nil { 65 | logger.Verboseln("parse rename result json error ", err2) 66 | return false, apierror.NewFailedApiError(err2.Error()) 67 | } 68 | return true, nil 69 | } 70 | -------------------------------------------------------------------------------- /aliyunpan_web/file_share.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 9 | "github.com/tickstep/library-go/logger" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type ( 15 | shareEntityResult struct { 16 | CreatedAt string `json:"created_at"` 17 | Creator string `json:"creator"` 18 | Description string `json:"description"` 19 | // 下载次数 20 | DownloadCount int `json:"download_count"` 21 | DriveId string `json:"drive_id"` 22 | Expiration string `json:"expiration"` 23 | Expired bool `json:"expired"` 24 | FileId string `json:"file_id"` 25 | FileIdList []string `json:"file_id_list"` 26 | // 浏览次数 27 | PreviewCount int `json:"preview_count"` 28 | // 转存次数 29 | SaveCount int `json:"save_count"` 30 | ShareId string `json:"share_id"` 31 | ShareMsg string `json:"share_msg"` 32 | ShareName string `json:"share_name"` 33 | SharePolicy string `json:"share_policy"` 34 | SharePwd string `json:"share_pwd"` 35 | ShareUrl string `json:"share_url"` 36 | Status string `json:"status"` 37 | UpdatedAt string `json:"updated_at"` 38 | 39 | FirstFile *fileEntityResult `json:"first_file"` 40 | } 41 | 42 | ShareListParam struct { 43 | Creator string `json:"creator"` 44 | Limit int64 `json:"limit"` 45 | Marker string `json:"marker"` 46 | } 47 | 48 | ShareListResult struct { 49 | Items []*shareEntityResult `json:"items"` 50 | NextMarker string `json:"next_marker"` 51 | } 52 | 53 | ShareCancelResult struct { 54 | // 分享ID 55 | Id string 56 | // 是否成功 57 | Success bool 58 | } 59 | ) 60 | 61 | func createShareEntity(item *shareEntityResult) *aliyunpan.ShareEntity { 62 | if item == nil { 63 | return nil 64 | } 65 | return &aliyunpan.ShareEntity{ 66 | Creator: item.Creator, 67 | DriveId: item.DriveId, 68 | ShareId: item.ShareId, 69 | ShareName: item.ShareName, 70 | SharePwd: item.SharePwd, 71 | ShareUrl: item.ShareUrl, 72 | FileIdList: item.FileIdList, 73 | SaveCount: item.SaveCount, 74 | Status: item.Status, 75 | Expiration: apiutil.UtcTime2LocalFormat(item.Expiration), 76 | UpdatedAt: apiutil.UtcTime2LocalFormat(item.UpdatedAt), 77 | CreatedAt: apiutil.UtcTime2LocalFormat(item.CreatedAt), 78 | FirstFile: createFileEntity(item.FirstFile), 79 | } 80 | } 81 | 82 | // ShareLinkList 获取所有分享链接列表 83 | func (p *WebPanClient) ShareLinkList(userId string) ([]*aliyunpan.ShareEntity, *apierror.ApiError) { 84 | resultList := []*aliyunpan.ShareEntity{} 85 | param := ShareListParam{ 86 | Creator: userId, 87 | Limit: 100, 88 | Marker: "", 89 | } 90 | for { 91 | if r, e := p.GetShareLinkListReq(param); e == nil { 92 | for _, item := range r.Items { 93 | resultList = append(resultList, createShareEntity(item)) 94 | } 95 | 96 | // next page? 97 | if r.NextMarker != "" { 98 | param.Marker = r.NextMarker 99 | time.Sleep(500 * time.Millisecond) 100 | } else { 101 | break 102 | } 103 | } else { 104 | return nil, e 105 | } 106 | } 107 | return resultList, nil 108 | } 109 | 110 | // ShareLinkCancel 取消分享链接 111 | func (p *WebPanClient) ShareLinkCancel(shareIdList []string) ([]*ShareCancelResult, *apierror.ApiError) { 112 | // url 113 | fullUrl := &strings.Builder{} 114 | fmt.Fprintf(fullUrl, "%s/adrive/v4/batch", API_URL) 115 | logger.Verboseln("do request url: " + fullUrl.String()) 116 | 117 | // param 118 | pr := BatchRequestList{} 119 | for _, shareId := range shareIdList { 120 | pr = append(pr, &BatchRequest{ 121 | Id: shareId, 122 | Method: "POST", 123 | Url: "/share_link/cancel", 124 | Headers: map[string]string{ 125 | "Content-Type": "application/json", 126 | }, 127 | Body: map[string]interface{}{ 128 | "share_id": shareId, 129 | }, 130 | }) 131 | } 132 | 133 | batchParam := BatchRequestParam{ 134 | Requests: pr, 135 | Resource: "file", 136 | } 137 | 138 | // request 139 | result, err := p.BatchTask(fullUrl.String(), &batchParam) 140 | if err != nil { 141 | logger.Verboseln("share cancel error ", err) 142 | return nil, apierror.NewFailedApiError(err.Error()) 143 | } 144 | 145 | // parse result 146 | r := []*ShareCancelResult{} 147 | for _, item := range result.Responses { 148 | r = append(r, &ShareCancelResult{ 149 | Id: item.Id, 150 | Success: item.Status == 204, 151 | }) 152 | } 153 | return r, nil 154 | } 155 | 156 | // ShareLinkCreate 创建分享 157 | func (p *WebPanClient) ShareLinkCreate(param aliyunpan.ShareCreateParam) (*aliyunpan.ShareEntity, *apierror.ApiError) { 158 | // header 159 | header := map[string]string{ 160 | "authorization": p.webToken.GetAuthorizationStr(), 161 | } 162 | 163 | // url 164 | fullUrl := &strings.Builder{} 165 | fmt.Fprintf(fullUrl, "%s/adrive/v2/share_link/create", API_URL) 166 | logger.Verboseln("do request url: " + fullUrl.String()) 167 | 168 | // data 169 | postData := param 170 | 171 | // check pwd 172 | if postData.SharePwd != "" && len(postData.SharePwd) != 4 { 173 | return nil, apierror.NewFailedApiError("密码必须是4个字符") 174 | } 175 | 176 | // format time 177 | if postData.Expiration != "" { 178 | postData.Expiration = apiutil.LocalTime2UtcFormat(param.Expiration) 179 | } 180 | 181 | // request 182 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 183 | if err != nil { 184 | logger.Verboseln("create share list error ", err) 185 | return nil, apierror.NewFailedApiError(err.Error()) 186 | } 187 | logger.Verboseln("response: ", string(body)) 188 | 189 | // handler common error 190 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 191 | return nil, err1 192 | } 193 | 194 | // parse result 195 | r := &shareEntityResult{} 196 | if err2 := json.Unmarshal(body, r); err2 != nil { 197 | logger.Verboseln("parse share create result json error ", err2) 198 | return nil, apierror.NewFailedApiError(err2.Error()) 199 | } 200 | return createShareEntity(r), nil 201 | } 202 | 203 | func (p *WebPanClient) GetShareLinkListReq(param ShareListParam) (*ShareListResult, *apierror.ApiError) { 204 | // header 205 | header := map[string]string{ 206 | "authorization": p.webToken.GetAuthorizationStr(), 207 | } 208 | 209 | // url 210 | fullUrl := &strings.Builder{} 211 | fmt.Fprintf(fullUrl, "%s/adrive/v3/share_link/list", API_URL) 212 | logger.Verboseln("do request url: " + fullUrl.String()) 213 | 214 | if param.Limit <= 0 { 215 | param.Limit = 100 216 | } 217 | // data 218 | postData := map[string]interface{}{ 219 | "category": "file,album", 220 | "creator": param.Creator, 221 | "include_canceled": false, 222 | "order_by": "created_at", 223 | "order_direction": "DESC", 224 | "limit": param.Limit, 225 | } 226 | if param.Marker != "" { 227 | postData["marker"] = param.Marker 228 | } 229 | 230 | // request 231 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 232 | logger.Verboseln(string(body)) 233 | if err != nil { 234 | logger.Verboseln("get share list error ", err) 235 | return nil, apierror.NewFailedApiError(err.Error()) 236 | } 237 | 238 | // handler common error 239 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 240 | return nil, err1 241 | } 242 | 243 | // parse result 244 | r := &ShareListResult{} 245 | if err2 := json.Unmarshal(body, r); err2 != nil { 246 | logger.Verboseln("parse share list result json error ", err2) 247 | return nil, apierror.NewFailedApiError(err2.Error()) 248 | } 249 | return r, nil 250 | } 251 | 252 | // FastShareLinkCreate 创建快传分享 253 | func (p *WebPanClient) FastShareLinkCreate(param aliyunpan.FastShareCreateParam) (*aliyunpan.FastShareCreateResult, *apierror.ApiError) { 254 | // header 255 | header := map[string]string{ 256 | "authorization": p.webToken.GetAuthorizationStr(), 257 | } 258 | 259 | // url 260 | fullUrl := &strings.Builder{} 261 | fmt.Fprintf(fullUrl, "%s/adrive/v1/share/create", API_URL) 262 | logger.Verboseln("do request url: " + fullUrl.String()) 263 | 264 | // data 265 | var fileList []aliyunpan.FastShareFileItem 266 | for _, fileId := range param.FileIdList { 267 | fileList = append(fileList, aliyunpan.FastShareFileItem{DriveId: param.DriveId, FileId: fileId}) 268 | } 269 | postData := struct { 270 | DriveFileList []aliyunpan.FastShareFileItem `json:"drive_file_list"` 271 | }{DriveFileList: fileList} 272 | 273 | // request 274 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 275 | if err != nil { 276 | logger.Verboseln("create fast share list error ", err) 277 | return nil, apierror.NewFailedApiError(err.Error()) 278 | } 279 | logger.Verboseln("response: ", string(body)) 280 | 281 | // handler common error 282 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 283 | return nil, err1 284 | } 285 | 286 | // parse result 287 | r := &aliyunpan.FastShareCreateResult{} 288 | if err2 := json.Unmarshal(body, r); err2 != nil { 289 | logger.Verboseln("parse fast share create result json error ", err2) 290 | return nil, apierror.NewFailedApiError(err2.Error()) 291 | } 292 | r.Expiration = apiutil.UtcTime2LocalFormat(r.Expiration) 293 | return r, nil 294 | } 295 | -------------------------------------------------------------------------------- /aliyunpan_web/file_starred.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "fmt" 19 | "github.com/tickstep/aliyunpan-api/aliyunpan" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 22 | "github.com/tickstep/library-go/logger" 23 | "strings" 24 | ) 25 | 26 | type () 27 | 28 | // FileStarred 收藏文件 29 | func (p *WebPanClient) FileStarred(param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 30 | return p.doFileStarredBatchRequestList(true, param) 31 | } 32 | 33 | // FileUnstarred 取消收藏文件 34 | func (p *WebPanClient) FileUnstarred(param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 35 | return p.doFileStarredBatchRequestList(false, param) 36 | } 37 | 38 | func (p *WebPanClient) doFileStarredBatchRequestList(starred bool, param []*aliyunpan.FileBatchActionParam) ([]*aliyunpan.FileBatchActionResult, *apierror.ApiError) { 39 | if param == nil { 40 | return nil, apierror.NewFailedApiError("参数不能为空") 41 | } 42 | 43 | // url 44 | fullUrl := &strings.Builder{} 45 | fmt.Fprintf(fullUrl, "%s/v2/batch", API_URL) 46 | logger.Verboseln("do request url: " + fullUrl.String()) 47 | 48 | // param 49 | pr := BatchRequestList{} 50 | for _, item := range param { 51 | body := apiutil.GetMapSet(item) 52 | if starred { 53 | body["starred"] = true 54 | body["custom_index_key"] = "starred_yes" 55 | } else { 56 | body["starred"] = false 57 | body["custom_index_key"] = "" 58 | } 59 | 60 | pr = append(pr, &BatchRequest{ 61 | Id: item.FileId, 62 | Method: "PUT", 63 | Url: "/file/update", 64 | Headers: map[string]string{ 65 | "Content-Type": "application/json", 66 | }, 67 | Body: body, 68 | }) 69 | } 70 | 71 | batchParam := BatchRequestParam{ 72 | Requests: pr, 73 | Resource: "file", 74 | } 75 | 76 | // request 77 | result, err := p.BatchTask(fullUrl.String(), &batchParam) 78 | if err != nil { 79 | logger.Verboseln("file starred error ", err) 80 | return nil, apierror.NewFailedApiError(err.Error()) 81 | } 82 | 83 | // parse result 84 | r := []*aliyunpan.FileBatchActionResult{} 85 | for _, item := range result.Responses { 86 | r = append(r, &aliyunpan.FileBatchActionResult{ 87 | FileId: item.Id, 88 | Success: item.Status == 200, 89 | }) 90 | } 91 | return r, nil 92 | } 93 | -------------------------------------------------------------------------------- /aliyunpan_web/file_upload.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 9 | "github.com/tickstep/library-go/logger" 10 | "github.com/tickstep/library-go/requester" 11 | "strings" 12 | ) 13 | 14 | type ( 15 | completeUploadFileReqResult struct { 16 | DriveId string `json:"drive_id"` 17 | DomainId string `json:"domain_id"` 18 | FileId string `json:"file_id"` 19 | Name string `json:"name"` 20 | Type string `json:"type"` 21 | ContentType string `json:"content_type"` 22 | CreatedAt string `json:"created_at"` 23 | UpdatedAt string `json:"updated_at"` 24 | FileExtension string `json:"file_extension"` 25 | Hidden bool `json:"hidden"` 26 | Size int64 `json:"size"` 27 | Starred bool `json:"starred"` 28 | Status string `json:"status"` 29 | UploadId string `json:"upload_id"` 30 | ParentFileId string `json:"parent_file_id"` 31 | Crc64Hash string `json:"crc64_hash"` 32 | ContentHash string `json:"content_hash"` 33 | ContentHashName string `json:"content_hash_name"` 34 | Category string `json:"category"` 35 | EncryptMode string `json:"encrypt_mode"` 36 | Location string `json:"location"` 37 | } 38 | ) 39 | 40 | const () 41 | 42 | // CreateUploadFile 创建上传文件,如果文件已经上传过则会直接秒传 43 | func (p *WebPanClient) CreateUploadFile(param *aliyunpan.CreateFileUploadParam) (*aliyunpan.CreateFileUploadResult, *apierror.ApiError) { 44 | // header 45 | header := map[string]string{ 46 | "authorization": p.webToken.GetAuthorizationStr(), 47 | } 48 | 49 | // url 50 | fullUrl := &strings.Builder{} 51 | fmt.Fprintf(fullUrl, "%s/adrive/v2/file/createWithFolders", API_URL) 52 | logger.Verboseln("do request url: " + fullUrl.String()) 53 | 54 | // data 55 | postData := param 56 | 57 | if len(postData.PartInfoList) == 0 { 58 | blockSize := aliyunpan.DefaultChunkSize 59 | if param.BlockSize > 0 { 60 | blockSize = param.BlockSize 61 | } 62 | postData.PartInfoList = aliyunpan.GenerateFileUploadPartInfoListWithChunkSize(param.Size, blockSize) 63 | } 64 | if postData.ContentHashName == "" { 65 | postData.ContentHashName = "sha1" 66 | } 67 | if postData.ParentFileId == "" { 68 | postData.ParentFileId = aliyunpan.DefaultRootParentFileId 69 | } 70 | if postData.ProofVersion == "" { 71 | postData.ProofVersion = "v1" 72 | } 73 | if postData.CheckNameMode == "" { 74 | postData.CheckNameMode = "auto_rename" 75 | } 76 | postData.Type = "file" 77 | 78 | // request 79 | resp, err := p.client.Req("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 80 | if err != nil { 81 | logger.Verboseln("create upload file error ", err) 82 | return nil, apierror.NewFailedApiError(err.Error()) 83 | } 84 | 85 | // handler common error 86 | body, err1 := apierror.ParseCommonResponseApiError(resp) 87 | if err1 != nil { 88 | return nil, err1 89 | } 90 | 91 | // parse result 92 | r := &aliyunpan.CreateFileUploadResult{} 93 | if err2 := json.Unmarshal(body, r); err2 != nil { 94 | logger.Verboseln("parse create upload file result json error ", err2) 95 | return nil, apierror.NewFailedApiError(err2.Error()) 96 | } 97 | return r, nil 98 | } 99 | 100 | // GetUploadUrl 获取上传数据链接参数 101 | // 因为有些文件过大,或者暂定上传后,然后过段时间再继续上传,这时候之前的上传链接可能已经失效了,所以需要重新获取上传数据的链接 102 | // 如果该文件已经上传完毕,则该接口返回错误 103 | func (p *WebPanClient) GetUploadUrl(param *aliyunpan.GetUploadUrlParam) (*aliyunpan.GetUploadUrlResult, *apierror.ApiError) { 104 | // header 105 | header := map[string]string{ 106 | "authorization": p.webToken.GetAuthorizationStr(), 107 | } 108 | 109 | // url 110 | fullUrl := &strings.Builder{} 111 | fmt.Fprintf(fullUrl, "%s/v2/file/get_upload_url", API_URL) 112 | logger.Verboseln("do request url: " + fullUrl.String()) 113 | 114 | // data 115 | postData := param 116 | 117 | // request 118 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 119 | if err != nil { 120 | logger.Verboseln("get upload url error ", err) 121 | return nil, apierror.NewFailedApiError(err.Error()) 122 | } 123 | 124 | // handler common error 125 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 126 | return nil, err1 127 | } 128 | 129 | // parse result 130 | r := &aliyunpan.GetUploadUrlResult{} 131 | if err2 := json.Unmarshal(body, r); err2 != nil { 132 | logger.Verboseln("parse get upload url result json error ", err2) 133 | return nil, apierror.NewFailedApiError(err2.Error()) 134 | } 135 | r.CreateAt = apiutil.UtcTime2LocalFormat(r.CreateAt) 136 | return r, nil 137 | } 138 | 139 | // UploadFileData 上传文件数据 140 | func (p *WebPanClient) UploadFileData(uploadUrl string, uploadFunc aliyunpan.UploadFunc) *apierror.ApiError { 141 | // header 142 | header := map[string]string{ 143 | "referer": "https://www.aliyundrive.com/", 144 | } 145 | 146 | // url 147 | fullUrl := &strings.Builder{} 148 | fmt.Fprintf(fullUrl, "%s", uploadUrl) 149 | logger.Verboseln("do request url: " + fullUrl.String()) 150 | 151 | // request 152 | if uploadFunc != nil { 153 | resp, err := uploadFunc("PUT", fullUrl.String(), header) 154 | if err != nil || (resp != nil && resp.StatusCode != 200) { 155 | logger.Verboseln("upload file data chunk error ", err) 156 | return apierror.NewFailedApiError("update data error") 157 | } 158 | } 159 | return nil 160 | } 161 | 162 | // UploadDataChunk 上传数据。该方法是同步阻塞的 163 | func (p *WebPanClient) UploadDataChunk(url string, data *aliyunpan.FileUploadChunkData) *apierror.ApiError { 164 | var client = requester.NewHTTPClient() 165 | 166 | // header 167 | header := map[string]string{ 168 | "referer": "https://www.aliyundrive.com/", 169 | } 170 | 171 | // url 172 | fullUrl := &strings.Builder{} 173 | fmt.Fprintf(fullUrl, "%s", url) 174 | logger.Verboseln("do request url: " + fullUrl.String()) 175 | 176 | // data 177 | if data == nil || data.Reader == nil || data.Len() == 0 { 178 | return apierror.NewFailedApiError("数据块错误") 179 | } 180 | // request 181 | resp, err := client.Req("PUT", fullUrl.String(), data, header) 182 | if err != nil || resp.StatusCode != 200 { 183 | logger.Verboseln("upload file data chunk error ", err) 184 | return apierror.NewFailedApiError(err.Error()) 185 | } 186 | return nil 187 | } 188 | 189 | // CompleteUploadFile 完成文件上传确认。完成文件数据上传后,需要调用该接口文件才会显示再网盘中 190 | func (p *WebPanClient) CompleteUploadFile(param *aliyunpan.CompleteUploadFileParam) (*aliyunpan.CompleteUploadFileResult, *apierror.ApiError) { 191 | // header 192 | header := map[string]string{ 193 | "authorization": p.webToken.GetAuthorizationStr(), 194 | } 195 | 196 | // url 197 | fullUrl := &strings.Builder{} 198 | fmt.Fprintf(fullUrl, "%s/v2/file/complete", API_URL) 199 | logger.Verboseln("do request url: " + fullUrl.String()) 200 | 201 | // data 202 | postData := map[string]interface{}{ 203 | "ignoreError": true, 204 | "drive_id": param.DriveId, 205 | "file_id": param.FileId, 206 | "upload_id": param.UploadId, 207 | } 208 | 209 | // request 210 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 211 | if err != nil { 212 | logger.Verboseln("complete upload file error ", err) 213 | return nil, apierror.NewFailedApiError(err.Error()) 214 | } 215 | 216 | // handler common error 217 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 218 | return nil, err1 219 | } 220 | 221 | // parse result 222 | r := &completeUploadFileReqResult{} 223 | if err2 := json.Unmarshal(body, r); err2 != nil { 224 | logger.Verboseln("parse complete upload file result json error ", err2) 225 | return nil, apierror.NewFailedApiError(err2.Error()) 226 | } 227 | 228 | return &aliyunpan.CompleteUploadFileResult{ 229 | DriveId: r.DriveId, 230 | DomainId: r.DomainId, 231 | FileId: r.FileId, 232 | Name: r.Name, 233 | Type: r.Type, 234 | Size: r.Size, 235 | UploadId: r.UploadId, 236 | ParentFileId: r.ParentFileId, 237 | Crc64Hash: r.Crc64Hash, 238 | ContentHash: r.ContentHash, 239 | ContentHashName: r.ContentHashName, 240 | CreatedAt: apiutil.UtcTime2LocalFormat(r.CreatedAt), 241 | }, nil 242 | } 243 | -------------------------------------------------------------------------------- /aliyunpan_web/file_video.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 9 | "github.com/tickstep/library-go/logger" 10 | "strings" 11 | ) 12 | 13 | type () 14 | 15 | // VideoGetPreviewPlayInfo 获取视频预览信息,调用该接口会触发视频云端转码 16 | func (p *WebPanClient) VideoGetPreviewPlayInfo(param *aliyunpan.VideoGetPreviewPlayInfoParam) (*aliyunpan.VideoGetPreviewPlayInfoResult, error) { 17 | header := map[string]string{ 18 | "authorization": p.webToken.GetAuthorizationStr(), 19 | } 20 | 21 | fullUrl := &strings.Builder{} 22 | fmt.Fprintf(fullUrl, "%s/v2/file/get_video_preview_play_info", API_URL) 23 | logger.Verboseln("do request url: " + fullUrl.String()) 24 | 25 | postData := map[string]interface{}{ 26 | "category": "live_transcoding", 27 | "drive_id": param.DriveId, 28 | "file_id": param.FileId, 29 | "template_id": "", 30 | } 31 | 32 | // request 33 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 34 | logger.Verboseln("response: " + string(body)) 35 | if err != nil { 36 | logger.Verboseln("get video preview play info error ", err) 37 | return nil, apierror.NewFailedApiError(err.Error()) 38 | } 39 | 40 | // handler common error 41 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 42 | return nil, err1 43 | } 44 | 45 | // parse result 46 | r := &aliyunpan.VideoGetPreviewPlayInfoResult{} 47 | if err2 := json.Unmarshal(body, r); err2 != nil { 48 | logger.Verboseln("parse video preview play info json error ", err2) 49 | return nil, apierror.NewFailedApiError(err2.Error()) 50 | } 51 | return r, nil 52 | } 53 | -------------------------------------------------------------------------------- /aliyunpan_web/login.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // WEB端API 16 | package aliyunpan_web 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 22 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 23 | "github.com/tickstep/library-go/logger" 24 | "github.com/tickstep/library-go/requester" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | const () 30 | 31 | type ( 32 | refreshTokenResult struct { 33 | AccessToken string `json:"access_token"` 34 | RefreshToken string `json:"refresh_token"` 35 | ExpiresIn int `json:"expires_in"` 36 | TokenType string `json:"token_type"` 37 | UserId string `json:"user_id"` 38 | UserName string `json:"user_name"` 39 | NickName string `json:"nick_name"` 40 | DefaultDriveId string `json:"default_drive_id"` 41 | DefaultSboxDriveId string `json:"default_sbox_drive_id"` 42 | Role string `json:"role"` 43 | Status string `json:"status"` 44 | ExpireTime string `json:"expire_time"` 45 | DeviceId string `json:"device_id"` 46 | } 47 | 48 | WebLoginToken struct { 49 | AccessTokenType string `json:"accessTokenType"` 50 | AccessToken string `json:"accessToken"` 51 | RefreshToken string `json:"refreshToken"` 52 | ExpiresIn int `json:"expiresIn"` 53 | ExpireTime string `json:"expireTime"` 54 | } 55 | ) 56 | 57 | func (w *WebLoginToken) GetAuthorizationStr() string { 58 | return w.AccessTokenType + " " + w.AccessToken 59 | } 60 | 61 | func (w *WebLoginToken) IsAccessTokenExpired() bool { 62 | local, _ := time.LoadLocation("Local") 63 | expireTime, _ := time.ParseInLocation("2006-01-02 15:04:05", w.ExpireTime, local) 64 | now := time.Now() 65 | 66 | return (expireTime.Unix() - now.Unix()) < 60 67 | } 68 | 69 | func GetAccessTokenFromRefreshToken(refreshToken string) (*WebLoginToken, *apierror.ApiError) { 70 | myclient := requester.NewHTTPClient() 71 | 72 | header := map[string]string{} 73 | 74 | fullUrl := &strings.Builder{} 75 | fmt.Fprintf(fullUrl, "%s/v2/account/token", AUTH_URL) 76 | logger.Verboseln("do request url: " + fullUrl.String()) 77 | postData := map[string]string{ 78 | "refresh_token": refreshToken, 79 | "api_id": "pJZInNHN2dZWk8qg", 80 | "grant_type": "refresh_token", 81 | } 82 | 83 | body, err := myclient.Fetch("POST", fullUrl.String(), postData, apiutil.AddCommonHeader(header)) 84 | if err != nil { 85 | logger.Verboseln("get access token error ", err) 86 | return nil, apierror.NewFailedApiError(err.Error()) 87 | } 88 | 89 | // handler common error 90 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 91 | return nil, err1 92 | } 93 | 94 | r := &refreshTokenResult{} 95 | if err1 := json.Unmarshal(body, r); err1 != nil { 96 | logger.Verboseln("parse refresh token result json error ", err1) 97 | return nil, apierror.NewFailedApiError(err1.Error()) 98 | } 99 | 100 | result := &WebLoginToken{ 101 | r.TokenType, 102 | r.AccessToken, 103 | r.RefreshToken, 104 | r.ExpiresIn, 105 | apiutil.UtcTime2LocalFormat(r.ExpireTime), 106 | } 107 | return result, nil 108 | } 109 | -------------------------------------------------------------------------------- /aliyunpan_web/logout.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 8 | "github.com/tickstep/library-go/logger" 9 | "strings" 10 | ) 11 | 12 | type ( 13 | DeviceLogoutResult struct { 14 | Result bool `json:"result"` 15 | Success bool `json:"success"` 16 | Code string `json:"code"` 17 | Message string `json:"message"` 18 | } 19 | ) 20 | 21 | // DeviceLogout 退出登录,登录的设备会同步注销 22 | func (p *WebPanClient) DeviceLogout() (*DeviceLogoutResult, *apierror.ApiError) { 23 | // header 24 | header := map[string]string{ 25 | "authorization": p.webToken.GetAuthorizationStr(), 26 | } 27 | 28 | // url 29 | fullUrl := &strings.Builder{} 30 | fmt.Fprintf(fullUrl, "%s/users/v1/users/device_logout", API_URL) 31 | logger.Verboseln("do request url: " + fullUrl.String()) 32 | 33 | // data 34 | postData := map[string]interface{}{} 35 | 36 | // request 37 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 38 | if err != nil { 39 | logger.Verboseln("device logout error ", err) 40 | return nil, apierror.NewFailedApiError(err.Error()) 41 | } 42 | logger.Verboseln("device logout response: " + string(body)) 43 | 44 | // handler common error 45 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 46 | return nil, err1 47 | } 48 | 49 | // parse result 50 | r := &DeviceLogoutResult{} 51 | if err2 := json.Unmarshal(body, r); err2 != nil { 52 | logger.Verboseln("parse device logout result json error ", err2) 53 | return nil, apierror.NewFailedApiError(err2.Error()) 54 | } 55 | return r, nil 56 | } 57 | -------------------------------------------------------------------------------- /aliyunpan_web/mkdir.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 22 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 23 | "github.com/tickstep/library-go/logger" 24 | "strings" 25 | ) 26 | 27 | type () 28 | 29 | // Mkdir 创建文件夹 30 | func (p *WebPanClient) Mkdir(driveId, parentFileId, dirName string) (*aliyunpan.MkdirResult, *apierror.ApiError) { 31 | if parentFileId == "" { 32 | // 默认根目录 33 | parentFileId = aliyunpan.DefaultRootParentFileId 34 | } 35 | header := map[string]string{ 36 | "authorization": p.webToken.GetAuthorizationStr(), 37 | } 38 | 39 | fullUrl := &strings.Builder{} 40 | fmt.Fprintf(fullUrl, "%s/adrive/v2/file/createWithFolders", API_URL) 41 | logger.Verboseln("do request url: " + fullUrl.String()) 42 | 43 | postData := map[string]interface{}{ 44 | "drive_id": driveId, 45 | "parent_file_id": parentFileId, 46 | "name": dirName, 47 | "check_name_mode": "refuse", 48 | "type": "folder", 49 | } 50 | 51 | // request 52 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 53 | if err != nil { 54 | logger.Verboseln("get file info error ", err) 55 | return nil, apierror.NewFailedApiError(err.Error()) 56 | } 57 | 58 | // handler common error 59 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 60 | return nil, err1 61 | } 62 | 63 | // parse result 64 | r := &aliyunpan.MkdirResult{} 65 | if err2 := json.Unmarshal(body, r); err2 != nil { 66 | logger.Verboseln("parse file info result json error ", err2) 67 | return nil, apierror.NewFailedApiError(err2.Error()) 68 | } 69 | return r, nil 70 | } 71 | 72 | func (p *WebPanClient) MkdirByFullPath(driveId, fullPath string) (*aliyunpan.MkdirResult, *apierror.ApiError) { 73 | fullPath = strings.ReplaceAll(fullPath, "//", "/") 74 | pathSlice := strings.Split(fullPath, "/") 75 | return p.MkdirRecursive(driveId, "", "", 0, pathSlice) 76 | } 77 | 78 | func (p *WebPanClient) MkdirRecursive(driveId, parentFileId string, fullPath string, index int, pathSlice []string) (*aliyunpan.MkdirResult, *apierror.ApiError) { 79 | r := &aliyunpan.MkdirResult{} 80 | if parentFileId == "" { 81 | // default root "/" entity 82 | parentFileId = aliyunpan.NewFileEntityForRootDir().FileId 83 | if index == 0 && len(pathSlice) == 1 { 84 | // root path "/" 85 | r.FileId = parentFileId 86 | return r, nil 87 | } 88 | 89 | fullPath = "" 90 | return p.MkdirRecursive(driveId, parentFileId, fullPath, index+1, pathSlice) 91 | } 92 | 93 | if index >= len(pathSlice) { 94 | r.FileId = parentFileId 95 | return r, nil 96 | } 97 | 98 | listFilePath := &aliyunpan.FileListParam{} 99 | listFilePath.DriveId = driveId 100 | listFilePath.ParentFileId = parentFileId 101 | fileResult, err := p.FileListGetAll(listFilePath, 0) 102 | if err != nil { 103 | r.FileId = "" 104 | return r, err 105 | } 106 | 107 | // existed? 108 | for _, fileEntity := range fileResult { 109 | if fileEntity.FileName == pathSlice[index] { 110 | return p.MkdirRecursive(driveId, fileEntity.FileId, fullPath+"/"+pathSlice[index], index+1, pathSlice) 111 | } 112 | } 113 | 114 | // not existed, mkdir dir 115 | name := pathSlice[index] 116 | if !apiutil.CheckFileNameValid(name) { 117 | r.FileId = "" 118 | return r, apierror.NewFailedApiError("文件夹名不能包含特殊字符:" + apiutil.FileNameSpecialChars) 119 | } 120 | 121 | rs, err := p.Mkdir(driveId, parentFileId, name) 122 | if err != nil { 123 | r.FileId = "" 124 | return r, err 125 | } 126 | 127 | if (index + 1) >= len(pathSlice) { 128 | return rs, nil 129 | } else { 130 | return p.MkdirRecursive(driveId, rs.FileId, fullPath+"/"+pathSlice[index], index+1, pathSlice) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /aliyunpan_web/signature.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 8 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 9 | "github.com/tickstep/library-go/crypto/secp256k1" 10 | "github.com/tickstep/library-go/logger" 11 | "math/rand" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type ( 17 | CreateSessionParam struct { 18 | DeviceName string `json:"deviceName"` 19 | ModelName string `json:"modelName"` 20 | PubKey string `json:"pubKey"` 21 | } 22 | 23 | CreateSessionResult struct { 24 | Result bool `json:"result"` 25 | Success bool `json:"success"` 26 | Code string `json:"code"` 27 | Message string `json:"message"` 28 | } 29 | ) 30 | 31 | const ( 32 | NONCE_MIN = int32(0) 33 | NONCE_MAX = int32(2147483647) 34 | ) 35 | 36 | func randomString(l int) []byte { 37 | bytes := make([]byte, l) 38 | for i := 0; i < l; i++ { 39 | rand.NewSource(time.Now().UnixNano()) 40 | bytes[i] = byte(randInt(1, 2^256-1)) 41 | } 42 | return bytes 43 | } 44 | 45 | func randInt(min int, max int) int { 46 | return min + rand.Intn(max-min) 47 | } 48 | 49 | func getNextNonce(nonce int32) int32 { 50 | next := nonce + 1 51 | if next > NONCE_MAX { 52 | return NONCE_MIN 53 | } else { 54 | return next 55 | } 56 | } 57 | 58 | // CalcSignature 生成新的密钥并计算接口签名 59 | func (p *WebPanClient) calcSignature() error { 60 | max := 32 61 | key := randomString(max) 62 | p.appConfig.Nonce = 0 63 | data := fmt.Sprintf("%s:%s:%s:%d", p.appConfig.AppId, p.appConfig.DeviceId, p.appConfig.UserId, p.appConfig.Nonce) 64 | var privKey = secp256k1.PrivKey(key) 65 | p.appConfig.PrivKey = &privKey 66 | pubKey := privKey.PubKey() 67 | p.appConfig.PubKey = &pubKey 68 | p.appConfig.PublicKey = "04" + hex.EncodeToString(pubKey.Bytes()) 69 | signature, err := privKey.Sign([]byte(data)) 70 | if err != nil { 71 | return err 72 | } 73 | p.appConfig.SignatureData = hex.EncodeToString(signature) + "01" 74 | return nil 75 | } 76 | 77 | // CalcNextSignature 使用已有的密钥并生成新的签名 78 | //func (p *WebPanClient) CalcNextSignature() error { 79 | // p.appConfig.Nonce = getNextNonce(p.appConfig.Nonce) 80 | // data := fmt.Sprintf("%s:%s:%s:%d", p.appConfig.AppId, p.appConfig.DeviceId, p.appConfig.UserId, p.appConfig.Nonce) 81 | // signature, err := p.appConfig.PrivKey.Sign([]byte(data)) 82 | // if err != nil { 83 | // return err 84 | // } 85 | // p.appConfig.SignatureData = hex.EncodeToString(signature) + "01" 86 | // return nil 87 | //} 88 | 89 | // AddSignatureHeader 增加接口签名header 90 | func (p *WebPanClient) AddSignatureHeader(headers map[string]string) map[string]string { 91 | if headers == nil { 92 | return headers 93 | } 94 | 95 | // add signature 96 | //headers["x-canary"] = "client=web,app=adrive,version=v3.17.0" 97 | headers["x-device-id"] = p.appConfig.DeviceId 98 | headers["x-signature"] = p.appConfig.SignatureData 99 | return headers 100 | } 101 | 102 | // CreateSession 上传会话签名秘钥给服务器 103 | func (p *WebPanClient) CreateSession(param *CreateSessionParam) (*CreateSessionResult, *apierror.ApiError) { 104 | if param == nil { 105 | param = &CreateSessionParam{ 106 | DeviceName: p.sessionConfig.DeviceName, 107 | ModelName: p.sessionConfig.ModelName, 108 | } 109 | } 110 | 111 | // 计算密钥 112 | p.calcSignature() 113 | 114 | // header 115 | header := map[string]string{ 116 | "authorization": p.webToken.GetAuthorizationStr(), 117 | } 118 | 119 | // url 120 | fullUrl := &strings.Builder{} 121 | fmt.Fprintf(fullUrl, "%s/users/v1/users/device/create_session", API_URL) 122 | logger.Verboseln("do request url: " + fullUrl.String()) 123 | 124 | // data 125 | postData := map[string]interface{}{ 126 | "deviceName": param.DeviceName, 127 | "modelName": param.ModelName, 128 | "pubKey": p.appConfig.PublicKey, 129 | } 130 | 131 | // request 132 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 133 | if err != nil { 134 | logger.Verboseln("do create session error ", err) 135 | return nil, apierror.NewFailedApiError(err.Error()) 136 | } 137 | 138 | // handler common error 139 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 140 | return nil, err1 141 | } 142 | 143 | // parse result 144 | r := &CreateSessionResult{} 145 | if err2 := json.Unmarshal(body, r); err2 != nil { 146 | logger.Verboseln("parse create session result json error ", err2) 147 | return nil, apierror.NewFailedApiError(err2.Error()) 148 | } 149 | return r, nil 150 | } 151 | 152 | // RenewSession 刷新签名秘钥,如果刷新失败则需要调用CreateSession重新上传新秘钥 153 | //func (p *WebPanClient) RenewSession() (*CreateSessionResult, *apierror.ApiError) { 154 | // // header 155 | // header := map[string]string{ 156 | // "authorization": p.webToken.GetAuthorizationStr(), 157 | // } 158 | // 159 | // // url 160 | // fullUrl := &strings.Builder{} 161 | // fmt.Fprintf(fullUrl, "%s/users/v1/users/device/renew_session", API_URL) 162 | // logger.Verboseln("do request url: " + fullUrl.String()) 163 | // 164 | // // request 165 | // data := map[string]string{} 166 | // body, err := p.client.Fetch("POST", fullUrl.String(), data, p.AddSignatureHeader(apiutil.AddCommonHeader(header))) 167 | // if err != nil { 168 | // logger.Verboseln("do renew session error ", err) 169 | // return nil, apierror.NewFailedApiError(err.Error()) 170 | // } 171 | // 172 | // // handler common error 173 | // if err1 := apierror.ParseCommonApiError(body); err1 != nil { 174 | // return nil, err1 175 | // } 176 | // 177 | // // parse result 178 | // r := &CreateSessionResult{} 179 | // if err2 := json.Unmarshal(body, r); err2 != nil { 180 | // logger.Verboseln("parse renew session result json error ", err2) 181 | // return nil, apierror.NewFailedApiError(err2.Error()) 182 | // } 183 | // return r, nil 184 | //} 185 | -------------------------------------------------------------------------------- /aliyunpan_web/user_info.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "github.com/tickstep/aliyunpan-api/aliyunpan" 21 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 22 | "github.com/tickstep/aliyunpan-api/aliyunpan/apiutil" 23 | "github.com/tickstep/library-go/logger" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | type ( 29 | // userInfoResult 用户信息返回实体 30 | userInfoResult struct { 31 | DomainId string `json:"domain_id"` 32 | UserId string `json:"user_id"` 33 | Avatar string `json:"avatar"` 34 | CreatedAt int64 `json:"created_at"` 35 | UpdatedAt int64 `json:"updated_at"` 36 | Email string `json:"email"` 37 | NickName string `json:"nick_name"` 38 | Phone string `json:"phone"` 39 | Role string `json:"role"` 40 | Status string `json:"status"` 41 | UserName string `json:"user_name"` 42 | Description string `json:"description"` 43 | DefaultDriveId string `json:"default_drive_id"` // 默认是备份盘,以前叫文件盘 44 | BackupDriveId string `json:"backup_drive_id"` // 备份盘 45 | ResourceDriveId string `json:"resource_drive_id"` // 资源库 46 | } 47 | 48 | personalInfoResult struct { 49 | // 权限 50 | PersonalRightsInfo struct { 51 | SpuID string `json:"spu_id"` 52 | Name string `json:"name"` 53 | IsExpires bool `json:"is_expires"` 54 | Privileges []struct { 55 | FeatureID string `json:"feature_id"` 56 | FeatureAttrID string `json:"feature_attr_id"` 57 | Quota int64 `json:"quota"` 58 | } `json:"privileges"` 59 | } `json:"personal_rights_info"` 60 | 61 | // quota配额 62 | PersonalSpaceInfo struct { 63 | UsedSize uint64 `json:"used_size"` 64 | TotalSize uint64 `json:"total_size"` 65 | } `json:"personal_space_info"` 66 | } 67 | 68 | safeBoxInfoResult struct { 69 | DriveId string `json:"drive_id"` 70 | SboxUsedSize int64 `json:"sbox_used_size"` 71 | SboxTotalSize int64 `json:"sbox_total_size"` 72 | RecommendVip string `json:"recommend_vip"` 73 | PinSetup bool `json:"pin_setup"` 74 | Locked bool `json:"locked"` 75 | InsuranceEnabled bool `json:"insurance_enabled"` 76 | } 77 | 78 | albumInfoResult struct { 79 | Code string `json:"code"` 80 | Message string `json:"message"` 81 | Data struct { 82 | DriveId string `json:"driveId"` 83 | DriveName string `json:"driveName"` 84 | } `json:"data"` 85 | ResultCode string `json:"resultCode"` 86 | } 87 | 88 | vipInfoResult struct { 89 | // Identity 身份标记,member-普通用户,vip-会员用户 90 | Identity string `json:"identity"` 91 | // Icon 图标URL路径 92 | Icon string `json:"icon"` 93 | VipList []struct { 94 | // Name 名称,例如:会员 95 | Name string `json:"name"` 96 | // Code 代码,例如:vip 97 | Code string `json:"code"` 98 | // PromotedAt 生效时间 99 | PromotedAt int `json:"promotedAt"` 100 | // Expire 过期时间 101 | Expire int `json:"expire"` 102 | } `json:"vipList"` 103 | } 104 | ) 105 | 106 | const () 107 | 108 | func parseUserRole(role string) aliyunpan.UserRole { 109 | switch role { 110 | case "user": 111 | return aliyunpan.User 112 | } 113 | return aliyunpan.UnknownRole 114 | } 115 | 116 | func parseUserStatus(status string) aliyunpan.UserStatus { 117 | switch status { 118 | case "enabled": 119 | return aliyunpan.Enabled 120 | } 121 | return aliyunpan.UnknownStatus 122 | } 123 | 124 | // GetUserInfo 获取用户信息 125 | func (p *WebPanClient) GetUserInfo() (*aliyunpan.UserInfo, *apierror.ApiError) { 126 | userInfo := &aliyunpan.UserInfo{} 127 | 128 | if r, err := p.getUserInfoReq(); err == nil { 129 | userInfo.DomainId = r.DomainId 130 | userInfo.FileDriveId = r.DefaultDriveId 131 | userInfo.ResourceDriveId = r.ResourceDriveId 132 | userInfo.UserId = r.UserId 133 | userInfo.UserName = r.UserName 134 | userInfo.CreatedAt = time.Unix(r.CreatedAt/1000, 0).Format("2006-01-02 15:04:05") 135 | userInfo.Email = r.Email 136 | userInfo.Phone = r.Email 137 | userInfo.Role = parseUserRole(r.Role) 138 | userInfo.Status = parseUserStatus(r.Status) 139 | userInfo.Nickname = r.NickName 140 | } else { 141 | return nil, err 142 | } 143 | 144 | if r, err := p.getPersonalInfoReq(); err == nil { 145 | userInfo.TotalSize = r.PersonalSpaceInfo.TotalSize 146 | userInfo.UsedSize = r.PersonalSpaceInfo.UsedSize 147 | } else { 148 | return nil, err 149 | } 150 | 151 | if r, err := p.getSafeBoxInfoReq(); err == nil { 152 | userInfo.SafeBoxDriveId = r.DriveId 153 | } else { 154 | return nil, err 155 | } 156 | 157 | if r, err := p.getAlbumInfoReq(); err == nil { 158 | userInfo.AlbumDriveId = r.Data.DriveId 159 | } else { 160 | return nil, err 161 | } 162 | 163 | return userInfo, nil 164 | } 165 | 166 | // getUserInfoReq 获取用户基本信息 167 | func (p *WebPanClient) getUserInfoReq() (*userInfoResult, *apierror.ApiError) { 168 | header := map[string]string{ 169 | "authorization": p.webToken.GetAuthorizationStr(), 170 | } 171 | 172 | fullUrl := &strings.Builder{} 173 | fmt.Fprintf(fullUrl, "%s/v2/user/get", USER_URL) 174 | logger.Verboseln("do request url: " + fullUrl.String()) 175 | postData := map[string]string{} 176 | 177 | // request 178 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, apiutil.AddCommonHeader(header)) 179 | if err != nil { 180 | logger.Verboseln("get user info error ", err) 181 | return nil, apierror.NewFailedApiError(err.Error()) 182 | } 183 | 184 | // handler common error 185 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 186 | return nil, err1 187 | } 188 | 189 | // parse result 190 | r := &userInfoResult{} 191 | if err2 := json.Unmarshal(body, r); err2 != nil { 192 | logger.Verboseln("parse user info result json error ", err2) 193 | return nil, apierror.NewFailedApiError(err2.Error()) 194 | } 195 | return r, nil 196 | } 197 | 198 | // getPersonalInfoReq 获取用户网盘基本信息,包括配额,上传下载等权限限制 199 | func (p *WebPanClient) getPersonalInfoReq() (*personalInfoResult, *apierror.ApiError) { 200 | header := map[string]string{ 201 | "authorization": p.webToken.GetAuthorizationStr(), 202 | } 203 | 204 | fullUrl := &strings.Builder{} 205 | fmt.Fprintf(fullUrl, "%s/v2/databox/get_personal_info", API_URL) 206 | logger.Verboseln("do request url: " + fullUrl.String()) 207 | postData := map[string]string{} 208 | 209 | // request 210 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, apiutil.AddCommonHeader(header)) 211 | if err != nil { 212 | logger.Verboseln("get person info error ", err) 213 | return nil, apierror.NewFailedApiError(err.Error()) 214 | } 215 | 216 | // handler common error 217 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 218 | return nil, err1 219 | } 220 | 221 | // parse result 222 | r := &personalInfoResult{} 223 | if err2 := json.Unmarshal(body, r); err2 != nil { 224 | logger.Verboseln("parse person info result json error ", err2) 225 | return nil, apierror.NewFailedApiError(err2.Error()) 226 | } 227 | return r, nil 228 | } 229 | 230 | // getSafeBoxInfoReq 获取保险箱信息 231 | func (p *WebPanClient) getSafeBoxInfoReq() (*safeBoxInfoResult, *apierror.ApiError) { 232 | header := map[string]string{ 233 | "authorization": p.webToken.GetAuthorizationStr(), 234 | } 235 | 236 | fullUrl := &strings.Builder{} 237 | fmt.Fprintf(fullUrl, "%s/v2/sbox/get", API_URL) 238 | logger.Verboseln("do request url: " + fullUrl.String()) 239 | postData := map[string]string{} 240 | 241 | // request 242 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, apiutil.AddCommonHeader(header)) 243 | if err != nil { 244 | logger.Verboseln("get safe box info error ", err) 245 | return nil, apierror.NewFailedApiError(err.Error()) 246 | } 247 | 248 | // handler common error 249 | if err1 := apierror.ParseCommonApiError(body); err1 != nil { 250 | return nil, err1 251 | } 252 | 253 | // parse result 254 | r := &safeBoxInfoResult{} 255 | if err2 := json.Unmarshal(body, r); err2 != nil { 256 | logger.Verboseln("parse safe box info result json error ", err2) 257 | return nil, apierror.NewFailedApiError(err2.Error()) 258 | } 259 | return r, nil 260 | } 261 | 262 | func (p *WebPanClient) getAlbumInfoReq() (*albumInfoResult, *apierror.ApiError) { 263 | header := map[string]string{ 264 | "authorization": p.webToken.GetAuthorizationStr(), 265 | } 266 | 267 | fullUrl := &strings.Builder{} 268 | fmt.Fprintf(fullUrl, "%s/adrive/v1/user/albums_info", API_URL) 269 | logger.Verboseln("do request url: " + fullUrl.String()) 270 | postData := map[string]string{} 271 | 272 | // request 273 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, apiutil.AddCommonHeader(header)) 274 | if err != nil { 275 | logger.Verboseln("get album info error ", err) 276 | return nil, apierror.NewFailedApiError(err.Error()) 277 | } 278 | 279 | // parse result 280 | r := &albumInfoResult{} 281 | if err2 := json.Unmarshal(body, r); err2 != nil { 282 | logger.Verboseln("parse album info result json error ", err2) 283 | return nil, apierror.NewFailedApiError(err2.Error()) 284 | } 285 | return r, nil 286 | } 287 | 288 | func (p *WebPanClient) getVipInfoReq() (*vipInfoResult, *apierror.ApiError) { 289 | header := map[string]string{ 290 | "authorization": p.webToken.GetAuthorizationStr(), 291 | } 292 | 293 | fullUrl := &strings.Builder{} 294 | fmt.Fprintf(fullUrl, "%s/business/v1.0/users/vip/info", API_URL) 295 | logger.Verboseln("do request url: " + fullUrl.String()) 296 | postData := map[string]string{} 297 | 298 | // request 299 | body, err := p.client.Fetch("POST", fullUrl.String(), postData, apiutil.AddCommonHeader(header)) 300 | if err != nil { 301 | logger.Verboseln("get vip info error ", err) 302 | return nil, apierror.NewFailedApiError(err.Error()) 303 | } 304 | 305 | // parse result 306 | r := &vipInfoResult{} 307 | if err2 := json.Unmarshal(body, r); err2 != nil { 308 | logger.Verboseln("parse vip info result json error ", err2) 309 | return nil, apierror.NewFailedApiError(err2.Error()) 310 | } 311 | return r, nil 312 | } 313 | -------------------------------------------------------------------------------- /aliyunpan_web/util.go: -------------------------------------------------------------------------------- 1 | package aliyunpan_web 2 | 3 | import ( 4 | "github.com/tickstep/aliyunpan-api/aliyunpan" 5 | "github.com/tickstep/aliyunpan-api/aliyunpan/apierror" 6 | "github.com/tickstep/library-go/escaper" 7 | "github.com/tickstep/library-go/logger" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | const () 13 | 14 | func (p *WebPanClient) recurseMatchPathByShellPattern(driveId string, index int, pathSlice *[]string, parentFileInfo *aliyunpan.FileEntity, resultList *aliyunpan.FileList) { 15 | if parentFileInfo == nil { 16 | // default root "/" entity 17 | parentFileInfo = aliyunpan.NewFileEntityForRootDir() 18 | if index == 0 && len(*pathSlice) == 1 { 19 | // root path "/" 20 | *resultList = append(*resultList, parentFileInfo) 21 | return 22 | } 23 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, parentFileInfo, resultList) 24 | return 25 | } 26 | 27 | if index >= len(*pathSlice) { 28 | // 已经是最后的路径分片了,是命中的结果 29 | *resultList = append(*resultList, parentFileInfo) 30 | return 31 | } 32 | 33 | if !strings.ContainsAny((*pathSlice)[index], aliyunpan.ShellPatternCharacters) { 34 | // 不包含通配符,先查缓存 35 | curPathStr := path.Clean(parentFileInfo.Path + "/" + (*pathSlice)[index]) 36 | 37 | // try cache 38 | if v := p.loadFilePathFromCache(driveId, curPathStr); v != nil { 39 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, v, resultList) 40 | return 41 | } 42 | } 43 | 44 | // 遍历目录下所有文件 45 | if parentFileInfo.IsFile() { 46 | return 47 | } 48 | fileListParam := &aliyunpan.FileListParam{ 49 | DriveId: driveId, 50 | ParentFileId: parentFileInfo.FileId, 51 | } 52 | fileResult, err := p.FileListGetAll(fileListParam, 0) 53 | if err != nil { 54 | logger.Verbosef("获取目录文件列表错误") 55 | return 56 | } 57 | if fileResult == nil || len(fileResult) == 0 { 58 | // 文件目录下文件为空 59 | return 60 | } 61 | 62 | curParentPathStr := parentFileInfo.Path 63 | if curParentPathStr == "/" { 64 | curParentPathStr = "" 65 | } 66 | 67 | // 先检测是否满足文件名全量匹配 68 | for _, fileEntity := range fileResult { 69 | // cache item 70 | fileEntity.Path = curParentPathStr + "/" + fileEntity.FileName 71 | p.storeFilePathToCache(driveId, fileEntity.Path, fileEntity) 72 | 73 | // 阿里云盘文件名支持*?[]等特殊符号,先排除文件名完全一致匹配的情况,这种情况下不能开启通配符匹配 74 | if fileEntity.FileName == (*pathSlice)[index] { 75 | // 匹配一个就直接返回 76 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, fileEntity, resultList) 77 | return 78 | } 79 | } 80 | 81 | // 使用通配符匹配 82 | for _, fileEntity := range fileResult { 83 | // cache item 84 | fileEntity.Path = curParentPathStr + "/" + fileEntity.FileName 85 | p.storeFilePathToCache(driveId, fileEntity.Path, fileEntity) 86 | 87 | // 使用通配符 88 | if matched, _ := path.Match((*pathSlice)[index], fileEntity.FileName); matched { 89 | p.recurseMatchPathByShellPattern(driveId, index+1, pathSlice, fileEntity, resultList) 90 | } 91 | } 92 | } 93 | 94 | // MatchPathByShellPattern 通配符匹配文件路径, pattern为绝对路径,符合的路径文件存放在resultList中 95 | func (p *WebPanClient) MatchPathByShellPattern(driveId string, pattern string) (resultList *aliyunpan.FileList, error *apierror.ApiError) { 96 | errInfo := apierror.NewApiError(apierror.ApiCodeFailed, "") 97 | resultList = &aliyunpan.FileList{} 98 | 99 | patternSlice := strings.Split(escaper.Escape(path.Clean(pattern), []rune{'['}), aliyunpan.PathSeparator) // 转义中括号 100 | if patternSlice[0] != "" { 101 | errInfo.Err = "路径不是绝对路径" 102 | return nil, errInfo 103 | } 104 | defer func() { // 捕获异常 105 | if err := recover(); err != nil { 106 | resultList = nil 107 | errInfo.Err = "查询路径异常" 108 | } 109 | }() 110 | 111 | parentFile := aliyunpan.NewFileEntityForRootDir() 112 | if path.Clean(strings.TrimSpace(pattern)) == "/" { 113 | *resultList = append(*resultList, parentFile) 114 | return resultList, nil 115 | } 116 | p.recurseMatchPathByShellPattern(driveId, 1, &patternSlice, parentFile, resultList) 117 | return resultList, nil 118 | } 119 | -------------------------------------------------------------------------------- /aliyunpan_web/web_pan_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 tickstep. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package aliyunpan_web 16 | 17 | import ( 18 | "github.com/tickstep/aliyunpan-api/aliyunpan" 19 | "github.com/tickstep/library-go/crypto" 20 | "github.com/tickstep/library-go/crypto/secp256k1" 21 | "github.com/tickstep/library-go/logger" 22 | "github.com/tickstep/library-go/requester" 23 | "strings" 24 | "sync" 25 | "time" 26 | ) 27 | 28 | const () 29 | 30 | type ( 31 | // AppConfig 存储客户端相关配置参数,目前主要是签名需要用的参数 32 | AppConfig struct { 33 | AppId string `json:"appId"` 34 | // DeviceId标识登录客户端,阿里限制:为了保障你的数据隐私安全,阿里云盘最多只允许你同时登录 10 台设备。你已超出最大设备数量,请先选择一台设备下线,才可以继续使用 35 | DeviceId string `json:"deviceId"` 36 | UserId string `json:"userId"` 37 | Nonce int32 `json:"nonce"` 38 | PublicKey string `json:"publicKey"` 39 | SignatureData string `json:"signatureData"` 40 | 41 | PrivKey *secp256k1.PrivKey `json:"-"` 42 | PubKey *crypto.PubKey `json:"-"` 43 | } 44 | SessionConfig struct { 45 | DeviceName string `json:"deviceName"` 46 | ModelName string `json:"modelName"` 47 | } 48 | 49 | WebPanClient struct { 50 | client *requester.HTTPClient // http 客户端 51 | webToken WebLoginToken 52 | appToken AppLoginToken 53 | appConfig AppConfig 54 | sessionConfig SessionConfig 55 | 56 | cacheMutex *sync.Mutex 57 | useCache bool 58 | // 网盘文件绝对路径到网盘文件信息实体映射缓存,避免FileInfoByPath频繁访问服务器触发风控 59 | filePathCacheMap sync.Map 60 | } 61 | ) 62 | 63 | // NewWebPanClient 创建WebPanClient 64 | func NewWebPanClient(webToken WebLoginToken, appToken AppLoginToken, appConfig AppConfig, sessionConfig SessionConfig) *WebPanClient { 65 | myclient := requester.NewHTTPClient() 66 | 67 | return &WebPanClient{ 68 | client: myclient, 69 | webToken: webToken, 70 | appToken: appToken, 71 | appConfig: appConfig, 72 | sessionConfig: sessionConfig, 73 | cacheMutex: &sync.Mutex{}, 74 | useCache: false, 75 | filePathCacheMap: sync.Map{}, 76 | } 77 | } 78 | 79 | func (p *WebPanClient) UpdateToken(webToken WebLoginToken) { 80 | p.webToken = webToken 81 | } 82 | 83 | func (p *WebPanClient) UpdateAppConfig(appConfig AppConfig) { 84 | p.appConfig = appConfig 85 | } 86 | 87 | func (p *WebPanClient) UpdateSessionConfig(sessionConfig SessionConfig) { 88 | p.sessionConfig = sessionConfig 89 | } 90 | 91 | func (p *WebPanClient) GetAccessToken() string { 92 | return p.webToken.AccessToken 93 | } 94 | 95 | // EnableCache 启用缓存 96 | func (p *WebPanClient) EnableCache() { 97 | p.cacheMutex.Lock() 98 | p.cacheMutex.Unlock() 99 | p.useCache = true 100 | } 101 | 102 | // ClearCache 清除已经缓存的数据 103 | func (p *WebPanClient) ClearCache() { 104 | p.cacheMutex.Lock() 105 | p.cacheMutex.Unlock() 106 | p.filePathCacheMap = sync.Map{} 107 | } 108 | 109 | // DisableCache 禁用缓存 110 | func (p *WebPanClient) DisableCache() { 111 | p.cacheMutex.Lock() 112 | p.cacheMutex.Unlock() 113 | p.useCache = false 114 | } 115 | 116 | func (p *WebPanClient) storeFilePathToCache(driveId, pathStr string, fileEntity *aliyunpan.FileEntity) { 117 | p.cacheMutex.Lock() 118 | p.cacheMutex.Unlock() 119 | if !p.useCache { 120 | return 121 | } 122 | pathStr = formatPathStyle(pathStr) 123 | cache, _ := p.filePathCacheMap.LoadOrStore(driveId, &sync.Map{}) 124 | cache.(*sync.Map).Store(pathStr, fileEntity) 125 | } 126 | 127 | func (p *WebPanClient) loadFilePathFromCache(driveId, pathStr string) *aliyunpan.FileEntity { 128 | p.cacheMutex.Lock() 129 | p.cacheMutex.Unlock() 130 | if !p.useCache { 131 | return nil 132 | } 133 | pathStr = formatPathStyle(pathStr) 134 | cache, _ := p.filePathCacheMap.LoadOrStore(driveId, &sync.Map{}) 135 | s := cache.(*sync.Map) 136 | if v, ok := s.Load(pathStr); ok { 137 | logger.Verboseln("file path cache hit: ", pathStr) 138 | return v.(*aliyunpan.FileEntity) 139 | } 140 | return nil 141 | } 142 | 143 | // SetTimeout 设置 http 请求超时时间 144 | func (p *WebPanClient) SetTimeout(t time.Duration) { 145 | if p.client != nil { 146 | p.client.Timeout = t 147 | } 148 | } 149 | 150 | // UpdateUserId 更新用户ID 151 | func (p *WebPanClient) UpdateUserId(userId string) { 152 | p.appConfig.UserId = userId 153 | } 154 | 155 | func formatPathStyle(pathStr string) string { 156 | pathStr = strings.ReplaceAll(pathStr, "\\", "/") 157 | if pathStr != "/" { 158 | pathStr = strings.TrimSuffix(pathStr, "/") 159 | } 160 | return pathStr 161 | } 162 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tickstep/aliyunpan-api 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/json-iterator/go v1.1.10 7 | github.com/satori/go.uuid v1.2.0 8 | github.com/stretchr/testify v1.6.1 9 | github.com/tickstep/library-go v0.1.3 10 | ) 11 | 12 | //replace github.com/tickstep/library-go => /Users/tickstep/Documents/Workspace/go/projects/library-go 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 2 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 3 | github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= 4 | github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= 5 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 6 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 7 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 8 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 9 | github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= 10 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 11 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 12 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 13 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 14 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 15 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 16 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 17 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 22 | github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= 23 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 24 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 26 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 27 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 28 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 29 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 30 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 31 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 32 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 33 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 34 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 35 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 36 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 37 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 38 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 39 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 40 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 41 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 42 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 46 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 49 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 50 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 51 | github.com/tickstep/library-go v0.1.0 h1:lzz6wpYCeliSzmVISa7rp9bGB4r+uIa9/VUTtk87DFU= 52 | github.com/tickstep/library-go v0.1.0/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA= 53 | github.com/tickstep/library-go v0.1.2 h1:iewbEZBl5+aWeL9zcjVHVC4BXET4+9gwc52fuDC8MXQ= 54 | github.com/tickstep/library-go v0.1.2/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA= 55 | github.com/tickstep/library-go v0.1.3 h1:OUj6nkimTsqhHXh5s+PbmHq5hR0m739Y1tu9QpfA2ng= 56 | github.com/tickstep/library-go v0.1.3/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA= 57 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 58 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 59 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 60 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 61 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 62 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 63 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 64 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 65 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 66 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 74 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 75 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 77 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 78 | --------------------------------------------------------------------------------