文件上传

上传流程

七牛文件上传分为客户端上传(主要是指网页端和移动端等面向终端用户的场景)和服务端上传两种场景,具体可以参考文档七牛业务流程

服务端SDK在上传方面主要提供两种功能,一种是生成客户端上传所需要的上传凭证,另外一种是直接上传文件到云端。

客户端上传凭证

客户端(移动端或者Web端)上传文件的时候,需要从客户自己的业务服务器获取上传凭证,而这些上传凭证是通过服务端的SDK来生成的,然后通过客户自己的业务API分发给客户端使用。根据上传的业务需求不同,七牛云 Go SDK支持丰富的上传凭证生成方式。

  1. // 存储相关功能的引入包只有这两个,后面不再赘述
  2. import (
  3. "github.com/qiniu/api.v7/auth/qbox"
  4. "github.com/qiniu/api.v7/storage"
  5. )
  6. accessKey := "your access key"
  7. secretKey := "your secret key"
  8. mac := qbox.NewMac(accessKey, secretKey)

简单上传的凭证

最简单的上传凭证只需要AccessKeySecretKeyBucket就可以。

  1. bucket:="your bucket name"
  2. putPolicy := storage.PutPolicy{
  3. Scope: bucket,
  4. }
  5. mac := qbox.NewMac(accessKey, secretKey)
  6. upToken := putPolicy.UploadToken(mac)

默认情况下,在不指定上传凭证的有效时间情况下,默认有效期为1个小时。也可以自行指定上传凭证的有效期,例如:

  1. //自定义凭证有效期(示例2小时,Expires 单位为秒,为上传凭证的有效时间)
  2. bucket := "your bucket name"
  3. putPolicy := storage.PutPolicy{
  4. Scope: bucket,
  5. }
  6. putPolicy.Expires = 7200 //示例2小时有效期
  7. mac := qbox.NewMac(accessKey, secretKey)
  8. upToken := putPolicy.UploadToken(mac)

覆盖上传的凭证

覆盖上传除了需要简单上传所需要的信息之外,还需要想进行覆盖的文件名称,这个文件名称同时可是客户端上传代码中指定的文件名,两者必须一致。

  1. bucket := "your bucket name"
  2. // 需要覆盖的文件名
  3. keyToOverwrite := "qiniu.mp4"
  4. putPolicy := storage.PutPolicy{
  5. Scope: fmt.Sprintf("%s:%s", bucket, keyToOverwrite),
  6. }
  7. mac := qbox.NewMac(accessKey, secretKey)
  8. upToken := putPolicy.UploadToken(mac)

自定义上传回复的凭证

默认情况下,文件上传到七牛之后,在没有设置returnBody或者回调相关的参数情况下,七牛返回给上传端的回复格式为hashkey,例如:

  1. {"hash":"Ftgm-CkWePC9fzMBTRNmPMhGBcSV","key":"qiniu.jpg"}

有时候我们希望能自定义这个返回的JSON格式的内容,可以通过设置returnBody参数来实现,在returnBody中,我们可以使用七牛支持的魔法变量自定义变量

  1. bucket := "your bucket name"
  2. putPolicy := storage.PutPolicy{
  3. Scope: bucket,
  4. ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
  5. }
  6. mac := qbox.NewMac(accessKey, secretKey)
  7. upToken := putPolicy.UploadToken(mac)

则文件上传到七牛之后,收到的回复内容格式如下:

  1. {"key":"github-x.png","hash":"FqKXVdTvIx_mPjOYdjDyUSy_H1jr","fsize":6091,"bucket":"if-pbl","name":"github logo"}

对于上面的自定义返回值,我们需要自定义结构体来解析这个回复,例如下面提供了一个解析结果的方法:

  1. // 自定义返回值结构体
  2. type MyPutRet struct {
  3. Key string
  4. Hash string
  5. Fsize int
  6. Bucket string
  7. Name string
  8. }
  9. localFile := "your local file path"
  10. bucket := "your bucket name"
  11. key := "your file save key"
  12. // 使用 returnBody 自定义回复格式
  13. putPolicy := storage.PutPolicy{
  14. Scope: bucket,
  15. ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
  16. }
  17. mac := qbox.NewMac(accessKey, secretKey)
  18. upToken := putPolicy.UploadToken(mac)
  19. cfg := storage.Config{}
  20. formUploader := storage.NewFormUploader(&cfg)
  21. ret := MyPutRet{}
  22. putExtra := storage.PutExtra{
  23. Params: map[string]string{
  24. "x:name": "github logo",
  25. },
  26. }
  27. err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
  28. if err != nil {
  29. fmt.Println(err)
  30. return
  31. }
  32. fmt.Println(ret.Bucket, ret.Key, ret.Fsize, ret.Hash, ret.Name)

带回调业务服务器的凭证

上面生成的自定义上传回复的上传凭证适用于上传端(无论是客户端还是服务端)和七牛服务器之间进行直接交互的情况下。在客户端上传的场景之下,有时候客户端需要在文件上传到七牛之后,从业务服务器获取相关的信息,这个时候就要用到七牛的上传回调及相关回调参数的设置。

  1. putPolicy := storage.PutPolicy{
  2. Scope: bucket,
  3. CallbackURL: "http://api.example.com/qiniu/upload/callback",
  4. CallbackBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
  5. CallbackBodyType: "application/json",
  6. }
  7. mac := qbox.NewMac(accessKey, secretKey)
  8. upToken := putPolicy.UploadToken(mac)

在使用了上传回调的情况下,客户端收到的回复就是业务服务器响应七牛的JSON格式内容,客户端收到回调之后必须响应JSON格式的回复給七牛,这个回复会被七牛传递给客户端。例如上面的 CallbackBody 的设置会在文件上传到七牛之后,触发七牛回调如下内容給业务服务器:

  1. {"key":"github-x.png","hash":"FqKXVdTvIx_mPjOYdjDyUSy_H1jr","fsize":6091,"bucket":"if-pbl","name":"github logo"}

通常情况下,我们建议使用application/json格式来设置callbackBody,保持数据格式的统一性。实际情况下,callbackBody也支持application/x-www-form-urlencoded格式来组织内容,这个主要看业务服务器在接收到callbackBody的内容时如果解析。例如:

  1. putPolicy := storage.PutPolicy{
  2. Scope: bucket,
  3. CallbackURL: "http://api.example.com/qiniu/upload/callback",
  4. CallbackBody: "key=$(key)&hash=$(etag)&bucket=$(bucket)&fsize=$(fsize)&name=$(x:name)",
  5. }
  6. mac := qbox.NewMac(accessKey, secretKey)
  7. upToken := putPolicy.UploadToken(mac)

带数据处理的凭证

七牛支持在文件上传到七牛之后,立即对其进行多种指令的数据处理,这个只需要在生成的上传凭证中指定相关的处理参数即可。

  1. saveMp4Entry := base64.URLEncoding.EncodeToString([]byte(bucket + ":avthumb_test_target.mp4"))
  2. saveJpgEntry := base64.URLEncoding.EncodeToString([]byte(bucket + ":vframe_test_target.jpg"))
  3. //数据处理指令,支持多个指令
  4. avthumbMp4Fop := "avthumb/mp4|saveas/" + saveMp4Entry
  5. vframeJpgFop := "vframe/jpg/offset/1|saveas/" + saveJpgEntry
  6. //连接多个操作指令
  7. persistentOps := strings.Join([]string{avthumbMp4Fop, vframeJpgFop}, ";")
  8. pipeline := "test"
  9. putPolicy := storage.PutPolicy{
  10. Scope: bucket,
  11. PersistentOps: persistentOps,
  12. PersistentPipeline: pipeline,
  13. PersistentNotifyURL: "http://api.example.com/qiniu/pfop/notify",
  14. }
  15. mac := qbox.NewMac(accessKey, secretKey)
  16. upToken := putPolicy.UploadToken(mac)

队列 pipeline 请参阅创建私有队列;转码操作具体参数请参阅音视频转码;saveas 请参阅处理结果另存

带自定义参数的凭证

七牛支持客户端上传文件的时候定义一些自定义参数,这些参数可以在returnBodycallbackBody里面和七牛内置支持的魔法变量(即系统变量)通过相同的方式来引用。这些自定义的参数名称必须以x:开头。例如客户端上传的时候指定了自定义的参数x:namex:age分别是stringint类型。那么可以通过下面的方式引用:

  1. putPolicy := storage.PutPolicy{
  2. //其他上传策略参数...
  3. ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)","age":$(x:age)}`,
  4. }

或者

  1. putPolicy := storage.PutPolicy{
  2. //其他上传策略参数...
  3. CallbackBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)","age":$(x:age)}`,
  4. }

综合上传凭证

上面的生成上传凭证的方法,都是通过设置上传策略?相关的参数来支持的,这些参数可以通过不同的组合方式来满足不同的业务需求,可以灵活地组织你所需要的上传凭证。

服务端直传

服务端直传是指客户利用七牛服务端SDK从服务端直接上传文件到七牛云,交互的双方一般都在机房里面,所以服务端可以自己生成上传凭证,然后利用SDK中的上传逻辑进行上传,最后从七牛云获取上传的结果,这个过程中由于双方都是业务服务器,所以很少利用到上传回调的功能,而是直接自定义returnBody来获取自定义的回复内容。

构建配置类

七牛存储支持空间创建在不同的机房,在使用七牛的 Go SDK 中的FormUploaderResumeUploader上传文件之前,必须要构建一个上传用的Config对象,在该对象中,可以指定空间对应的zone以及其他的一些影响上传的参数。

  1. cfg := storage.Config{}
  2. // 空间对应的机房
  3. cfg.Zone = &storage.ZoneHuadong
  4. // 是否使用https域名
  5. cfg.UseHTTPS = false
  6. // 上传是否使用CDN上传加速
  7. cfg.UseCdnDomains = false

其中关于Zone对象和机房的关系如下:

机房Zone对象
华东storage.ZoneHuadong
华北storage.ZoneHuabei
华南storage.ZoneHuanan
北美storage.ZoneBeimei

文件上传(表单方式)

最简单的就是上传本地文件,直接指定文件的完整路径即可上传。

  1. localFile = "/Users/jemy/Documents/github.png"
  2. bucket = "if-pbl"
  3. key = "github-x.png"
  4. putPolicy := storage.PutPolicy{
  5. Scope: bucket,
  6. }
  7. mac := qbox.NewMac(accessKey, secretKey)
  8. upToken := putPolicy.UploadToken(mac)
  9. cfg := storage.Config{}
  10. // 空间对应的机房
  11. cfg.Zone = &storage.ZoneHuadong
  12. // 是否使用https域名
  13. cfg.UseHTTPS = false
  14. // 上传是否使用CDN上传加速
  15. cfg.UseCdnDomains = false
  16. // 构建表单上传的对象
  17. formUploader := storage.NewFormUploader(&cfg)
  18. ret := storage.PutRet{}
  19. // 可选配置
  20. putExtra := storage.PutExtra{
  21. Params: map[string]string{
  22. "x:name": "github logo",
  23. },
  24. }
  25. err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
  26. if err != nil {
  27. fmt.Println(err)
  28. return
  29. }
  30. fmt.Println(ret.Key,ret.Hash)

字节数组上传(表单方式)

可以支持将内存中的字节数组上传到空间中。

  1. putPolicy := storage.PutPolicy{
  2. Scope: bucket,
  3. }
  4. mac := qbox.NewMac(accessKey, secretKey)
  5. upToken := putPolicy.UploadToken(mac)
  6. cfg := storage.Config{}
  7. // 空间对应的机房
  8. cfg.Zone = &storage.ZoneHuadong
  9. // 是否使用https域名
  10. cfg.UseHTTPS = false
  11. // 上传是否使用CDN上传加速
  12. cfg.UseCdnDomains = false
  13. formUploader := storage.NewFormUploader(&cfg)
  14. ret := storage.PutRet{}
  15. putExtra := storage.PutExtra{
  16. Params: map[string]string{
  17. "x:name": "github logo",
  18. },
  19. }
  20. data := []byte("hello, this is qiniu cloud")
  21. dataLen := int64(len(data))
  22. err := formUploader.Put(context.Background(), &ret, upToken, key, bytes.NewReader(data), dataLen, &putExtra)
  23. if err != nil {
  24. fmt.Println(err)
  25. return
  26. }
  27. fmt.Println(ret.Key, ret.Hash)

数据流上传(表单方式)

io.Reader对象的上传也是采用Put方法或者PutWithoutKey方法,使用方式和上述的 字节数组上传 方式相同。

文件分片上传

对于大的文件,我们一般推荐使用分片上传的方式来上传文件,分片上传通过将一个文件切割为标准的块(固定大小4MB),然后再将每个块切割为数据片,然后通过上传片的方式来进行文件的上传。一个块中的片和另外一个块中的片是可以并发的,但是同一个块中的片是顺序上传的。片大小必须可以整除块大小4MB,服务端SDK中默认片大小为4MB,以提高上传效率。分片上传不等于断点续传,但是分片上传可以支持断点续传。

断点续传是将每个块上传完毕的返回的context保存到本地的文件中持久化,如果本次上传被中断,下次可以从这个进度文件中读取每个块上传的状态,然后继续上传完毕没有完成的块,最后完成文件的拼接。这里需要注意,只有在块上传完毕之后,才向本地的进度文件写入context内容。另外需要注意,每个context的有效期最长是7天,过期的context会触发701的错误,我们可以检查context中的expire参数来确认上传的进度是否过期。

  1. putPolicy := storage.PutPolicy{
  2. Scope: bucket,
  3. }
  4. mac := qbox.NewMac(accessKey, secretKey)
  5. upToken := putPolicy.UploadToken(mac)
  6. cfg := storage.Config{}
  7. // 空间对应的机房
  8. cfg.Zone = &storage.ZoneHuadong
  9. // 是否使用https域名
  10. cfg.UseHTTPS = false
  11. // 上传是否使用CDN上传加速
  12. cfg.UseCdnDomains = false
  13. resumeUploader := storage.NewResumeUploader(&cfg)
  14. ret := storage.PutRet{}
  15. putExtra := storage.RputExtra{
  16. }
  17. err := resumeUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
  18. if err != nil {
  19. fmt.Println(err)
  20. return
  21. }
  22. fmt.Println(ret.Key, ret.Hash)

文件断点续传

断点续传是基于分片上传来实现的,基本原理就是用一个文本文件记录下上传的进度,如果上传中断,下次再从这个文件读取进度,继续上传未完成的分块。

  1. package main
  2. import (
  3. "crypto/md5"
  4. "encoding/hex"
  5. "encoding/json"
  6. "fmt"
  7. "github.com/qiniu/api.v7/auth/qbox"
  8. "github.com/qiniu/api.v7/storage"
  9. "golang.org/x/net/context"
  10. "io/ioutil"
  11. "os"
  12. "path/filepath"
  13. )
  14. var (
  15. accessKey = "your access key"
  16. secretKey = "your secret key"
  17. )
  18. func md5Hex(str string) string {
  19. h := md5.New()
  20. h.Write([]byte(str))
  21. return hex.EncodeToString(h.Sum(nil))
  22. }
  23. type ProgressRecord struct {
  24. Progresses []storage.BlkputRet `json:"progresses"`
  25. }
  26. func main() {
  27. localFile := "your local file path"
  28. bucket := "your bucket name"
  29. key := "your file save key"
  30. putPolicy := storage.PutPolicy{
  31. Scope: bucket,
  32. }
  33. mac := qbox.NewMac(accessKey, secretKey)
  34. upToken := putPolicy.UploadToken(mac)
  35. cfg := storage.Config{}
  36. // 空间对应的机房
  37. cfg.Zone = &storage.ZoneHuadong
  38. // 是否使用https域名
  39. cfg.UseHTTPS = false
  40. // 上传是否使用CDN上传加速
  41. cfg.UseCdnDomains = false
  42. // 必须仔细选择一个能标志上传唯一性的 recordKey 用来记录上传进度
  43. // 我们这里采用 md5(bucket+key+local_path+local_file_last_modified)+".progress" 作为记录上传进度的文件名
  44. fileInfo, statErr := os.Stat(localFile)
  45. if statErr != nil {
  46. fmt.Println(statErr)
  47. return
  48. }
  49. fileSize := fileInfo.Size()
  50. fileLmd := fileInfo.ModTime().UnixNano()
  51. recordKey := md5Hex(fmt.Sprintf("%s:%s:%s:%s", bucket, key, localFile, fileLmd)) + ".progress"
  52. // 指定的进度文件保存目录,实际情况下,请确保该目录存在,而且只用于记录进度文件
  53. recordDir := "/Users/jemy/Temp/progress"
  54. mErr := os.MkdirAll(recordDir, 0755)
  55. if mErr != nil {
  56. fmt.Println("mkdir for record dir error,", mErr)
  57. return
  58. }
  59. recordPath := filepath.Join(recordDir, recordKey)
  60. progressRecord := ProgressRecord{}
  61. // 尝试从旧的进度文件中读取进度
  62. recordFp, openErr := os.Open(recordPath)
  63. if openErr == nil {
  64. progressBytes, readErr := ioutil.ReadAll(recordFp)
  65. if readErr == nil {
  66. mErr := json.Unmarshal(progressBytes, &progressRecord)
  67. if mErr == nil {
  68. // 检查context 是否过期,避免701错误
  69. for _, item := range progressRecord.Progresses {
  70. if storage.IsContextExpired(item) {
  71. fmt.Println(item.ExpiredAt)
  72. progressRecord.Progresses = make([]storage.BlkputRet, storage.BlockCount(fileSize))
  73. break
  74. }
  75. }
  76. }
  77. }
  78. recordFp.Close()
  79. }
  80. if len(progressRecord.Progresses) == 0 {
  81. progressRecord.Progresses = make([]storage.BlkputRet, storage.BlockCount(fileSize))
  82. }
  83. resumeUploader := storage.NewResumeUploader(&cfg)
  84. ret := storage.PutRet{}
  85. progressLock := sync.RWMutex{}
  86. putExtra := storage.RputExtra{
  87. Progresses: progressRecord.Progresses,
  88. Notify: func(blkIdx int, blkSize int, ret *storage.BlkputRet) {
  89. progressLock.Lock()
  90. progressLock.Unlock()
  91. //将进度序列化,然后写入文件
  92. progressRecord.Progresses[blkIdx] = *ret
  93. progressBytes, _ := json.Marshal(progressRecord)
  94. fmt.Println("write progress file", blkIdx, recordPath)
  95. wErr := ioutil.WriteFile(recordPath, progressBytes, 0644)
  96. if wErr != nil {
  97. fmt.Println("write progress file error,", wErr)
  98. }
  99. },
  100. }
  101. err := resumeUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
  102. if err != nil {
  103. fmt.Println(err)
  104. return
  105. }
  106. //上传成功之后,一定记得删除这个进度文件
  107. os.Remove(recordPath)
  108. fmt.Println(ret.Key, ret.Hash)
  109. }

解析自定义回复内容

有些情况下,七牛返回给上传端的内容不是默认的hashkey形式,这种情况下,可能出现在自定义returnBody或者自定义了callbackBody的情况下,前者一般是服务端直传的场景,而后者则是接受上传回调的场景,这两种场景之下,都涉及到需要将自定义的回复进行内容解析,一般建议在交互过程中,都采用JSON的方式,这样处理起来方法比较一致,而且JSON的方法最通用。默认情况下SDK提供了storage.PutRet来作为标准回复的解析结构体,如果需要用到自定义回复,可以类似上面讲解returnBody的时候给的例子,自定义一个结构体用于回复的解析,例如:

  1. // 自定义返回值结构体
  2. type MyPutRet struct {
  3. Key string
  4. Hash string
  5. Fsize int
  6. Bucket string
  7. Name string
  8. }

业务服务器验证七牛回调

在上传策略里面设置了上传回调相关参数的时候,七牛在文件上传到服务器之后,会主动地向callbackUrl发送POST请求的回调,回调的内容为callbackBody模版所定义的内容,如果这个模版里面引用了魔法变量或者自定义变量,那么这些变量会被自动填充对应的值,然后在发送给业务服务器。

业务服务器在收到来自七牛的回调请求的时候,可以根据请求头部的Authorization字段来进行验证,查看该请求是否是来自七牛的未经篡改的请求。

Go SDK 提供了一个方法 qbox.VerifyCallback 用于验证回调的请求:

  1. // VerifyCallback 验证上传回调请求是否来自七牛
  2. func VerifyCallback(mac *Mac, req *http.Request) (bool, error) {
  3. return mac.VerifyCallback(req)
  4. }