概要
S3に5GB以上のデータをアップロードする際は、multipart uploadを使う必要があります。Goで実装してみたコードや調査した情報を共有します。aws-sdk-go-v2を使っています。5MB未満のデータについては、multipart uploadは対応しておらず、PutObjectでアップロードする必要があるので注意してください。
multipart uploadの考え方
基本的にはどの言語で実装する際も、以下の手順になると思います。分割データのサイズを決めて、分割回数分、以下の手順の1〜4を行い、最後に手順5を行います。
-
CreateMultipartUpload
でmultipart uploadを開始し、Upload IDを取得します。このUpload IDは特定のmultipart uploadの全てのpartを関連付けるために使用されます。 -
UploadPart
で分割データをアップロードします。 -
AbortMultipartUpload
は手順2で失敗した場合に実行します。エラーが発生する前に、先にアップロードが成功していた分割データの削除を行います。 -
CompletedPart
にアップロードが完了した分割データの情報を格納します。
分割数分、1〜4のループが終わった後に、以下の手順5を行います。 -
CompleteMultipartUpload
で全ての分割データのアップロードが完了したことをS3に伝えます。S3はPart numberを元に、昇順にデータをつなぎ合わせて、新しいオブジェクトを作成します。
実装
分割データをforループで50MBずつアップロードしています。今回はやっていませんが、goroutineを使って分割データを並列にアップロードすれば、さらに高速化することができそうです。
import (
"bytes"
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
func multipartUpload(client *s3.Client, body []byte, uploadPath string, contType string, fileSize int64, bucket string) error {
input := &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(uploadPath),
ContentType: aws.String(contType),
}
resp, err := client.CreateMultipartUpload(context.TODO(), input)
if err != nil {
return err
}
var curr, partLength int64
var remaining = fileSize
var completedParts []types.CompletedPart
const maxPartSize int64 = int64(50 * 1024 * 1024)
partNumber := 1
for curr = 0; remaining != 0; curr += partLength {
if remaining < maxPartSize {
partLength = remaining
} else {
partLength = maxPartSize
}
partInput := &s3.UploadPartInput{
Body: bytes.NewReader(body[curr : curr+partLength]),
Bucket: resp.Bucket,
Key: resp.Key,
PartNumber: int32(partNumber),
UploadId: resp.UploadId,
}
uploadResult, err := client.UploadPart(context.TODO(), partInput)
if err != nil {
aboInput := &s3.AbortMultipartUploadInput{
Bucket: resp.Bucket,
Key: resp.Key,
UploadId: resp.UploadId,
}
_, aboErr := client.AbortMultipartUpload(context.TODO(), aboInput)
if aboErr != nil {
return aboErr
}
return err
}
completedParts = append(completedParts, types.CompletedPart{
ETag: uploadResult.ETag,
PartNumber: int32(partNumber),
})
remaining -= partLength
partNumber++
}
compInput := &s3.CompleteMultipartUploadInput{
Bucket: resp.Bucket,
Key: resp.Key,
UploadId: resp.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: completedParts,
},
}
_, compErr := client.CompleteMultipartUpload(context.TODO(), compInput)
if err != nil {
return compErr
}
return nil
}
まとめ
SDKの内部で、データの容量からmultipart uploadを行うか判断して、処理してくれると最高なんだけど。求めすぎかな。