为了尽可能地改善终端用户的上传体验,七牛云存储首创了客户端直传功能。一般云存储的上传流程是:

客户端(终端用户) => 业务服务器 => 云存储服务

这样多了一次上传的流程,和本地存储相比,会相对慢一些。但七牛引入了客户端直传,将整个上传过程调整为:

客户端(终端用户) => 七牛 => 业务服务器

客户端(终端用户)直接上传到七牛的服务器,通过DNS智能解析,七牛会选择到离终端用户最近的ISP服务商节点,速度会比本地存储快很多。文件上传成功以后,七牛的服务器使用回调功能,只需要将非常少的数据(比如Key)传给应用服务器,应用服务器进行保存即可。

注意:如果您只是想要上传已存在您电脑本地或者是服务器上的文件到七牛云存储,可以直接使用七牛提供的 qshell 上传工具。
文件上传有两种方式,一种是以普通方式直传文件,简称普通上传,另一种方式是断点续上传,断点续上传在网络条件很一般的情况下也能有出色的上传速度,而且对大文件的传输非常友好。

上传流程

在七牛云存储中,整个上传流程大体分为这样几步:

  1. 业务服务器颁发 上传凭证给客户端(终端用户)
  2. 客户端凭借上传凭证上传文件到七牛
  3. 在七牛获得完整数据后,发起一个 HTTP 请求回调到业务服务器
  4. 业务服务器保存相关信息,并返回一些信息给七牛
  5. 七牛原封不动地将这些信息转发给客户端(终端用户)

需要注意的是,回调到业务服务器的过程是可选的,它取决于业务服务器颁发的 上传凭证。如果没有回调,七牛会返回一些标准的信息(比如文件的 hash)给客户端。如果上传发生在业务服务器,以上流程可以自然简化为:

  1. 业务服务器生成 uptoken
func uptoken(bucketName string) string {
    putPolicy := rs.PutPolicy {
        Scope:         bucketName,
        //CallbackUrl: callbackUrl,   
        //CallbackBody:callbackBody,    
        //ReturnUrl:   returnUrl,  
        //ReturnBody:  returnBody,    
        //AsyncOps:    asyncOps,    
        //EndUser:     endUser,    
        //Expires:     expires,   
    }
    return  putPolicy.Token(nil)
}
  1. 凭借 上传凭证 上传文件到七牛
// 文件上传
func TestUploadFile(t *testing.T) {
    var ak = core.QiNiuAK
    var sk = core.QiNiuSk
    var bucket = core.QiNiuBucket
    var url = core.QiuNiuUrl

    src, err := os.ReadFile("./img/meinv.jpeg")
    if err != nil {
        t.Fatal(err)
    }
    fileSize := len(src)

    putPolicy := storage.PutPolicy{
        Scope: bucket,
    }
    mac := qbox.NewMac(ak, sk)
    upToken := putPolicy.UploadToken(mac)
    cfg := storage.Config{
        Zone:          &storage.ZoneHuanan,
        UseCdnDomains: false,
        UseHTTPS:      false,
    }
    putExtra := storage.PutExtra{}
    formUploader := storage.NewFormUploader(&cfg)
    ret := storage.PutRet{}
    key := "go-cloud-storage/meinv.jpeg"
    err = formUploader.Put(context.Background(), &ret, upToken, key, bytes.NewReader(src), int64(fileSize), &putExtra)
    if err != nil {
        t.Fatal(err)
    }
    url2 := url + ret.Key
    fmt.Println(ret)
    fmt.Println(url2)
}


// 分片上传
func TestUploadChunkFile(t *testing.T) {
    var ak = core.QiNiuAK
    var sk = core.QiNiuSk
    var bucket = core.QiNiuBucket
    var url = core.QiuNiuUrl

    putPolicy := storage.PutPolicy{
        Scope: bucket,
    }
    mac := qbox.NewMac(ak, sk)
    upToken := putPolicy.UploadToken(mac)
    cfg := storage.Config{
        Zone:          &storage.ZoneHuanan,
        UseCdnDomains: false,
        UseHTTPS:      false,
    }
    resumeUploaderV2 := storage.NewResumeUploaderV2(&cfg)
    upHost, err := resumeUploaderV2.UpHost(ak, bucket)
    if err != nil {
        t.Fatal(err)
    }
    key := "go-cloud-storage/lala.mp4"
    // 初始化分块上传
    initPartsRet := storage.InitPartsRet{}
    err = resumeUploaderV2.InitParts(context.TODO(), upToken, upHost, bucket, key, true, &initPartsRet)
    if err != nil {
        t.Fatal(err)
    }

    fileInfo, err := os.Open("./music/lala.mp4")
    if err != nil {
        t.Fatal(err)
    }
    defer fileInfo.Close()
    fileContent, err := ioutil.ReadAll(fileInfo)
    if err != nil {
        t.Fatal(err)
    }
    fileLen := len(fileContent)
    chunkSize2 := 2 * 1024 * 1024

    num := fileLen / chunkSize2
    if fileLen%chunkSize2 > 0 {
        num++
    }

    // 分块上传
    var uploadPartInfos []storage.UploadPartInfo
    for i := 1; i <= num; i++ {
        partNumber := int64(i)
        fmt.Printf("开始上传第%v片数据", partNumber)

        var partContentBytes []byte
        endSize := i * chunkSize2
        if endSize > fileLen {
            endSize = fileLen
        }
        partContentBytes = fileContent[(i-1)*chunkSize2 : endSize]
        partContentMd5 := Md5(string(partContentBytes))
        uploadPartsRet := storage.UploadPartsRet{}
        err = resumeUploaderV2.UploadParts(context.TODO(), upToken, upHost, bucket, key, true,
            initPartsRet.UploadID, partNumber, partContentMd5, &uploadPartsRet, bytes.NewReader(partContentBytes),
            len(partContentBytes))
        if err != nil {
            t.Fatal(err)
        }
        uploadPartInfos = append(uploadPartInfos, storage.UploadPartInfo{
            Etag:       uploadPartsRet.Etag,
            PartNumber: partNumber,
        })
        fmt.Printf("结束上传第%d片数据\n", partNumber)
    }

    // 完成上传
    rPutExtra := storage.RputV2Extra{Progresses: uploadPartInfos}
    comletePartRet := storage.PutRet{}
    err = resumeUploaderV2.CompleteParts(context.TODO(), upToken, upHost, &comletePartRet, bucket, key,
        true, initPartsRet.UploadID, &rPutExtra)
    if err != nil {
        t.Fatal(err)
    }

    url2 := url + comletePartRet.Key
    fmt.Println(comletePartRet.Hash)
    fmt.Println(url2)
}


// 断点续传
func TestResumeUploadFile(t *testing.T) {
    ak := core.QiNiuAK
    sk := core.QiNiuSk
    localFile := "./music/abc.mp4"
    bucket := core.QiNiuBucket
    key := "go-cloud-storage/abc.mp4"
    url := core.QiuNiuUrl
    putPolicy := storage.PutPolicy{
        Scope: bucket,
    }
    mac := qbox.NewMac(ak, sk)
    upToken := putPolicy.UploadToken(mac)
    cfg := storage.Config{
        Zone:          &storage.ZoneHuanan,
        UseCdnDomains: false,
        UseHTTPS:      false,
    }
    resumeUploaderV2 := storage.NewResumeUploaderV2(&cfg)
    ret := storage.PutRet{}
    recorder, err := storage.NewFileRecorder(os.TempDir())
    if err != nil {
        t.Fatal(err)
    }
    putExtra := storage.RputV2Extra{
        Recorder: recorder,
    }
    err = resumeUploaderV2.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
    if err != nil {
        fmt.Println(err)
        return
    }
    url2 := url + ret.Key
    fmt.Println(ret)
    fmt.Println(url2)
}