8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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.

マルチパートアップロードなどという面倒な実装は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を発行するには下記のアクセス権限が発行元のユーザーやロールに必要なので事前に設定する必要があります。

{
    "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で苦しんでいける仲間を募集しています。

8
5
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
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?