大きなファイルをS3にawscliでMultipart Upload

S3にPUTするときの最大サイズは5GBだそうです。これを超えるサイズをアップロードする場合にはMultipart Uploadが必要です。

aws s3 cpコマンドでは大きいファイルをアップロードする際には自動でMultipart Uploadになりますが、Multipart Uploadの処理の中身を理解するために、aws s3apiコマンドで手動で動かしてみました。

手順概要

  1. aws s3api create-multipart-uploadコマンドでMultipart Upload開始を宣言し、UploadIdを取得
  2. aws s3api upload-partコマンドで分割したファイルをアップロード。分割した数だけこのコマンドを実行
    • UploadIdは全部同じものを指定
    • 1から始まる整数をパーツの番号として指定
    • パートごとにETagが返却されるので、それを記録しておく
  3. aws s3api complete-multipart-uploadコマンドでUploadIdとETagのリストを渡すことで完了

手順詳細

手順をBashスクリプトにしたものを載せておきます。

set -Ceu
set -o pipefail

# アップロード先を指定するパラメータ
profile=default
bucket=SAMPLE_BUCKET
key=movies/sample.mov

# アップロードするローカルにあるサンプルファイル
localfile=sample.mov

# 分割したファイルを保存する一時ディレクトリ
mkdir -p parts

# part 1つあたりのサイズ
partsize=6000000
# AWSの仕様によりpart 1つあたりのサイズは5MB以上が必要です

# ファイルサイズから分割数を計算
part_count=$(( ($(ls -l sample.mov | awk '{print $5}') + $partsize - 1) / $partsize))

################################
# 手順1
################################

# UploadId を取得
upload_id=$(aws --profile $profile s3api create-multipart-upload --bucket $bucket --key $key --query UploadId --output text)

################################
# 手順2
################################

# 分割したファイルを順番に aws s3api upload-part コマンドによりアップロード。
# 手順3で必要なJSONファイルも同時に作成します。

echo '{"Parts":[' >| parts.json

for i in $(seq $part_count); do # 1から分割数までをループ
    echo $i

    # アップロードするファイルの$i番目のpartを抜き出す
    ((cat $localfile | tail -c+$(( ( $i - 1) * $partsize + 1 ))) || true) | head -c $partsize >| parts/$i
    # headがあるとその前のcatやtailは異常終了してしまいますが、
    # 冒頭で set -e しているので、スクリプトが中断しないように true を書いています。
    # シンプルに書くと$iが2ならば次のようなコマンドです。
    # cat $localfile | tail -c+6000001 | head -c 12000000 > parts/2

    echo -n '{"ETag":' >> parts.json

    # part 1つをアップロード
    # ETagが出力されるので、そのままJSONに書き出す
    aws --profile $profile s3api upload-part --bucket $bucket --key $key --part-number $i --body parts/$i --upload-id $upload_id --query ETag --output text >> parts.json

    # JSONに書き出すPartNumberは1から始まる連番
    echo -n ', "PartNumber":'$i'}' >> parts.json
    if [[ $i < $part_count ]]; then
        echo ',' >> parts.json
    else
        echo >> parts.json
    fi
done

echo ']}' >> parts.json

# parts.json には以下のようなJSONが書き出されます
# {"Parts":[
#     {"ETag":"03c58c6387cd642d23657231feb1044f", "PartNumber":1},
#     {"ETag":"d18f4e61324478f2b47f907e2b1367b3", "PartNumber":2},
#     {"ETag":"52e280adbaa9afcfd30d071255a5b452", "PartNumber":3},
#     {"ETag":"144e843c6ad29b05f5faedaf464a3a9a", "PartNumber":4},
#     {"ETag":"2251deec7a0d3bad59df68114b18d27d", "PartNumber":5}
# ]}

################################
# 手順3
################################

# ETagのリストをJSONで渡して完了
# これをするまでは aws s3 ls コマンドで見てもアップロード中のファイルは見えない
aws --profile $profile s3api complete-multipart-upload --bucket $bucket --key $key --upload-id $upload_id --multipart-upload file://parts.json

# aws s3api complete-multipart-uploa コマンドは以下のようなレスポンスをします
# {
#     "VersionId": "Z4epmUvPw5tDhUd5ctQ6hqtbJRABcND8",
#     "Location": "https://SAMPLE_BUCKET.s3.ap-northeast-1.amazonaws.com/movies%2Fsample.mov",
#     "Bucket": "SAMPLE_BUCKET",
#     "Key": "movies/sample.mov",
#     "ETag": "\"af82619f75aff5484a77ab040f516057-2\""
# }

メモ

分割サイズ

上記スクリプトでは6000000バイト(6MB弱)ずつに分割しています。分割サイズが5MBを下回ると次のようなエラーになってしまいます。

An error occurred (EntityTooSmall) when calling the CompleteMultipartUpload operation: Your proposed upload is smaller than the minimum allowed size

最後のパートだけはサイズが小さくても大丈夫です。どうしようもないですからね。

参考 UploadPart - Amazon Simple Storage Service

list-parts コマンド

手順2のあと以下のようなコマンドを実行すると

aws --profile $profile s3api list-parts --bucket $bucket --key $key --upload-id $upload_id

このようなJSONが出力されます。

{
    "Parts": [
        {
            "PartNumber": 1,
            "LastModified": "2021-02-02T06:05:03.000Z",
            "ETag": "\"5ec31e0a715293b7512d178890908310\"",
            "Size": 6000000
        },
        {
            "PartNumber": 2,
            "LastModified": "2021-02-02T06:05:04.000Z",
            "ETag": "\"9b41c50e2d76cc7959f07cffbafaedd6\"",
            "Size": 6000000
        },
        ...
    ],
    "Initiator": {
        ...
    },
    "Owner": {
        ...
    },
    "StorageClass": "STANDARD"
}

手抜きして、これをそのまま手順3のaws s3api complete-multipart-uploadコマンドに渡してしまおうかと思ったのですが、aws s3api complete-multipart-uploadコマンドはETagPartNumberのみが必要で、それ以外の要素がJSONに含まれるとエラーになってしまいました。

それにaws s3api list-partsコマンドのレスポンスにあるETagはなぜか値自体にダブルクオーテーションが含まれていました。

Pythonのboto3で試すと

同じことをPythonのboto3で試したら、aws s3api list-partsコマンドと同じく、ETagの値自体にダブルクオーテーションが含まれており、手順3に相当する箇所でもダブルクオーテーションを含めたままで動作しました。違和感・・・