文件上传
上传流程
七牛文件上传分为客户端上传(主要是指网页端和移动端等面向终端用户的场景)和服务端上传两种场景,具体可以参考文档七牛业务流程。
服务端SDK在上传方面主要提供两种功能,一种是生成客户端上传所需要的上传凭证,另外一种是直接上传文件到云端。
客户端上传凭证
客户端(移动端或者Web端)上传文件的时候,需要从客户自己的业务服务器获取上传凭证,而这些上传凭证是通过服务端的SDK来生成的,然后通过客户自己的业务API分发给客户端使用。根据上传的业务需求不同,七牛云 Go SDK支持丰富的上传凭证生成方式。
// 存储相关功能的引入包只有这两个,后面不再赘述
import (
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
)
accessKey := "your access key"
secretKey := "your secret key"
mac := qbox.NewMac(accessKey, secretKey)
简单上传的凭证
最简单的上传凭证只需要AccessKey
,SecretKey
和Bucket
就可以。
bucket:="your bucket name"
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
默认情况下,在不指定上传凭证的有效时间情况下,默认有效期为1个小时。也可以自行指定上传凭证的有效期,例如:
//自定义凭证有效期(示例2小时,Expires 单位为秒,为上传凭证的有效时间)
bucket := "your bucket name"
putPolicy := storage.PutPolicy{
Scope: bucket,
}
putPolicy.Expires = 7200 //示例2小时有效期
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
覆盖上传的凭证
覆盖上传除了需要简单上传
所需要的信息之外,还需要想进行覆盖的文件名称,这个文件名称同时可是客户端上传代码中指定的文件名,两者必须一致。
bucket := "your bucket name"
// 需要覆盖的文件名
keyToOverwrite := "qiniu.mp4"
putPolicy := storage.PutPolicy{
Scope: fmt.Sprintf("%s:%s", bucket, keyToOverwrite),
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
自定义上传回复的凭证
默认情况下,文件上传到七牛之后,在没有设置returnBody
或者回调
相关的参数情况下,七牛返回给上传端的回复格式为hash
和key
,例如:
{"hash":"Ftgm-CkWePC9fzMBTRNmPMhGBcSV","key":"qiniu.jpg"}
有时候我们希望能自定义这个返回的JSON格式的内容,可以通过设置returnBody
参数来实现,在returnBody
中,我们可以使用七牛支持的魔法变量和自定义变量。
bucket := "your bucket name"
putPolicy := storage.PutPolicy{
Scope: bucket,
ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
则文件上传到七牛之后,收到的回复内容格式如下:
{"key":"github-x.png","hash":"FqKXVdTvIx_mPjOYdjDyUSy_H1jr","fsize":6091,"bucket":"if-pbl","name":"github logo"}
对于上面的自定义返回值,我们需要自定义结构体来解析这个回复,例如下面提供了一个解析结果的方法:
// 自定义返回值结构体
type MyPutRet struct {
Key string
Hash string
Fsize int
Bucket string
Name string
}
localFile := "your local file path"
bucket := "your bucket name"
key := "your file save key"
// 使用 returnBody 自定义回复格式
putPolicy := storage.PutPolicy{
Scope: bucket,
ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
formUploader := storage.NewFormUploader(&cfg)
ret := MyPutRet{}
putExtra := storage.PutExtra{
Params: map[string]string{
"x:name": "github logo",
},
}
err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Bucket, ret.Key, ret.Fsize, ret.Hash, ret.Name)
带回调业务服务器的凭证
上面生成的自定义上传回复
的上传凭证适用于上传端(无论是客户端还是服务端)和七牛服务器之间进行直接交互的情况下。在客户端上传的场景之下,有时候客户端需要在文件上传到七牛之后,从业务服务器获取相关的信息,这个时候就要用到七牛的上传回调及相关回调参数的设置。
putPolicy := storage.PutPolicy{
Scope: bucket,
CallbackURL: "http://api.example.com/qiniu/upload/callback",
CallbackBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
CallbackBodyType: "application/json",
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
在使用了上传回调的情况下,客户端收到的回复就是业务服务器响应七牛的JSON格式内容,客户端收到回调之后必须响应JSON格式的回复給七牛,这个回复会被七牛传递给客户端。例如上面的 CallbackBody
的设置会在文件上传到七牛之后,触发七牛回调如下内容給业务服务器:
{"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
的内容时如果解析。例如:
putPolicy := storage.PutPolicy{
Scope: bucket,
CallbackURL: "http://api.example.com/qiniu/upload/callback",
CallbackBody: "key=$(key)&hash=$(etag)&bucket=$(bucket)&fsize=$(fsize)&name=$(x:name)",
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
带数据处理的凭证
七牛支持在文件上传到七牛之后,立即对其进行多种指令的数据处理,这个只需要在生成的上传凭证中指定相关的处理参数即可。
saveMp4Entry := base64.URLEncoding.EncodeToString([]byte(bucket + ":avthumb_test_target.mp4"))
saveJpgEntry := base64.URLEncoding.EncodeToString([]byte(bucket + ":vframe_test_target.jpg"))
//数据处理指令,支持多个指令
avthumbMp4Fop := "avthumb/mp4|saveas/" + saveMp4Entry
vframeJpgFop := "vframe/jpg/offset/1|saveas/" + saveJpgEntry
//连接多个操作指令
persistentOps := strings.Join([]string{avthumbMp4Fop, vframeJpgFop}, ";")
pipeline := "test"
putPolicy := storage.PutPolicy{
Scope: bucket,
PersistentOps: persistentOps,
PersistentPipeline: pipeline,
PersistentNotifyURL: "http://api.example.com/qiniu/pfop/notify",
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
队列 pipeline 请参阅创建私有队列;转码操作具体参数请参阅音视频转码;saveas 请参阅处理结果另存。
带自定义参数的凭证
七牛支持客户端上传文件的时候定义一些自定义参数,这些参数可以在returnBody
和callbackBody
里面和七牛内置支持的魔法变量(即系统变量)通过相同的方式来引用。这些自定义的参数名称必须以x:
开头。例如客户端上传的时候指定了自定义的参数x:name
和x:age
分别是string
和int
类型。那么可以通过下面的方式引用:
putPolicy := storage.PutPolicy{
//其他上传策略参数...
ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)","age":$(x:age)}`,
}
或者
putPolicy := storage.PutPolicy{
//其他上传策略参数...
CallbackBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)","age":$(x:age)}`,
}
综合上传凭证
上面的生成上传凭证的方法,都是通过设置上传策略?相关的参数来支持的,这些参数可以通过不同的组合方式来满足不同的业务需求,可以灵活地组织你所需要的上传凭证。
服务端直传
服务端直传是指客户利用七牛服务端SDK从服务端直接上传文件到七牛云,交互的双方一般都在机房里面,所以服务端可以自己生成上传凭证,然后利用SDK中的上传逻辑进行上传,最后从七牛云获取上传的结果,这个过程中由于双方都是业务服务器,所以很少利用到上传回调的功能,而是直接自定义returnBody
来获取自定义的回复内容。
构建配置类
七牛存储支持空间创建在不同的机房,在使用七牛的 Go SDK 中的FormUploader
和ResumeUploader
上传文件之前,必须要构建一个上传用的Config
对象,在该对象中,可以指定空间对应的zone
以及其他的一些影响上传的参数。
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
其中关于Zone
对象和机房的关系如下:
机房 | Zone对象 |
---|---|
华东 | storage.ZoneHuadong |
华北 | storage.ZoneHuabei |
华南 | storage.ZoneHuanan |
北美 | storage.ZoneBeimei |
文件上传(表单方式)
最简单的就是上传本地文件,直接指定文件的完整路径即可上传。
localFile = "/Users/jemy/Documents/github.png"
bucket = "if-pbl"
key = "github-x.png"
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
// 构建表单上传的对象
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
// 可选配置
putExtra := storage.PutExtra{
Params: map[string]string{
"x:name": "github logo",
},
}
err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Key,ret.Hash)
字节数组上传(表单方式)
可以支持将内存中的字节数组上传到空间中。
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{
Params: map[string]string{
"x:name": "github logo",
},
}
data := []byte("hello, this is qiniu cloud")
dataLen := int64(len(data))
err := formUploader.Put(context.Background(), &ret, upToken, key, bytes.NewReader(data), dataLen, &putExtra)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Key, ret.Hash)
数据流上传(表单方式)
io.Reader
对象的上传也是采用Put
方法或者PutWithoutKey
方法,使用方式和上述的 字节数组上传
方式相同。
文件分片上传
对于大的文件,我们一般推荐使用分片上传的方式来上传文件,分片上传通过将一个文件切割为标准的块(固定大小4MB),然后再将每个块切割为数据片,然后通过上传片的方式来进行文件的上传。一个块中的片和另外一个块中的片是可以并发的,但是同一个块中的片是顺序上传的。片大小必须可以整除块大小4MB,服务端SDK中默认片大小为4MB,以提高上传效率。分片上传不等于断点续传,但是分片上传可以支持断点续传。
断点续传是将每个块上传完毕的返回的context保存到本地的文件中持久化,如果本次上传被中断,下次可以从这个进度文件中读取每个块上传的状态,然后继续上传完毕没有完成的块,最后完成文件的拼接。这里需要注意,只有在块上传完毕之后,才向本地的进度文件写入context内容。另外需要注意,每个context的有效期最长是7天,过期的context会触发701的错误,我们可以检查context中的expire参数来确认上传的进度是否过期。
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
resumeUploader := storage.NewResumeUploader(&cfg)
ret := storage.PutRet{}
putExtra := storage.RputExtra{
}
err := resumeUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret.Key, ret.Hash)
文件断点续传
断点续传是基于分片上传来实现的,基本原理就是用一个文本文件记录下上传的进度,如果上传中断,下次再从这个文件读取进度,继续上传未完成的分块。
package main
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/storage"
"golang.org/x/net/context"
"io/ioutil"
"os"
"path/filepath"
)
var (
accessKey = "your access key"
secretKey = "your secret key"
)
func md5Hex(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
type ProgressRecord struct {
Progresses []storage.BlkputRet `json:"progresses"`
}
func main() {
localFile := "your local file path"
bucket := "your bucket name"
key := "your file save key"
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(accessKey, secretKey)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Zone = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = false
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
// 必须仔细选择一个能标志上传唯一性的 recordKey 用来记录上传进度
// 我们这里采用 md5(bucket+key+local_path+local_file_last_modified)+".progress" 作为记录上传进度的文件名
fileInfo, statErr := os.Stat(localFile)
if statErr != nil {
fmt.Println(statErr)
return
}
fileSize := fileInfo.Size()
fileLmd := fileInfo.ModTime().UnixNano()
recordKey := md5Hex(fmt.Sprintf("%s:%s:%s:%s", bucket, key, localFile, fileLmd)) + ".progress"
// 指定的进度文件保存目录,实际情况下,请确保该目录存在,而且只用于记录进度文件
recordDir := "/Users/jemy/Temp/progress"
mErr := os.MkdirAll(recordDir, 0755)
if mErr != nil {
fmt.Println("mkdir for record dir error,", mErr)
return
}
recordPath := filepath.Join(recordDir, recordKey)
progressRecord := ProgressRecord{}
// 尝试从旧的进度文件中读取进度
recordFp, openErr := os.Open(recordPath)
if openErr == nil {
progressBytes, readErr := ioutil.ReadAll(recordFp)
if readErr == nil {
mErr := json.Unmarshal(progressBytes, &progressRecord)
if mErr == nil {
// 检查context 是否过期,避免701错误
for _, item := range progressRecord.Progresses {
if storage.IsContextExpired(item) {
fmt.Println(item.ExpiredAt)
progressRecord.Progresses = make([]storage.BlkputRet, storage.BlockCount(fileSize))
break
}
}
}
}
recordFp.Close()
}
if len(progressRecord.Progresses) == 0 {
progressRecord.Progresses = make([]storage.BlkputRet, storage.BlockCount(fileSize))
}
resumeUploader := storage.NewResumeUploader(&cfg)
ret := storage.PutRet{}
progressLock := sync.RWMutex{}
putExtra := storage.RputExtra{
Progresses: progressRecord.Progresses,
Notify: func(blkIdx int, blkSize int, ret *storage.BlkputRet) {
progressLock.Lock()
progressLock.Unlock()
//将进度序列化,然后写入文件
progressRecord.Progresses[blkIdx] = *ret
progressBytes, _ := json.Marshal(progressRecord)
fmt.Println("write progress file", blkIdx, recordPath)
wErr := ioutil.WriteFile(recordPath, progressBytes, 0644)
if wErr != nil {
fmt.Println("write progress file error,", wErr)
}
},
}
err := resumeUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
if err != nil {
fmt.Println(err)
return
}
//上传成功之后,一定记得删除这个进度文件
os.Remove(recordPath)
fmt.Println(ret.Key, ret.Hash)
}
解析自定义回复内容
有些情况下,七牛返回给上传端的内容不是默认的hash
和key
形式,这种情况下,可能出现在自定义returnBody
或者自定义了callbackBody
的情况下,前者一般是服务端直传的场景,而后者则是接受上传回调的场景,这两种场景之下,都涉及到需要将自定义的回复进行内容解析,一般建议在交互过程中,都采用JSON
的方式,这样处理起来方法比较一致,而且JSON
的方法最通用。默认情况下SDK提供了storage.PutRet
来作为标准回复的解析结构体,如果需要用到自定义回复,可以类似上面讲解returnBody
的时候给的例子,自定义一个结构体用于回复的解析,例如:
// 自定义返回值结构体
type MyPutRet struct {
Key string
Hash string
Fsize int
Bucket string
Name string
}
业务服务器验证七牛回调
在上传策略里面设置了上传回调相关参数的时候,七牛在文件上传到服务器之后,会主动地向callbackUrl
发送POST请求的回调,回调的内容为callbackBody
模版所定义的内容,如果这个模版里面引用了魔法变量或者自定义变量,那么这些变量会被自动填充对应的值,然后在发送给业务服务器。
业务服务器在收到来自七牛的回调请求的时候,可以根据请求头部的Authorization
字段来进行验证,查看该请求是否是来自七牛的未经篡改的请求。
Go SDK 提供了一个方法 qbox.VerifyCallback
用于验证回调的请求:
// VerifyCallback 验证上传回调请求是否来自七牛
func VerifyCallback(mac *Mac, req *http.Request) (bool, error) {
return mac.VerifyCallback(req)
}