一、阿里云盘
官方文档
接入流程 · 语雀流程概述服务端 API 调用流程如下图所示1. 创建账...https://www.yuque.com/aliyundrive/zpfszx/btw0tw
1. 接入授权
1.1. App Key、App Secret和用户授权验证
在通过网盘开发者认证之后,创建个人应用会生成APP ID(对应App Key)和App Secret,后续在组装生成用户授权验证地址时会使用到,同时也需要配置好Scopes(授权范围)以及回调URI(用于授权后跳转到个人应用等),参数细节详见官方文档。
// 生成授权二维码链接
// 此处只填写了必须的参数
func GenerateAuthUrl() string {
params := url.Values{}
params.Add("response_type", "code")
params.Add("client_id", appKey)
params.Add("redirect_uri", redirectUri)
// params.Add("state", state) // 可通过填入随机数来防止CSRF攻击
params.Add("scope", scope)
authUrlWithParams := authUrl + "?" + params.Encode()
return authUrlWithParams
}
1.2. 授权验证方式壹——无后端服务授权模式
阿里云盘通过OAuth2.0 + PKCE的方式,通过提供code_verifier(常通过个人应用进行运算得来)来进行一种无需App Secret的授权验证模式。流程图(Sequence Diagram)
通过code_verifier做参数请求阿里云盘,交换得到的Access Token,默认有效期为30天。
(此次并未作代码实践)
1.3. 授权验证方式贰——扫码授权验证
流程图如下:
实践中,最初通过"github.com/skip2/go-qrcode",把组装的授权验证链接转换为二维码,可以正常工作。
// 生成二维码图片并保存
func SaveQrCode(authUrl string) {
qrCode, _ := qrcode.New(authUrl, qrcode.Medium)
err := qrCode.WriteFile(256, "auth_qr_code.png")
if err != nil {
log.Fatal(err)
}
fmt.Println("QR code saved as auth_qr_code.png")
}
后续又使用API获取授权二维码,同样是可以工作的,有效时长默认3分钟。
// 获取授权二维码的链接和用于校验状态的sid
func GetQrCodeFromAli() (string, string, error) {
reqBody := map[string]interface{}{
"client_id": appKey,
"client_secret": appSecret,
"scopes": scopes,
}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", apiBase+"/oauth/authorize/qrcode", bytes.NewBuffer(jsonData))
if err != nil {
return "", "", err
}
req.Header.Set("Content-Type", "application/json")
defer req.Body.Close()
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var qrCodeResp QrCodeResponse
_ = json.Unmarshal(body, &qrCodeResp)
return qrCodeResp.QrCodeUrl, qrCodeResp.Sid, nil
}
// 获取二维码图片
func GetQrCodePic(sid string) {
// 构建请求 URL
url := fmt.Sprintf("%s/oauth/qrcode/%s", apiBase, sid)
// 创建请求
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatalf("Failed to create request: %v", err)
}
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Failed to send request: %v", err)
}
defer resp.Body.Close()
// 创建本地文件
filePath := "qrcode.jpeg"
file, err := os.Create(filePath)
if err != nil {
log.Fatalf("Failed to create file: %v", err)
}
defer file.Close()
// 写入文件
_, err = io.Copy(file, resp.Body)
if err != nil {
log.Fatalf("Failed to write to file: %v", err)
}
log.Printf("QR code image saved to %s", filePath)
}
1.4. 用code_verifier或code(authCode)交换获取Access Token
上述两种不同的方式在交换获取Access Token的方法是有差异的,无后端模式扫码后会提供code_verifier,而扫码模式将会提供code(或authCode),从官方文档中可见,在传参的时候有一些差异(下方代码是传code)。
// 交换Access Token
func ExchangeToken(code string) (*TokenResponse, error) {
params := url.Values{}
params.Add("grant_type", "authorization_code")
params.Add("code", code)
params.Add("client_id", appKey)
params.Add("client_secret", appSecret)
params.Add("redirect_uri", redirectUri)
resp, err := http.PostForm(tokenUrl, params)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var tokenResp TokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return nil, err
}
if tokenResp.Code != "" {
return nil, fmt.Errorf("API error: %s", tokenResp.Message)
}
return &tokenResp, nil
}
拿到Access Token之后就可以个人应用就可以调用API来与阿里云盘交互了。
2. 上传文件
想要通过个人应用(通过授权校验后)上传文件到阿里云盘,主要有三个流程:
- 创建文件
- 文件上传
- 上传完毕
不过,一系列的文件操作都需要提供唯一标识用户云盘的drive_id,可以通过一个POST请求来获取。
2.1. 获取drive_id
在官方文档中,没有在这一部分再次说明需要设置Header,不过想要获得drive_id,是通过access_token获取用户信息,要把access_token放到head里。
// 获取用户信息
func GetUserDriveInfo(accessToken string) (*UserDriveInfoResponse, error) {
req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/user/getDriveInfo", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken) // 设置Header
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var userDriveInfoResp UserDriveInfoResponse
json.Unmarshal(body, &userDriveInfoResp)
return &userDriveInfoResp, nil
}
有了access_token和drive_id之后,可以开始上传文件的流程了。
2.2. 创建文件
在正式上传文件之前,会在阿里云盘的服务器上创建一个占位符或元数据记录,不是实际的文件内容,而是文件的元数据,包括文件名、大小、类型等信息。最后返回一个唯一的文件标识符(file_id或upload_id)在后续的上传过程中使用。
发送请求时的必要参数有:
// 创建文件
func createFile(accessToken string, driveId string, parentFileID string, fileName string, fileSize int64, checkNameMode string) (*CreateFileResponse, error) {
reqBody := map[string]interface{}{
"drive_id": driveId,
"parent_file_id": parentFileID,
"name": fileName,
"type": "file",
"check_name_mode": checkNameMode,
"size": fileSize, // 如果后续是秒传,这个参数是必须的,此处不是秒传
"local_created_at": time.Now().Unix(), // 选填参数
}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/create", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var createFileResp CreateFileResponse
json.Unmarshal(body, &createFileResp)
return &createFileResp, nil
}
返回参数上,创建好文件之后,会返回part_info_list[*].upload_url、file_id、upload_id,后续的上传会通过这个链接进行。
2.3. 刷新获取上传链接
除了创建文件之后会返回上传链接,也可以根据创建文件后返回的upload_id来获取上传链接。
// 获取上传链接
func getUploadUrl(accessToken string, driveId string, fileId, uploadId string) ([]struct {
PartNumber int `json:"part_number"`
UploadUrl string `json:"upload_url"`
}, error) {
reqBody := map[string]interface{}{ // 都是必填参数
"drive_id": driveId,
"file_id": fileId,
"upload_id": uploadId,
"part_info_list": []map[string]int{
{"part_number": 1},
},
}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/getUploadUrl", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var createFileResp struct {
PartInfoList []struct {
PartNumber int `json:"part_number"`
UploadUrl string `json:"upload_url"`
} `json:"part_info_list"`
}
json.Unmarshal(body, &createFileResp)
return createFileResp.PartInfoList, nil
}
2.4. 上传文件
主流的上传方式是分片上传,也支持单步上传和文件秒传。
2.4.1. 单片上传
在阿里云盘中,单文件上传最大支持5GB,通过一个向上传链接发送PUT请求。
// 上传文件
func upload(accessToken string, driveId string, dstDir string, filePath string) error {
// 1. create
fileSize, _ := GetFileSize(filePath)
// 创建文件请求的响应结果
createFileResp, _ := createFile(accessToken, driveId, dstDir, filePath, fileSize, "auto_rename")
// 2. upload
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
uploadUrl, _ := getUploadUrl(accessToken, driveId, createFileResp.FileId, createFileResp.UploadId)
req, err := http.NewRequest("PUT", uploadUrl[0].UploadUrl, file)
req.Header.Set("Authorization", "Bearer "+accessToken)
if err != nil {
return err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 3. complete
return completeUpload(accessToken, driveId, createFileResp.FileId, createFileResp.UploadId)
}
2.4.2. 分片上传
与单步上传相比,分片上传基本就是把一个大文件分为了多个部分,进行了多次单片上传。不过,在参数上有不同。分片上传需要在创建文件后刷新获取上传链接,而在刷新获取上传链接时,part_info_list不再只分为一片。此外,如何分片,也是必不可少的。
// 刷新获取上传链接时,提供的参数有所不同
reqBody := map[string]interface{}{ // 都是必填参数
"drive_id": driveId,
"file_id": fileId,
"upload_id": uploadId,
"part_info_list": []map[string]int{
{"part_number": 1},
{"part_number": 2},
{"part_number": 3},
},
}
在返回响应时,会返回多个用于上传的链接(分为3片的文件会分别通过三个链接上传),官方的示例如下:
2.4.3. 文件秒传
文件秒传,通过把用户的上传的进行HASH匹配,如果存在文件HASH值,就不需要上传而是直接从服务器复制过去。上文提到,如果需要使用秒传能力,在创建时带上秒传所需参数。
如果文件比较大,计算content_hash比较耗时。 可以使用只计算文件前1k的sha1,放入pre_hash字段。如果前1k没有匹配,说明文件无法做秒传。如果匹配到再计算完整的sha1,进行秒传。
(未实践...)
2.5. 上传完毕
这个步骤的目的是:通知阿里云盘服务器所有文件块已上传完成,服务器可以将这些块合并成一个完整的文件。
// completeUpload 完成上传
func completeUpload(accessToken string, driveId string, fileId, uploadId string) error {
var newFile File
reqBody := map[string]interface{}{
"drive_id": driveId,
"file_id": fileId,
"upload_id": uploadId,
}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/complete", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &newFile)
return nil
}
二、百度网盘
官方文档
获取用户信息https://pan.baidu.com/union/doc/pksg0s9ns
1. 预准备
前置的创建应用、获取App Key、获取App Secret、以及交换获取Access Token不再赘述,大致步骤与阿里云盘相同,只是参数和用来请求的链接不同。下面主要关注不同之处:
1.1. 简化模式授权
此种模式与阿里云盘中无后端服务授权模式类似,不过不像阿里云盘需要通过个人应用运算生成code_verifier。
1.2. 设备码模式授权
流程图如下,通过向硬件设备请求设备码和用户码,作为参数来请求Access Token,使得一些不支持浏览器或输入受限的设备也可以接入。
2. 上传文件
百度网盘上传文件,同样也分为三个步骤:
- 预上传
- 文件上传(有不需要预上传的单步上传)
- 创建文件
与阿里云盘不同的是:百度网盘提供的Access Token已经可以唯一标识用户,所以也没有像阿里云盘那样去获取drive_id,上传文件的时候,常把Access Token用来作为参数。
此外:
2.1. 预上传
在预上传时,如果文件有进行分片,则需要把各个分片的MD5,填入一个字符串数组作为参数;与阿里云盘不同的是,第一个步骤必须填入size这个参数(阿里云盘只有在秒传的时候需要填写size)。
同样的,响应时会返回uploadid用来唯一标识本次传输任务。
2.2. 获取上传域名
在获取上传链接时,百度网盘会返回多个上传链接(为了并行执行多个分片的上传,也可能是负载均衡),使用其中任意一个作为上传链接即可。
// 获取上传 URL
func getUploadUrl(accessToken string, dstDir string, uploadId string) string {
req, err := http.NewRequest("GET",
"https://d.pcs.baidu.com/rest/2.0/pcs/file?"+
"method=locateupload&appid=250528&access_token="+accessToken+
"&path="+dstDir+"&uploadid="+uploadId+"&upload_version=2.0", nil)
if err != nil {
return ""
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "Exec req err"
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var uploadDomainResponse UploadDomainResponse
json.Unmarshal(body, &uploadDomainResponse)
return uploadDomainResponse.Servers[0].Server
}
下面是官方文档中的响应示例:
2.3. 文件上传
2.3.1. 单步上传
针对小于2GB的文件,百度网盘提供了无需预上传和创建文件流程的单步上传,请求参数如下:
// 单步上传
func uploadFile(accessToken, localPath, remotePath string) error {
url := "https://c.pcs.baidu.com/rest/2.0/pcs/file"
params := map[string]string{
"method": "upload",
"access_token": accessToken,
"path": remotePath,
"ondup": "newcopy",
}
file, err := os.Open(localPath)
if err != nil {
return err
}
defer file.Close()
resp, err := grequests.Post(url, &grequests.RequestOptions{
Params: params,
Files: []grequests.FileUpload{
{
FileContents: file,
},
},
})
if err != nil {
return err
}
if !resp.Ok {
return fmt.Errorf("upload failed with status code: %d", resp.StatusCode)
}
return nil
}
2.3.2. 分片上传
在请求参数上,百度网盘把参数大多都放在URL的位置,与阿里云盘大多放在Body请求体中不同。此处的分片上传,通过partseq来区分、上传分好的多个分片(也是并行的)。
2.4. 创建文件
创建文件和阿里云盘中完成上传所实现的功能类似,把多个分片,组成一个完整的文件。