S3にPUTするときの最大サイズは5GBだそうです。これを超えるサイズをアップロードする場合にはMultipart Uploadが必要です。
aws s3 cpコマンドでは大きいファイルをアップロードする際には自動でMultipart Uploadになりますが、Multipart Uploadの処理の中身を理解するために、aws s3apiコマンドで手動で動かしてみました。
手順概要
aws s3api create-multipart-uploadコマンドでMultipart Upload開始を宣言し、UploadIdを取得aws s3api upload-partコマンドで分割したファイルをアップロード。分割した数だけこのコマンドを実行- UploadIdは全部同じものを指定
- 1から始まる整数をパーツの番号として指定
- パートごとにETagが返却されるので、それを記録しておく
aws s3api complete-multipart-uploadコマンドでUploadIdとETagのリストを渡すことで完了
手順詳細
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コマンドはETagとPartNumberのみが必要で、それ以外の要素がJSONに含まれるとエラーになってしまいました。
それにaws s3api list-partsコマンドのレスポンスにあるETagはなぜか値自体にダブルクオーテーションが含まれていました。
Pythonのboto3で試すと
同じことをPythonのboto3で試したら、aws s3api list-partsコマンドと同じく、ETagの値自体にダブルクオーテーションが含まれており、手順3に相当する箇所でもダブルクオーテーションを含めたままで動作しました。違和感・・・