Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Go】aws-sdk-go-v2でio.Seeker未実装streamを使用してS3 objectをuploadする

aws-sdk-go-v2 でS3にオブジェクトをアップロードするには PutObject が利用できます。 引数となる PutObjectInputBody fieldに、アップロードしたいオブジェクトコンテンツを io.Reader で渡すことができます。 例えば、下記例では bytes.Buffer 型の値を渡しています。

input := &s3.PutObjectInput{
    Bucket:        aws.String(bucketName),
    Key:           aws.String(key),
    Body:          bytes.NewBuffer(b) // b : object binary
}

resp, err := client.PutObject(ctx, input)
if err != nil {
    return err
}

ただしアップロードは失敗し、下記のようなerrorが返ってきます。

operation error S3: PutObject, failed to compute payload hash: failed to seek body to start, request stream is not seekable

error文言からは PutObjectペイロードのハッシュを計算するため、 Bodyio.Seeker を実装したstreamを渡さなければいけないことがわかります。 bytes.BufferSeek(offset int64, whence int) (int64, error) を実装していないので失敗します。

// Seeker is the interface that wraps the basic Seek method.
//
// Seek sets the offset for the next Read or Write to offset,
// interpreted according to whence:
// SeekStart means relative to the start of the file,
// SeekCurrent means relative to the current offset, and
// SeekEnd means relative to the end.
// Seek returns the new offset relative to the start of the
// file and an error, if any.
//
// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset is legal, but the behavior of subsequent
// I/O operations on the underlying object is implementation-dependent.
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

このことは公式ドキュメントでも言及されています。

aws.github.io

os.File など io.Seeker 実装型の値で渡せば問題ないのですが、 他の io.Reader で渡したい場合、明示的にコンテンツサイズを計算して渡してあげる必要があります。 具体的には

  1. PutObjectInput に、ContentLength を設定する
  2. PutObject 第三実引数に、 SwapComputePayloadSHA256ForUnsignedPayloadMiddleware を渡した WithAPIOptions() 呼び出しを設定する

とします。

input := s3.PutObjectInput{
    Bucket:        aws.String(bucketName),
    Key:           aws.String(key),
    Body:          bytes.NewBuffer(b),
    ContentLength: int64(len(b)), // 1.
}

resp, err := client.PutObject(ctx, input, s3.WithAPIOptions(
    v4.SwapComputePayloadSHA256ForUnsignedPayloadMiddleware, // 2.
))
if err != nil {
    return err
}

または、下記のようにUpload Managerを使用するとペイロードハッシュのことを考慮せずとも io.Reader を渡せます。 ただしこちらの方法を使用すると、PutObject APIではなくマルチパートアップロードが実行されるので注意です。

uploader := manager.NewUploader(client)
_, err = uploader.Upload(ctx, &s3.PutObjectInput{
    Bucket: aws.String(bucketName),
    Key:    aws.String(key),
    Body:   bytes.NewBuffer(b),
})

aws.github.io

module version

github.com/aws/aws-sdk-go-v2 v1.11.1
github.com/aws/aws-sdk-go-v2/config v1.10.2
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.19.1

参考文献

aws.github.io

docs.aws.amazon.com