7、AWS SDK for Go-文件分片上传

汝彭薄
2023-12-01

对象介绍

分段即分片。
文档地址:传送门

源数据

ETag	表示对象的特定版本。对于未作为分段上传上传、未加密或使用 Amazon S3 托管密钥 (SSE-S3) 的服务器端加密进行加密的对象,
ETag 是数据的 MD5 摘要。

对象分片上传

对象的分片上传优势

使用分段上传可提供以下优势:

  • 提高吞吐量 – 您可以并行上传分段以提高吞吐量。
  • 从任何网络问题中快速恢复 – 较小的分段大小可以将由于网络错误而需重启失败的上传所产生的影响降至最低。
  • 暂停和恢复对象上传 – 您可以在一段时间内逐步上传对象分段。启动分段上传后,不存在过期期限;您必须显式地完成或停止分段上传。
  • 在您知道对象的最终大小前开始上传 – 您可以在创建对象时将其上传。

建议您按以下方式使用分段上传:

  • 如果您在稳定的高带宽网络上传大型对象,请通过并行分段上传对象实现多线程上传,使用分段上传以充分利用您的可用带宽。
  • 如果您在断点网络中上传对象,请使用分段上传以提高应对网络错误的复原能力,从而避免重新上传。在使用分段上传时,您只需重新尝试上传在上传期间中断的部分即可。而无需从头重新开始上传对象。

分段上传流程

分段上分为三个步骤:开始上传、上传对象分段,以及在上传所有分段后完成分段上传。

分段上传开始

当您发送请求以开始分段上传时,Amazon S3 将返回具有上传 ID 的响应,此 ID 是分段上传的唯一标识符。无论您何时上传分段、列出分段、完成上传或停止上传,您都必须包括此上传 ID。如果您想要提供描述已上传的对象的任何元数据,必须在请求中提供它以开始分段上传。

分段上传

上传分段时,除了指定上传 ID,还必须指定分段编号。
您可以选择 1 和 10000 之间的任意分段编号。
分段编号在您正在上传的对象中唯一地识别分段及其位置。
您选择的分段编号不必是连续序列(例如,它可以是 1、5 和 14)。
如果您使用之前上传的分段的同一分段编号上传新分段,则之前上传的分段将被覆盖。

无论您何时上传分段,Amazon S3 都将在其响应中返回实体标签 (ETag) 标头。
对于每个分段上传,您必须记录分段编号和 ETag 值。
您必须在随后的请求中包括这些值以完成分段上传。

分段上传完成

完成分段上传时,Amazon S3 通过按升序的分段编号规范化分段来创建对象。
如果在开始分段上传请求中提供了任何对象元数据,则 Amazon S3 会将该元数据与对象相关联。
成功完成请求后,分段将不再存在。

完成分段上传请求必须包括上传 ID 以及分段编号和相应的 ETag 值的列表。
Amazon S3 响应包括可唯一地识别组合对象数据的 ETag。
此 ETag 无需成为对象数据的 MD5 哈希。

分段上传调用示例

对于此示例,假设您正在为一个 100 GB 的文件生成分段上传。在这种情况下,您应在整个过程中进行以下 API 调用。总共将有 1002 个 API 调用。

  • 一个用于启动该过程的 CreateMultipartUpload 调用。

  • 1000 个单独的 UploadPart 调用,每次上传 100 MB 的一部分,总大小为 100 GB。

  • 一个用于完成该过程的 CompleteMultipartUpload 调用。

分段上传列表

您可以列出特定分段上传或所有正在进行的分段上传的分段。
列出分段操作将返回您已为特定分段上传而上传的分段信息。
对于每个列出分段请求,Amazon S3 将返回有关特定分段上传的分段信息,最多为 1000 个分段。
如果分段上传中的分段超过 1000 个,您必须发送一系列列出分段请求以检索所有分段。
请注意,返回的分段列表不包括未完成上传的分段。
使用列出分段上传 操作,您可以获得正在进行的分段上传的列表。

正在进行的分段上传是已开始但还未完成或停止的上传。
每个请求将返回最多 1000 个分段上传。
如果正在进行的分段上传超过 1000 个,您必须发送其他请求才能检索剩余的分段上传。
仅使用返回的列表进行验证。
发送完成分段上传 请求时,请勿使用此列表的结果。
相反,当上传分段和 Amazon S3 返回的相应 ETag 值时,请保留您自己的指定分段编号的列表。

使用分段上传操作的校验和

在将对象上传到 Amazon S3 时,可指定校验和算法以供 Amazon S3 使用。默认情况下,Amazon S3 使用 MD5 来验证数据完整性;但是,您可以指定要使用的其他校验和算法。使用 MD5 时,Amazon S3 会在上传完成后计算整个分段对象的校验和。此校验和不是整个对象的校验和,而是每个分段的校验和的校验和。

当您指示 Amazon S3 使用其他校验和时,Amazon S3 会计算每个分段的校验和值并存储这些值。您可以使用 API 或 SDK 通过 GetObject 或 HeadObject 来检索单个分段的校验和值。

并发分段上传操作

在分布式开发环境中,您的应用程序可以同时在同一对象上开始多个更新。您的应用程序可能会使用同一对象键开始多个分段上传。然后,对于其中每个上传,您的应用程序可以上传分段并将完成上传请求发送到 Amazon S3,以创建数据元。当存储桶启用了 S3 版本控制时,完成分段上传将始终创建一个新版本。对于未启用版本控制的存储桶,在开始分段上传和完成分段上传期间接收的某些其他请求可能会优先开始。

注意

在开始分段上传和完成分段上传期间接收的某些其他请求可能会优先开始。例如,如果在使用键开始分段上传之后,但在完成分段上传之前其他操作删除了该键,则完成分段上传响应可能表示在未看到对象的情况下即成功创建了对象。

分段所需权限

方法列表:

开始分段上传
上传分段
上传分段(复制)
完成分段上传
中止分段上传
列出分段
列出分段上传

所需权限:

s3:PutObject
s3:AbortMultipartUpload
s3:ListMultipartUploadParts

预签名

默认情况下,所有对象和存储桶都是私有的。但是,您可以使用预签名 URL 选择性地共享对象,或者允许客户/用户将对象上传到存储桶,而无需 AWS 安全凭证或权限。

您可以使用预签名 URL 生成可用于访问 Amazon S3 存储桶的 URL。创建预签名 URL 时,将其与特定操作相关联。您可以共享 URL,任何有权访问该 URL 的人都可以像原始签名用户一样执行嵌入在 URL 中的操作。URL 在达到其过期时间后会过期且不再起作用。

预签名作用

可以共享对象、上传对象、删除对象

分片上传使用预签名 URL

预签名 URL 允许您访问在 URL 中识别的对象,条件是预签名 URL 的创建者拥有访问该对象的权限。即,如果您收到用于上传对象的预签名 URL,只要该预签名 URL 的创建者拥有上传该对象所需的权限,您即可上传对象。

默认情况下,所有的对象和存储桶都是私有的。如果您希望您的用户/客户能够将特定对象上载到您的存储桶,但您不要求他们拥有 AWS 安全凭证或权限,那么预签名 URL 将非常有用。

创建预签名 URL 时,您必须提供安全凭证,然后指定一个存储桶名称、一个对象键、一个 HTTP 方法(对上传对象执行 PUT 操作)和一个截止日期和时间。预签名 URL 仅在指定的持续时间内有效。也就是说,必须在到期日期和时间之前启动操作。

如果操作由多个步骤构成(例如分段上传),则所有步骤必须在到期前启动。否则,当 Amazon S3 尝试使用失效的 URL 启动步骤时,您将收到错误消息。

在到期日期和时间之前,可以多次使用预签名 URL。

对象分片上传示例

纯后端的分片上传示例

package main

import (
	"bytes"
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"net/http"
	"os"
	"time"
)

var (
	accessKey = "xxx"
	secretKey = "xxx"
	region    = "oss-cn-beijing"
	endpoint  = "oss-cn-beijing.aliyuncs.com"
)

func main() {
	//只要不修改session,session就可以安全的并发使用。
	sess, err := session.NewSession(&aws.Config{
		Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
		Endpoint:    aws.String(endpoint),
		Region:      aws.String(region),
		//minio:true,oss:false
		S3ForcePathStyle: aws.Bool(false),
		//SDK 支持使用客户端 TLS 证书配置的环境和会话选项,这些证书作为客户端 TLS 握手的一部分发送以进行客户端身份验证。
		//如果使用,则需要 Cert 和 Key 值。如果缺少一个,或者无法加载文件的内容,则会返回一个错误。
		//ClientTLSCert:              nil,
		//ClientTLSKey:               nil,
	})
	if err != nil {
		panic(err)
	}

	svc := s3.New(sess)
	//本地文件 5.jpg
	filename := "5.jpg"
	var chunkSize int64 = 26214400 //25M
	name, size := getFileInfo(filename)
	var bucket = "bkt-bj1"
	//	分片上传
	PartsUpload(svc, bucket, name, size, chunkSize)

}

//先获取文件名称和大小
func getFileInfo(filename string) (string, int64) {
	//文件操作
	file, err := os.Open(filename)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			panic(err)
		}
	}()
	fileInfo, _ := file.Stat()

	return filename, fileInfo.Size()
}
func PartsUpload(svc *s3.S3, bucket, key string, size, chunkSize int64) {
	oneDay := 86400

	//文件操作
	file, err := os.Open(key)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			panic(err)
		}
	}()
	buffer := make([]byte, size)
	fileType := http.DetectContentType(buffer)
	if _, err := file.Read(buffer); err != nil {
		panic(err)
	}

	//给分片上传1天的过期时间
	expires := time.Now().Add(time.Duration(oneDay) * time.Second)
	//创建分片
	res, err := svc.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
		Bucket:  &bucket,
		Key:     &key,
		Expires: &expires,
		//
		ContentType: aws.String(fileType),
	})
	if err != nil {
		panic(err)
	}
	var remainSize int64
	//获取分片个数
	chunkNum := size / chunkSize
	if size%chunkSize > 0 {
		chunkNum++
		remainSize = size % chunkSize
	}
	var completedParts []*s3.CompletedPart
	var i int64
	for ; i < chunkNum; i++ {
		partNum := i + 1
		partSize := chunkSize
		if partNum == chunkNum {
			if remainSize > 0 {
				partSize = remainSize
			}
		}
		fileBytes := buffer[chunkSize*i : chunkSize*i+partSize]
		//UploadPart=	req, out := c.UploadPartRequest(input)+req.Send()
		uploadResult, err := svc.UploadPart(&s3.UploadPartInput{
			Bucket:     &bucket,
			Key:        &key,
			PartNumber: aws.Int64(partNum),
			UploadId:   res.UploadId,
			//
			Body:          bytes.NewReader(fileBytes),
			ContentLength: aws.Int64(int64(len(fileBytes))),
		})
		if err != nil {
			panic(err)
		}
		completedParts = append(completedParts, &s3.CompletedPart{
			ETag:       uploadResult.ETag,
			PartNumber: aws.Int64(partNum),
		})
	}

	compResp, err := svc.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{
		Bucket:   &bucket,
		Key:      &key,
		UploadId: res.UploadId,
		//
		MultipartUpload: &s3.CompletedMultipartUpload{
			Parts: completedParts,
		},
	})
	if err != nil {
		panic(err)
	}
	fmt.Printf("compResp:%+v\n", compResp.String())
}

后端预签名,后端分片上传示例

package main

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/gofrs/uuid"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

var (
	accessKey = "xxx"
	secretKey = "xxx"
	region    = "oss-cn-beijing"
	endpoint  = "oss-cn-beijing.aliyuncs.com"
)

func main() {
	//只要不修改session,session就可以安全的并发使用。
	sess, err := session.NewSession(&aws.Config{
		Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
		Endpoint:    aws.String(endpoint),
		Region:      aws.String(region),
		//minio:true,oss:false
		S3ForcePathStyle: aws.Bool(false),
		//SDK 支持使用客户端 TLS 证书配置的环境和会话选项,这些证书作为客户端 TLS 握手的一部分发送以进行客户端身份验证。
		//如果使用,则需要 Cert 和 Key 值。如果缺少一个,或者无法加载文件的内容,则会返回一个错误。
		//ClientTLSCert:              nil,
		//ClientTLSKey:               nil,
	})
	if err != nil {
		panic(err)
	}

	svc := s3.New(sess)
	//本地文件
	filename := "6.jpg"
	var chunkSize int64 = 26214400 //25M
	name, size := getFileInfo(filename)
	var bucket = "bkt-bj1"
	//	分片上传预签名URL
	uploadCredential := PartsUploadPre(svc, bucket, name, size, chunkSize)
	completedPart := UploadByURLs(uploadCredential, name)
	//使用预签名URL上传并完成上传
	Comp(uploadCredential.CompleteURL, completedPart)
}

//先获取文件名称和大小
func getFileInfo(filename string) (string, int64) {
	//文件操作
	file, err := os.Open(filename)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			panic(err)
		}
	}()
	fileInfo, _ := file.Stat()

	return filename, fileInfo.Size()
}

// UploadCredential 返回给客户端的上传凭证
type UploadCredential struct {
	SessionID   string   `json:"sessionID"`
	ChunkSize   int64    `json:"chunkSize"` // 分块大小,0 为部分快
	Expires     int64    `json:"expires"`   // 上传凭证过期时间, Unix 时间戳
	UploadURLs  []string `json:"uploadURLs,omitempty"`
	UploadID    string   `json:"uploadID,omitempty"`
	CompleteURL string   `json:"completeURL,omitempty"`
}

//分片上传
func PartsUploadPre(svc *s3.S3, bucket, key string, size, chunkSize int64) *UploadCredential {
	var oneDay = 86400

	var uploadCredential = &UploadCredential{
		ChunkSize: chunkSize,
		//创建一个key回调用
		SessionID: uuid.Must(uuid.NewV4()).String(),
	}
	//回调有效期
	expires := time.Now().Add(time.Duration(oneDay) * time.Second)
	uploadCredential.Expires = expires.Unix()

	var urls []string
	//创建分片
	res, err := svc.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
		Bucket:  &bucket,
		Key:     &key,
		Expires: &expires,
	})
	if err != nil {
		panic(err)
	}
	//获取分片个数
	chunkNum := size / chunkSize
	if size%chunkSize > 0 {
		chunkNum++
	}
	var i int64
	for ; i < chunkNum; i++ {
		partNum := i + 1
		signedReq, _ := svc.UploadPartRequest(&s3.UploadPartInput{
			Bucket:     &bucket,
			Key:        &key,
			PartNumber: aws.Int64(partNum),
			UploadId:   res.UploadId,
		})
		//预签名一个一天的地址
		signedURL, err := signedReq.Presign(time.Duration(oneDay) * time.Second)
		if err != nil {
			panic(err)
		}
		urls = append(urls, signedURL)

	}

	signedReq, _ := svc.CompleteMultipartUploadRequest(&s3.CompleteMultipartUploadInput{
		Bucket:   &bucket,
		Key:      &key,
		UploadId: res.UploadId,
	})
	signedURL, err := signedReq.Presign(time.Duration(oneDay) * time.Second)
	if err != nil {
		panic(err)
	}
	uploadCredential.UploadURLs = urls
	uploadCredential.CompleteURL = signedURL
	uploadCredential.UploadID = *res.UploadId
	//该结构入库,并返回前端。待前端上传完文件,使用sessionID回调。后端根据sessionID查询记录做相应的db更新。
	return uploadCredential
}
func UploadByURLs(uploadCredential *UploadCredential, uploadKey string) []CompletedPart {
	chunkSize := int(uploadCredential.ChunkSize)
	//文件操作
	file, err := os.Open(uploadKey)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			panic(err)
		}
	}()
	fileInfo, _ := file.Stat()
	size := int(fileInfo.Size())
	buffer := make([]byte, size)
	//fileType := http.DetectContentType(buffer)
	if _, err := file.Read(buffer); err != nil {
		panic(err)
	}

	remainSize := 0 //当最后一片小于块大小时
	//获取分片个数
	chunkNum := size / chunkSize
	if size%chunkSize > 0 {
		//有余数,余数即最后一片的大小,非块大小
		chunkNum++
		remainSize = size % chunkSize
	}
	var completedPart []CompletedPart
	for i := 0; i < chunkNum; i++ {
		signedURL := uploadCredential.UploadURLs[i]
		partNum := i + 1
		partSize := chunkSize
		//最后一片的处理
		if partNum == chunkNum {
			if remainSize > 0 {
				partSize = remainSize
			}
		}
		fileBytes := buffer[chunkSize*i : chunkSize*i+partSize]
		//直接将文件内容传入请求体,不要form表单和任意的key
		req, err := http.NewRequest("PUT", signedURL, bytes.NewReader(fileBytes))
		if err != nil {
			panic(err)
		}
		res, _ := http.DefaultClient.Do(req)
		defer res.Body.Close()
		body, _ := ioutil.ReadAll(res.Body)

		fmt.Printf("-----i---------res:%+v\n", res.Header["Etag"])
		fmt.Printf("-----i---------body:%+v\n", string(body))
		completedPart = append(completedPart, CompletedPart{
			ETag:       res.Header["Etag"][0],
			PartNumber: int64(partNum),
		})
	}

	return completedPart
}

/**
 *  @Author: wangzhen
 *  @Date: 2022-07-19 08:53:36
 *  @Description:
 *  @param compURL
 *  @param completedPart
 */
func Comp(compURL string, completedPart []CompletedPart) {
	var byteXML CompleteMultipartUpload
	byteXML.Parts = completedPart
	marshal, err := xml.Marshal(byteXML)
	if err != nil {
		panic(err)
	}
	//<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>"30d7bfaaf99d52090061d7d7ef999dd8"</ETag></Part></CompleteMultipartUpload>
	//req, err := http.NewRequest("POST", compURL, bytes.NewReader([]byte(xml.Header+string(marshal))))
	req, err := http.NewRequest("POST", compURL, bytes.NewReader(marshal))
	if err != nil {
		panic(err)
	}
	compRes, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := compRes.Body.Close(); err != nil {
			panic(err)
		}
	}()
	body, err := ioutil.ReadAll(compRes.Body)
	if err != nil {
		panic(err)
	}
	fmt.Printf("comp Res:%+v\n", compRes)
	fmt.Printf("comp body:%+v\n", string(body))
	///成功返回格式:
	//<CompleteMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Location>http://127.0.0.1:9000/wzzz/02-qt%E7%AE%80%E4%BB%8B_-.mp4</Location><Bucket>wzzz</Bucket><Key>02-qt简介_-.mp4</Key><ETag>&#34;04273bd4d505a743dfa4250aff17b0e0-2&#34;</ETag></CompleteMultipartUploadResult>
	//错误返回格式:
	/*
		<Error><Code>MalformedXML</Code><Message>The XML you provided was not well-formed or did not validate against our published schema.</Message><Key>02-qt简介_-.mp4</Key><BucketName>wzzz</BucketName><Resource>/wzzz/02-qt简介_-.mp4</Resource><RequestId>1702DFDBB74A1B54</RequestId><HostId>2234a90c-d10d-4d52-a2bc-95a6b2e18b9e</HostId></Error>
	*/
}

type CompletedPart struct {
	ETag       string `type:"ETag"`
	PartNumber int64  `type:"PartNumber"`
}
type CompleteMultipartUpload struct {
	Parts []CompletedPart `xml:"Part"`
}

后端预签名,前端分片上传示例

后端分片预签名,
前端使用预签名URL上传文件分片,完成后通知后端完成分片上传。

运行后端代码,然后浏览器打开页面:
选择文件=>上传(我选择的文件时big1.mp4)
然后测试文件下载:
http://127.0.0.1:8111/download/bkt-bj1/big1.mp4

后端代码

package main

import (
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/gin-gonic/gin"
	"github.com/gofrs/uuid"
	"net/http"
	"time"
)

const (
	oneDay = 86400
)

var (
	accessKey = "xxx"
	secretKey = "xxx"
	region    = "oss-cn-beijing"
	endpoint  = "oss-cn-beijing.aliyuncs.com"
)

func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		origin := c.Request.Header.Get("Origin")
		if origin != "" {
			c.Header("Access-Control-Allow-Origin", origin)
			//主要设置Access-Control-Allow-Origin
			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
			c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
			c.Header("Access-Control-Allow-Credentials", "false")
			c.Set("content-type", "application/json")
		}
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
		c.Next()
	}
}

func main() {
	//只要不修改session,session就可以安全的并发使用。
	sess, err := session.NewSession(&aws.Config{
		Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
		Endpoint:    aws.String(endpoint),
		Region:      aws.String(region),
		//minio:true,oss:false
		S3ForcePathStyle: aws.Bool(false),
		//SDK 支持使用客户端 TLS 证书配置的环境和会话选项,这些证书作为客户端 TLS 握手的一部分发送以进行客户端身份验证。
		//如果使用,则需要 Cert 和 Key 值。如果缺少一个,或者无法加载文件的内容,则会返回一个错误。
		//ClientTLSCert:              nil,
		//ClientTLSKey:               nil,
	})
	if err != nil {
		panic(err)
	}
	bucket := "bkt-bj1"
	svc := s3.New(sess)
	var chunkSize int64 = 26214400 //25M
	r := gin.Default()
	r.Use(Cors())
	type UploadReq struct {
		Name string `json:"name"`
		Size int64  `json:"size"`
	}
	//上传请求
	r.POST("/uploadRequest", func(c *gin.Context) {
		var param UploadReq
		err := c.ShouldBindJSON(&param)
		if err != nil {
			panic(err)
		}
		fmt.Println("param:", param)
		name, size := param.Name, param.Size
		//name, size := AnalogFrontEnd() //模拟生成参数
		uploadCredential := PartsUpload(svc, bucket, name, size, chunkSize)
		c.JSON(200, uploadCredential)
	})
	type CompReq struct {
		Name      string          `json:"name"`
		SessionID string          `json:"sessionID"`
		UploadID  string          `json:"uploadID"`
		Parts     []CompletedPart `json:"part"`
	}
	//上传完成
	r.POST("/uploadComplete", func(c *gin.Context) {
		var param CompReq
		err := c.ShouldBindJSON(&param)
		fmt.Printf("uploadComplete param:%+v\n", param)
		if err != nil {
			panic(err)
		}
		//	模拟前端上传文件,分片+合并
		CompleteMulti(svc, bucket, param.Name, param.UploadID, param.Parts)
		c.JSON(200, param.Name)
	})

	type DownloadReq struct {
		Bucket string `uri:"bucket" binding:"required"`
		Key    string `uri:"key" binding:"required"`
	}
	//下载
	r.GET("/download/:bucket/:key", func(c *gin.Context) {
		var param DownloadReq
		err := c.ShouldBindUri(&param)
		if err != nil {
			panic(err)
		}
		fmt.Println("param:", param)
		c.Redirect(http.StatusFound, Download(svc, param.Bucket, param.Key))
	})
	r.Run(":8111")

}
func Download(svc *s3.S3, bucket, key string) string {
	//name := "02-qt简介_-.mp4"
	signedReq, _ := svc.GetObjectRequest(&s3.GetObjectInput{
		Bucket: &bucket,
		Key:    &key,
	})
	signedURL, err := signedReq.Presign(time.Duration(oneDay) * time.Second)
	if err != nil {
		panic(err)
	}
	fmt.Println("signedURL:", signedURL)

	return signedURL
}

// UploadCredential 返回给客户端的上传凭证
type UploadCredential struct {
	Name       string   `json:"name"`
	SessionID  string   `json:"sessionID"`
	ChunkSize  int64    `json:"chunkSize"` // 分块大小,0 为部分快
	Expires    int64    `json:"expires"`   // 上传凭证过期时间, Unix 时间戳
	UploadURLs []string `json:"uploadURLs"`
	UploadID   string   `json:"uploadID"`
}

func PartsUpload(svc *s3.S3, bucket, name string, size, chunkSize int64) *UploadCredential {
	var uploadCredential = &UploadCredential{
		Name:      name,
		ChunkSize: chunkSize,
		//创建一个key回调用
		SessionID: uuid.Must(uuid.NewV4()).String(),
	}
	//回调有效期
	expires := time.Now().Add(time.Duration(oneDay) * time.Second)
	uploadCredential.Expires = expires.Unix()

	var urls []string
	//创建分片
	res, err := svc.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
		Bucket:  &bucket,
		Key:     &name,
		Expires: &expires,
	})
	if err != nil {
		panic(err)
	}
	//获取分片个数
	chunkNum := size / chunkSize
	if size%chunkSize > 0 {
		chunkNum++
	}
	var i int64
	for ; i < chunkNum; i++ {
		partNum := i + 1
		signedReq, _ := svc.UploadPartRequest(&s3.UploadPartInput{
			Bucket:     &bucket,
			Key:        &name,
			PartNumber: aws.Int64(partNum),
			UploadId:   res.UploadId,
		})
		signedURL, err := signedReq.Presign(time.Duration(oneDay) * time.Second)
		if err != nil {
			panic(err)
		}
		urls = append(urls, signedURL)
	}

	uploadCredential.UploadURLs = urls
	uploadCredential.UploadID = *res.UploadId
	//该结构入库,并返回前端。待前端上传完文件,使用sessionID回调。后端根据sessionID查询记录做相应的db更新。
	return uploadCredential
}

type CompletedPart struct {
	ETag       string `json:"eTag"`
	PartNumber int64  `json:"partNumber"`
}

func CompleteMulti(svc *s3.S3, bucket, name, uploadID string, completedPart []CompletedPart) {
	input := &s3.CompleteMultipartUploadInput{
		Bucket:   &bucket,
		Key:      &name,
		UploadId: &uploadID,
		//
		MultipartUpload: &s3.CompletedMultipartUpload{
			Parts: make([]*s3.CompletedPart, 0, len(completedPart)),
		},
	}
	for _, part := range completedPart {
		input.MultipartUpload.Parts = append(input.MultipartUpload.Parts, &s3.CompletedPart{
			ETag:       &part.ETag,
			PartNumber: &part.PartNumber,
		})
	}

	if _, err := svc.CompleteMultipartUpload(input); err != nil {
		fmt.Println("CompleteMultipartUpload err:", err)
		panic(1)
	}
	///成功返回格式:
	//<CompleteMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Location>http://127.0.0.1:9000/wzzz/02-qt%E7%AE%80%E4%BB%8B_-.mp4</Location><Bucket>wzzz</Bucket><Key>02-qt简介_-.mp4</Key><ETag>&#34;04273bd4d505a743dfa4250aff17b0e0-2&#34;</ETag></CompleteMultipartUploadResult>
	//错误返回格式:
	/*
		<Error><Code>MalformedXML</Code><Message>The XML you provided was not well-formed or did not validate against our published schema.</Message><Key>02-qt简介_-.mp4</Key><BucketName>wzzz</BucketName><Resource>/wzzz/02-qt简介_-.mp4</Resource><RequestId>1702DFDBB74A1B54</RequestId><HostId>2234a90c-d10d-4d52-a2bc-95a6b2e18b9e</HostId></Error>
	*/
}


前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>分片文件上传</title>
    <h3>分片文件上传</h3>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<input type="file" name="file" id="file">
<button id="upload" onClick="upload()">上传</button>
<script type="text/javascript">
    // 设置上传成功数量记录
    successTotal = 0
    function upload() {
        var file = document.getElementById("file").files[0];
        console.log(file.name+"|"+file.size)
        var filename=file.name
        var filesize=file.size
        $.ajax({
            url: 'http://127.0.0.1:8111/uploadRequest',
            type: 'POST',
            async:false,  //使用同步的方式,true为异步方式

            cache: false,
            dataType : "json",
            data: JSON.stringify({
                "name":filename,
                "size":filesize,
            }),
            processData: false,
            contentType : "application/json",

            success:function (res){
                console.log("pre resp:",res)
                var chunkSize= res.chunkSize
               var sessionID= res.sessionID
                // res.expires
                // res.uploadURLs
               var uploadID= res.uploadID

               var remainSize = 0 //当最后一片小于块大小时
                //获取分片个数
               var  chunkNum  = filesize / chunkSize
                if (filesize%chunkSize > 0 ){
                    //有余数,余数即最后一片的大小,非块大小
                    chunkNum++
                    remainSize = filesize % chunkSize
                }
                var compData = new Object();
                compData["name"]=filename
                compData["sessionID"]=sessionID
                compData["uploadID"]=uploadID
                compData["part"]=[]


                for (var i=0;i<res.uploadURLs.length;i++){
                   var partNum=i+1;
                   var signedURL = res.uploadURLs[i]
                    var partSize = chunkSize
                    //最后一片的处理
                    if( partNum == chunkNum) {
                        if (remainSize > 0 ){
                            partSize = remainSize
                        }
                    }
                    var chunk = file.slice(chunkSize*i,chunkSize*i+partSize);//切割文件
                    // 使用ajax提交
                    $.ajax({
                        url: signedURL,
                        async:false,  //使用同步的方式,true为异步方式
                        type: 'put',
                        cache: false,
                        data: chunk,
                        processData: false,
                        contentType: false,
                        success:function (data, textStatus, request){
                            console.log("s3 resp:",data)
                            console.log("s3 getAllResponseHeaders:",request.getAllResponseHeaders())
                            console.log("s3 xhr.getResponseHeader(\"ETag\"):",request.getResponseHeader("ETag"))
                            compData["part"].push({
                                "eTag":request.getResponseHeader("ETag"),
                                "partNumber":partNum
                            })
                        }
                    }).done(function(data, textStatus, request){
                        console.log("s3d resp:",data)
                        console.log("s3d getAllResponseHeaders:",request.getAllResponseHeaders())
                        console.log("s3d xhr.getResponseHeader(\"ETag\"):",request.getResponseHeader("ETag"))

                    }).fail(function(data, textStatus, request) {
                        console.log("s3f resp:",data)
                        console.log("s3f getAllResponseHeaders:",request.getAllResponseHeaders())
                        console.log("s3f xhr.getResponseHeader(\"ETag\"):",request.getResponseHeader("ETag"))


                    });
                }
                //    合并
                console.log("compData req:",compData)
                $.ajax({
                    url: "http://127.0.0.1:8111/uploadComplete",
                    type: 'post',
                    contentType : "application/json",
                    async:false,  //使用同步的方式,true为异步方式
                    dataType : "json",
                    cache: false,
                    data: JSON.stringify(compData),
                    processData: false,
                    success:function (res){
                        console.log("comp resp:",res)
                        alert("上传成功")
                    }
                }).done(function(res){
                    console.log("comp done:",res)

                }).fail(function(res) {
                    console.log("comp file:",res)

                });


            }
        }).done(function(res){
            console.log("pre down :",res)

        }).fail(function(res) {
            console.log("pre fail :",res)

        });





        // if (chunktotal == successTotal) {
        //     alert("上传成功")
        // } else {
        //     alert("上传失败")
        // }
    }


</script>
</body>
</html>
 类似资料: