LoginSignup
1
0

More than 1 year has passed since last update.

Goでのmultipart uploadの実装

Last updated at Posted at 2023-02-25

概要

S3に5GB以上のデータをアップロードする際は、multipart uploadを使う必要があります。Goで実装してみたコードや調査した情報を共有します。aws-sdk-go-v2を使っています。5MB未満のデータについては、multipart uploadは対応しておらず、PutObjectでアップロードする必要があるので注意してください。

multipart uploadの考え方

基本的にはどの言語で実装する際も、以下の手順になると思います。分割データのサイズを決めて、分割回数分、以下の手順の1〜4を行い、最後に手順5を行います。

  1. CreateMultipartUploadでmultipart uploadを開始し、Upload IDを取得します。このUpload IDは特定のmultipart uploadの全てのpartを関連付けるために使用されます。
  2. UploadPartで分割データをアップロードします。
  3. AbortMultipartUploadは手順2で失敗した場合に実行します。エラーが発生する前に、先にアップロードが成功していた分割データの削除を行います。
  4. CompletedPartにアップロードが完了した分割データの情報を格納します。
    分割数分、1〜4のループが終わった後に、以下の手順5を行います。
  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を行うか判断して、処理してくれると最高なんだけど。求めすぎかな。

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0