ブラウザからAWS S3にマルチパートアップロードをする

## はじめに

Webアプリケーションでブラウザから直接S3にマルチパートファイルアップロードをする方法を備忘録的に残します。苦しんでる人の助けになればと思います。

動作する全てのコードはここにあります。
https://github.com/inunekousapon/s3_sts_upload_sample

## やること

- STSを利用したS3への直接アップロード
- マルチパートアップロード

## やらないこと

- マルチパートアップロードの自前実装
- 署名付きURLを利用したオブジェクトのアップロード

## ブラウザからS3に直接アップロードする方法

2つの方法があります。

- 署名付きURLアップロード  
- AWS Security Token Serviceを使った方法  

通常、署名付きアップロードを使うのが最適な選択肢になると思います。しかし、マルチパートアップロードを選択しようと思うと実装が難しいことがわかりました。

下記は署名付きURLを利用したマルチパートアップロードの記事です。
https://zenn.dev/kujime/articles/484b6e25d058fb

このSDKのissueでは署名付きURLを利用したマルチパートアップロードが使いづらいという点について話し合われています。（2021.08.06現在解決していません）
https://github.com/aws/aws-sdk-js/issues/1603


一方、AWSSDK for JavascriptにはManagedUploadというメソッドがあり、完全にマルチパートアップロードに対応しています。なぜ、署名付きURLアップロードに使えないのか理解に苦しみます。

> The managed uploader allows for easy and efficient uploading of buffers, blobs, or streams, using a configurable amount of concurrency to perform multipart uploads where possible. This abstraction also enables uploading streams of unknown size due to the use of multipart uploads.

https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3/ManagedUpload.html

マルチパートアップロードなどという面倒な実装はAWS公式のライブラリに任せたいので、「AWS Security Token Serviceを使った方法」を紹介したいと思います。


## コード

### サーバー側

```app.py
import os
import json

import boto3
from flask import Flask, render_template


app = Flask(__name__)


@app.route('/', methods=['GET'])
def upload_file():
    sts_connection = boto3.client('sts',
        aws_access_key_id=os.getenv('AWS_ACCESS_KEY'),
        aws_secret_access_key=os.getenv('AWS_SECRET_KEY'),
        region_name='ap-northeast-1',
    )
    b = sts_connection.get_federation_token(
        Name="Bob",
        DurationSeconds=900,
        Policy='{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:PutObject","Resource":"*"}]}'
    )
    access_key_id = b['Credentials']['AccessKeyId']
    secret_access_key = b['Credentials']['SecretAccessKey']
    session_token = b['Credentials']['SessionToken']
    return render_template('index.html',
        access_key_id=access_key_id,
        secret_access_key=secret_access_key,
        session_token=session_token,
        bucket_name='your bucket name',
        object_key='your object key',
    )
```

flaskによるサンプルです。
ブラウザで利用できる一時的な認証情報を取得するにはAsumeRoleまたはGetFederationTokenのどちらかを利用するといいと思います。今回はGetFederationTokenを利用しています。
（問題がある場合のご指摘お待ちしております）

get_federation_tokenのPolicyをより厳密に書くことで、アップロードしたいキーのみ許可するなど署名付きURLによるアップロードと同等のアクセス制御を作ることができます。

FederationTokenを発行するには下記のアクセス権限が発行元のユーザーやロールに必要なので事前に設定する必要があります。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "sts:GetFederationToken",
            "Resource": "*"
        }
    ]
}
```

### クライアント側

```index.html
<!doctype html>
<html>
<head></head>
<body>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <input id="upload" type="file">
    <button id="addFile" onclick="addFile()">
        アップロード
    </button>
    <div id="progress"></div>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.961.0.min.js"></script>
<script type="text/javascript">
function addFile() {
    var files = document.getElementById("upload").files;
    if(!files.length) {
        return alert("No file.");
    }
    var bucket = "{{ bucket_name }}";
    var file = files[0];
    var key = "{{ object_key }}";

    AWS.config.update({region: 'ap-northeast-1'});
    AWS.config.credentials = new AWS.Credentials(
        accessKeyId="{{access_key_id}}",
        secretAccessKey="{{ secret_access_key }}",
        sessionToken="{{ session_token　}}",
    )

    var promise = new AWS.S3.ManagedUpload({
        params: {
            Bucket: bucket,
            Key: key,
            Body: file
        }
    }).on('httpUploadProgress', function(evt) {
        var disp = "Uploaded :: " + parseInt((evt.loaded * 100) / evt.total)+'%';
        console.log(disp);
        document.getElementById('progress').innerText = disp;
    })
    .promise();
    promise.then(
        function(data) {
            alert("Successfully uploaded file.");
        },
        function(err) {
            return alert("There was an error uploading your file: ", err.message);
        }
    );
}
</script>
</body>
</html>
```

自前でマルチパートアップロードを書くよりかなり記述量が少なくなったかと思います。
'httpUploadProgress'のイベントでアップロードの進捗状況を取得することができます。

## まとめ

公式のライブラリにマルチパートアップロードする仕組みがあるから、それをなんとか使う方法を模索したほうが楽です。
セキュリティ的に盲点があるかもしれないため、問題点があるのならコメントがほしいです。
一緒にAWSで苦しんでいける仲間を募集しています。