• 个人应用接入使用阿里云盘和百度网盘


    一、阿里云盘

    官方文档
    接入流程 · 语雀流程概述服务端 API 调用流程如下图所示1. 创建账...icon-default.png?t=O83Ahttps://www.yuque.com/aliyundrive/zpfszx/btw0tw

    1. 接入授权
    1.1. App Key、App Secret和用户授权验证

    在通过网盘开发者认证之后,创建个人应用会生成APP ID(对应App Key)和App Secret,后续在组装生成用户授权验证地址时会使用到,同时也需要配置好Scopes(授权范围)以及回调URI(用于授权后跳转到个人应用等),参数细节详见官方文档。

    1. // 生成授权二维码链接
    2. // 此处只填写了必须的参数
    3. func GenerateAuthUrl() string {
    4. params := url.Values{}
    5. params.Add("response_type", "code")
    6. params.Add("client_id", appKey)
    7. params.Add("redirect_uri", redirectUri)
    8. // params.Add("state", state) // 可通过填入随机数来防止CSRF攻击
    9. params.Add("scope", scope)
    10. authUrlWithParams := authUrl + "?" + params.Encode()
    11. return authUrlWithParams
    12. }
    1.2. 授权验证方式壹——无后端服务授权模式

    阿里云盘通过OAuth2.0 + PKCE的方式,通过提供code_verifier(常通过个人应用进行运算得来)来进行一种无需App Secret的授权验证模式。流程图(Sequence Diagram)

    通过code_verifier做参数请求阿里云盘,交换得到的Access Token,默认有效期为30天。

    (此次并未作代码实践)

    1.3. 授权验证方式贰——扫码授权验证

    流程图如下:

    实践中,最初通过"github.com/skip2/go-qrcode",把组装的授权验证链接转换为二维码,可以正常工作。

    1. // 生成二维码图片并保存
    2. func SaveQrCode(authUrl string) {
    3. qrCode, _ := qrcode.New(authUrl, qrcode.Medium)
    4. err := qrCode.WriteFile(256, "auth_qr_code.png")
    5. if err != nil {
    6. log.Fatal(err)
    7. }
    8. fmt.Println("QR code saved as auth_qr_code.png")
    9. }

    后续又使用API获取授权二维码,同样是可以工作的,有效时长默认3分钟。

    1. // 获取授权二维码的链接和用于校验状态的sid
    2. func GetQrCodeFromAli() (string, string, error) {
    3. reqBody := map[string]interface{}{
    4. "client_id": appKey,
    5. "client_secret": appSecret,
    6. "scopes": scopes,
    7. }
    8. jsonData, _ := json.Marshal(reqBody)
    9. req, err := http.NewRequest("POST", apiBase+"/oauth/authorize/qrcode", bytes.NewBuffer(jsonData))
    10. if err != nil {
    11. return "", "", err
    12. }
    13. req.Header.Set("Content-Type", "application/json")
    14. defer req.Body.Close()
    15. client := &http.Client{}
    16. resp, err := client.Do(req)
    17. if err != nil {
    18. return "", "", err
    19. }
    20. defer resp.Body.Close()
    21. body, _ := io.ReadAll(resp.Body)
    22. var qrCodeResp QrCodeResponse
    23. _ = json.Unmarshal(body, &qrCodeResp)
    24. return qrCodeResp.QrCodeUrl, qrCodeResp.Sid, nil
    25. }
    26. // 获取二维码图片
    27. func GetQrCodePic(sid string) {
    28. // 构建请求 URL
    29. url := fmt.Sprintf("%s/oauth/qrcode/%s", apiBase, sid)
    30. // 创建请求
    31. req, err := http.NewRequest("GET", url, nil)
    32. if err != nil {
    33. log.Fatalf("Failed to create request: %v", err)
    34. }
    35. // 发送请求
    36. client := &http.Client{}
    37. resp, err := client.Do(req)
    38. if err != nil {
    39. log.Fatalf("Failed to send request: %v", err)
    40. }
    41. defer resp.Body.Close()
    42. // 创建本地文件
    43. filePath := "qrcode.jpeg"
    44. file, err := os.Create(filePath)
    45. if err != nil {
    46. log.Fatalf("Failed to create file: %v", err)
    47. }
    48. defer file.Close()
    49. // 写入文件
    50. _, err = io.Copy(file, resp.Body)
    51. if err != nil {
    52. log.Fatalf("Failed to write to file: %v", err)
    53. }
    54. log.Printf("QR code image saved to %s", filePath)
    55. }
    1.4. 用code_verifier或code(authCode)交换获取Access Token

    上述两种不同的方式在交换获取Access Token的方法是有差异的,无后端模式扫码后会提供code_verifier,而扫码模式将会提供code(或authCode),从官方文档中可见,在传参的时候有一些差异(下方代码是传code)。

    1. // 交换Access Token
    2. func ExchangeToken(code string) (*TokenResponse, error) {
    3. params := url.Values{}
    4. params.Add("grant_type", "authorization_code")
    5. params.Add("code", code)
    6. params.Add("client_id", appKey)
    7. params.Add("client_secret", appSecret)
    8. params.Add("redirect_uri", redirectUri)
    9. resp, err := http.PostForm(tokenUrl, params)
    10. if err != nil {
    11. return nil, err
    12. }
    13. defer resp.Body.Close()
    14. body, err := io.ReadAll(resp.Body)
    15. if err != nil {
    16. return nil, err
    17. }
    18. var tokenResp TokenResponse
    19. err = json.Unmarshal(body, &tokenResp)
    20. if err != nil {
    21. return nil, err
    22. }
    23. if tokenResp.Code != "" {
    24. return nil, fmt.Errorf("API error: %s", tokenResp.Message)
    25. }
    26. return &tokenResp, nil
    27. }

    拿到Access Token之后就可以个人应用就可以调用API来与阿里云盘交互了。

    2. 上传文件

    想要通过个人应用(通过授权校验后)上传文件到阿里云盘,主要有三个流程:

    1. 创建文件
    2. 文件上传
    3. 上传完毕

    不过,一系列的文件操作都需要提供唯一标识用户云盘的drive_id,可以通过一个POST请求来获取。

    2.1. 获取drive_id

    在官方文档中,没有在这一部分再次说明需要设置Header,不过想要获得drive_id,是通过access_token获取用户信息,要把access_token放到head里。

    1. // 获取用户信息
    2. func GetUserDriveInfo(accessToken string) (*UserDriveInfoResponse, error) {
    3. req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/user/getDriveInfo", nil)
    4. if err != nil {
    5. return nil, err
    6. }
    7. req.Header.Set("Authorization", "Bearer "+accessToken) // 设置Header
    8. client := &http.Client{}
    9. resp, err := client.Do(req)
    10. if err != nil {
    11. return nil, err
    12. }
    13. defer resp.Body.Close()
    14. body, _ := io.ReadAll(resp.Body)
    15. var userDriveInfoResp UserDriveInfoResponse
    16. json.Unmarshal(body, &userDriveInfoResp)
    17. return &userDriveInfoResp, nil
    18. }

    有了access_token和drive_id之后,可以开始上传文件的流程了。

    2.2. 创建文件

    在正式上传文件之前,会在阿里云盘的服务器上创建一个占位符或元数据记录,不是实际的文件内容,而是文件的元数据,包括文件名、大小、类型等信息。最后返回一个唯一的文件标识符(file_id或upload_id)在后续的上传过程中使用。

    发送请求时的必要参数有:

    1. // 创建文件
    2. func createFile(accessToken string, driveId string, parentFileID string, fileName string, fileSize int64, checkNameMode string) (*CreateFileResponse, error) {
    3. reqBody := map[string]interface{}{
    4. "drive_id": driveId,
    5. "parent_file_id": parentFileID,
    6. "name": fileName,
    7. "type": "file",
    8. "check_name_mode": checkNameMode,
    9. "size": fileSize, // 如果后续是秒传,这个参数是必须的,此处不是秒传
    10. "local_created_at": time.Now().Unix(), // 选填参数
    11. }
    12. jsonData, _ := json.Marshal(reqBody)
    13. req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/create", bytes.NewBuffer(jsonData))
    14. if err != nil {
    15. return nil, err
    16. }
    17. req.Header.Set("Authorization", "Bearer "+accessToken)
    18. req.Header.Set("Content-Type", "application/json")
    19. client := &http.Client{}
    20. resp, err := client.Do(req)
    21. if err != nil {
    22. return nil, err
    23. }
    24. defer resp.Body.Close()
    25. body, _ := io.ReadAll(resp.Body)
    26. var createFileResp CreateFileResponse
    27. json.Unmarshal(body, &createFileResp)
    28. return &createFileResp, nil
    29. }

    返回参数上,创建好文件之后,会返回part_info_list[*].upload_url、file_id、upload_id,后续的上传会通过这个链接进行。

    2.3. 刷新获取上传链接

    除了创建文件之后会返回上传链接,也可以根据创建文件后返回的upload_id来获取上传链接。

    1. // 获取上传链接
    2. func getUploadUrl(accessToken string, driveId string, fileId, uploadId string) ([]struct {
    3. PartNumber int `json:"part_number"`
    4. UploadUrl string `json:"upload_url"`
    5. }, error) {
    6. reqBody := map[string]interface{}{ // 都是必填参数
    7. "drive_id": driveId,
    8. "file_id": fileId,
    9. "upload_id": uploadId,
    10. "part_info_list": []map[string]int{
    11. {"part_number": 1},
    12. },
    13. }
    14. jsonData, _ := json.Marshal(reqBody)
    15. req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/getUploadUrl", bytes.NewBuffer(jsonData))
    16. if err != nil {
    17. return nil, err
    18. }
    19. req.Header.Set("Authorization", "Bearer "+accessToken)
    20. req.Header.Set("Content-Type", "application/json")
    21. client := &http.Client{}
    22. resp, err := client.Do(req)
    23. if err != nil {
    24. return nil, err
    25. }
    26. defer resp.Body.Close()
    27. body, _ := io.ReadAll(resp.Body)
    28. var createFileResp struct {
    29. PartInfoList []struct {
    30. PartNumber int `json:"part_number"`
    31. UploadUrl string `json:"upload_url"`
    32. } `json:"part_info_list"`
    33. }
    34. json.Unmarshal(body, &createFileResp)
    35. return createFileResp.PartInfoList, nil
    36. }
    2.4. 上传文件

    主流的上传方式是分片上传,也支持单步上传和文件秒传。

    2.4.1. 单片上传

    在阿里云盘中,单文件上传最大支持5GB,通过一个向上传链接发送PUT请求。

    1. // 上传文件
    2. func upload(accessToken string, driveId string, dstDir string, filePath string) error {
    3. // 1. create
    4. fileSize, _ := GetFileSize(filePath)
    5. // 创建文件请求的响应结果
    6. createFileResp, _ := createFile(accessToken, driveId, dstDir, filePath, fileSize, "auto_rename")
    7. // 2. upload
    8. file, err := os.Open(filePath)
    9. if err != nil {
    10. return err
    11. }
    12. defer file.Close()
    13. uploadUrl, _ := getUploadUrl(accessToken, driveId, createFileResp.FileId, createFileResp.UploadId)
    14. req, err := http.NewRequest("PUT", uploadUrl[0].UploadUrl, file)
    15. req.Header.Set("Authorization", "Bearer "+accessToken)
    16. if err != nil {
    17. return err
    18. }
    19. client := &http.Client{}
    20. resp, err := client.Do(req)
    21. if err != nil {
    22. return err
    23. }
    24. defer resp.Body.Close()
    25. // 3. complete
    26. return completeUpload(accessToken, driveId, createFileResp.FileId, createFileResp.UploadId)
    27. }
    2.4.2. 分片上传

    与单步上传相比,分片上传基本就是把一个大文件分为了多个部分,进行了多次单片上传。不过,在参数上有不同。分片上传需要在创建文件后刷新获取上传链接,而在刷新获取上传链接时,part_info_list不再只分为一片。此外,如何分片,也是必不可少的。

    1. // 刷新获取上传链接时,提供的参数有所不同
    2. reqBody := map[string]interface{}{ // 都是必填参数
    3. "drive_id": driveId,
    4. "file_id": fileId,
    5. "upload_id": uploadId,
    6. "part_info_list": []map[string]int{
    7. {"part_number": 1},
    8. {"part_number": 2},
    9. {"part_number": 3},
    10. },
    11. }

    在返回响应时,会返回多个用于上传的链接(分为3片的文件会分别通过三个链接上传),官方的示例如下:  

    2.4.3. 文件秒传

    文件秒传,通过把用户的上传的进行HASH匹配,如果存在文件HASH值,就不需要上传而是直接从服务器复制过去。上文提到,如果需要使用秒传能力,在创建时带上秒传所需参数。

    如果文件比较大,计算content_hash比较耗时。 可以使用只计算文件前1k的sha1,放入pre_hash字段。如果前1k没有匹配,说明文件无法做秒传。如果匹配到再计算完整的sha1,进行秒传。

    (未实践...)

    2.5. 上传完毕

    这个步骤的目的是:通知阿里云盘服务器所有文件块已上传完成,服务器可以将这些块合并成一个完整的文件。

    1. // completeUpload 完成上传
    2. func completeUpload(accessToken string, driveId string, fileId, uploadId string) error {
    3. var newFile File
    4. reqBody := map[string]interface{}{
    5. "drive_id": driveId,
    6. "file_id": fileId,
    7. "upload_id": uploadId,
    8. }
    9. jsonData, _ := json.Marshal(reqBody)
    10. req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/complete", bytes.NewBuffer(jsonData))
    11. if err != nil {
    12. return err
    13. }
    14. req.Header.Set("Authorization", "Bearer "+accessToken)
    15. req.Header.Set("Content-Type", "application/json")
    16. client := &http.Client{}
    17. resp, err := client.Do(req)
    18. if err != nil {
    19. return err
    20. }
    21. defer resp.Body.Close()
    22. body, _ := io.ReadAll(resp.Body)
    23. json.Unmarshal(body, &newFile)
    24. return nil
    25. }

    二、百度网盘

    官方文档

    获取用户信息icon-default.png?t=O83Ahttps://pan.baidu.com/union/doc/pksg0s9ns

    1. 预准备

    前置的创建应用、获取App Key、获取App Secret、以及交换获取Access Token不再赘述,大致步骤与阿里云盘相同,只是参数和用来请求的链接不同。下面主要关注不同之处:

    1.1. 简化模式授权

    此种模式与阿里云盘中无后端服务授权模式类似,不过不像阿里云盘需要通过个人应用运算生成code_verifier。

    1.2. 设备码模式授权

    流程图如下,通过向硬件设备请求设备码和用户码,作为参数来请求Access Token,使得一些不支持浏览器或输入受限的设备也可以接入。

    2. 上传文件

    百度网盘上传文件,同样也分为三个步骤:

    1. 预上传
    2. 文件上传(有不需要预上传的单步上传)
    3. 创建文件

    与阿里云盘不同的是:百度网盘提供的Access Token已经可以唯一标识用户,所以也没有像阿里云盘那样去获取drive_id,上传文件的时候,常把Access Token用来作为参数。

    此外:

    2.1. 预上传

    在预上传时,如果文件有进行分片,则需要把各个分片的MD5,填入一个字符串数组作为参数;与阿里云盘不同的是,第一个步骤必须填入size这个参数(阿里云盘只有在秒传的时候需要填写size)。

    同样的,响应时会返回uploadid用来唯一标识本次传输任务。

    2.2. 获取上传域名

    在获取上传链接时,百度网盘会返回多个上传链接(为了并行执行多个分片的上传,也可能是负载均衡),使用其中任意一个作为上传链接即可。

    1. // 获取上传 URL
    2. func getUploadUrl(accessToken string, dstDir string, uploadId string) string {
    3. req, err := http.NewRequest("GET",
    4. "https://d.pcs.baidu.com/rest/2.0/pcs/file?"+
    5. "method=locateupload&appid=250528&access_token="+accessToken+
    6. "&path="+dstDir+"&uploadid="+uploadId+"&upload_version=2.0", nil)
    7. if err != nil {
    8. return ""
    9. }
    10. client := &http.Client{}
    11. resp, err := client.Do(req)
    12. if err != nil {
    13. return "Exec req err"
    14. }
    15. defer resp.Body.Close()
    16. body, _ := io.ReadAll(resp.Body)
    17. var uploadDomainResponse UploadDomainResponse
    18. json.Unmarshal(body, &uploadDomainResponse)
    19. return uploadDomainResponse.Servers[0].Server
    20. }

    下面是官方文档中的响应示例:

    2.3. 文件上传
    2.3.1. 单步上传

    针对小于2GB的文件,百度网盘提供了无需预上传和创建文件流程的单步上传,请求参数如下:

    1. // 单步上传
    2. func uploadFile(accessToken, localPath, remotePath string) error {
    3. url := "https://c.pcs.baidu.com/rest/2.0/pcs/file"
    4. params := map[string]string{
    5. "method": "upload",
    6. "access_token": accessToken,
    7. "path": remotePath,
    8. "ondup": "newcopy",
    9. }
    10. file, err := os.Open(localPath)
    11. if err != nil {
    12. return err
    13. }
    14. defer file.Close()
    15. resp, err := grequests.Post(url, &grequests.RequestOptions{
    16. Params: params,
    17. Files: []grequests.FileUpload{
    18. {
    19. FileContents: file,
    20. },
    21. },
    22. })
    23. if err != nil {
    24. return err
    25. }
    26. if !resp.Ok {
    27. return fmt.Errorf("upload failed with status code: %d", resp.StatusCode)
    28. }
    29. return nil
    30. }
    2.3.2. 分片上传

    在请求参数上,百度网盘把参数大多都放在URL的位置,与阿里云盘大多放在Body请求体中不同。此处的分片上传,通过partseq来区分、上传分好的多个分片(也是并行的)。

    2.4. 创建文件

    创建文件和阿里云盘中完成上传所实现的功能类似,把多个分片,组成一个完整的文件。

  • 相关阅读:
    Android 10.0 系统settings系统属性控制一级菜单显示隐藏
    Python机器视觉--OpenCV入门--鼠标事件与TrackBar控件(含小项目:OpenCV调色板)
    GET 和 POST请求的本质区别是什么?
    有关QT的问题大全
    sql注入漏洞(CVE-2022-32991)
    论文笔记:E(n) Equivariant Graph Neural Networks
    Appium移动自动化测试—如何安装Appium
    如何禁止复制电脑文档到U盘或其它移动设备
    科普读书会丨《被讨厌的勇气》:愤怒不是目的,是一种工具
    邮件|gitpushgithub报错|Lombok注解
  • 原文地址:https://blog.csdn.net/m0_75034791/article/details/143439139