Web サイトで選択した画像を multipart/form-data で POST して Amazon S3 に保存する！！

#やりたかったこと
Web サイトで画像ファイルを選択した後、アップロードボタンでその画像を multipart/form-data で POST し、 AWS のS3 に保存したかったので方法を考えました。Web サイトで画像の保存を含めた、AWS の サービス を使用したい場合に参考になるかと思います。
#全体の構成図
<img width="986" alt="スクリーンショット 2020-03-06 13.25.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/95797899-ebca-3134-42c3-4b0ad96f23c1.png">

#Web サイトのイメージ
<img width="819" alt="スクリーンショット 2020-03-06 11.11.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/0acb777e-5717-64a2-a107-b0a0fa31e11c.png">

#使用した開発言語とサービス
Web 側の開発言語として **HTML、CSS、JavaScript** を、Lambda 側での言語として **Python 3.7**を使用しています。
以下が使用したサービスになります。

**・AWS Lambda**
 イベントをトリガーとして、関数コードを実行することができる AWS のサービスです。
**・Amazon S3 (アップロードした画像を保存する用と、Web サイトを公開する場所として使う)**
 Simple Storage Service の略で、AWS のオンラインストレージサービス

Web 側で画像を multipart/form-data の形で、 ajax を使用して POST した後、API Gateway を叩くことで　Lambda　を起動し、Lambda 内の関数で画像を S3 に保存します。おまけとして保存した画像のファイル名を Webサイトのローカルストレージに格納して、保存した画像を参照できるようにします。

#全体の作成手順
1. [Web サイトの作成](#web-サイトの作成)
2. [S3 に画像保存用の Bucket 作成](#s3-に画像保存用の-bucket-作成)
3. [Lambda を作成(API Gatewayを含む)](#lambda-を作成api-gatewayを含む)

という手順になります。

#Web サイトの作成
##Web サイト側のコード全文
今回は Web サイト側から multipart/form-data の形式で API に POST する形をとりました。submit ボタンクリック時に別画面に遷移しないようにする処理や、各種ボタンのスタイル変更なども行なっています。

```index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!--    jquery のライブラリ は固定ではない-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> 
    <title>Title</title>
    <style>
        label > input {
            display:none; /* ファイルボタンのスタイルを無効にする */
        }
        label > button {
            display:none; /* ボタンのスタイルを無効にする */
        }

        label {
            display: inline-block;
            padding: 0.5em 1em;
            text-decoration: none;
            background: #fd9535;/*ボタン色*/
            color: #FFF;
            border-bottom: solid 4px #627295;
            border-radius: 3px;
        }
        button{
            display: inline-block;
            padding: 0.5em 1em;
            text-decoration: none;
            background: #fd9535;/*ボタン色*/
            color: #FFF;
            border-bottom: solid 4px #627295;
            border-radius: 3px;
        }
        label:active {
            /*ボタンを押したとき*/
            -webkit-transform: translateY(4px);
            transform: translateY(4px);/*下に動く*/
            border-bottom: none;/*線を消す*/
        }
        button:active{
            /*ボタンを押したとき*/
            -webkit-transform: translateY(4px);
            transform: translateY(4px);/*下に動く*/
            border-bottom: none;/*線を消す*/
        }
    </style>
</head>
<body>
<form action="xxx APIGateway のURLを入力する場所 xxx" method="post" enctype="multipart/form-data" id="imgForm"  target="sendPhoto">
    <p>
        <label for ="upfile">
            ファイルを選択してください
            <input type="file" name="fileName" id="upfile" accept="image/*" capture="camera">
        </label>
        <span style="color: #ff0000;" data-mce-style="color: #ff0000;"><div><img id="thumbnail" src=""></div></span>
    </p>
    <p>
        <label for="upload">
            アップロード
            <button type="submit" action="" name="save" id="upload" >upload</button>
        </label>
    </p>
    <p id="compUpload"></p>
</form>
<iframe name="sendPhoto" style="width:0px;height:0px;border:0px;"></iframe>
<script>
$('#upfile').change(function(){
if (this.files.length > 0) {
// 選択されたファイル情報を取得
var file = this.files[0];
// readerのresultプロパティに、データURLとしてエンコードされたファイルデータを格納
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
$('#thumbnail').attr('src', reader.result );
}
}
});

// 画像をアップロード
    $('#imgForm').on('submit', function(e) {
        e.preventDefault();
        var formData = new FormData($('#imgForm').get(0));
        $.ajax($(this).attr('action'), {
            type: 'post',
            processData: false,
            contentType: false,
            data: formData,
            success: document.getElementById('compUpload').innerHTML = 'アップロード中' // 送信に成功したとき

    }).done(function(response){
        let jsonbody = JSON.parse(response.body);
            console.log('succes!');  // レスポンスがあったとき
            //ローカルストレージにレスポンスのファイル名を格納
            var array = [];
            var obj = {
                'キー1': '値1',
                'キー2': '値2'
            };
            array.push(obj);
            var setjson = JSON.stringify(obj);
            localStorage.setItem('キー', jsonbody.message);
            document.getElementById('compUpload').innerHTML = 'アップロード完了'

    }).fail(function() {
            console.log('error!'); // エラーが発生したとき
        });
        return false;
    });
</script>
</body>
</html>

```

上のコードをコピペで、html ファイルを作成してください。そのファイルを お好きなサーバーか、S3 に配置して表示してください。これで、Web 側の作成は "API Gateway の URL 指定"をのぞいて完了です。
###API Gateway の URL 指定
下記のコードが指定部分
```html:
<body>
<form action="xxx APIGateway のURLを入力する場所 xxx" method="post" enctype="multipart/form-data" id="imgForm"  target="sendPhoto">
```
ここは のちの Lambda を作成する手順で API Gateway の URL がわかった時点で追加します。
またデフォルトのボタンが味気なかったのでコード上部の style タグでデザインを変更しています。デフォルトの表示をdisplay:none; で非表示にした上で新しいデザインに変更しています。

```html:
<style>
        label > input {
            display:none; /* ファイルボタンのスタイルを無効にする */
        }
        label > button {
            display:none; /* ボタンのスタイルを無効にする */
        }

        label {
            display: inline-block;
            padding: 0.5em 1em;
            text-decoration: none;
            background: #fd9535;/*ボタン色*/
            color: #FFF;
            border-bottom: solid 4px #627295;
            border-radius: 3px;
        }
        button{
            display: inline-block;
            padding: 0.5em 1em;
            text-decoration: none;
            background: #fd9535;/*ボタン色*/
            color: #FFF;
            border-bottom: solid 4px #627295;
            border-radius: 3px;
        }
        label:active {
            /*ボタンを押したとき*/
            -webkit-transform: translateY(4px);
            transform: translateY(4px);/*下に動く*/
            border-bottom: none;/*線を消す*/
        }
        button:active{
            /*ボタンを押したとき*/
            -webkit-transform: translateY(4px);
            transform: translateY(4px);/*下に動く*/
            border-bottom: none;/*線を消す*/
        }
    </style>
```

##画像アップロード部分の説明
画像をアップロードするのに ajax 使用しています。サーバー側への POST が成功すると Web 側でアップロード中と表示されるようにしています。

```html:
<script>
// 画像をアップロード
    $('#imgForm').on('submit', function(e) {
        e.preventDefault();
        var formData = new FormData($('#imgForm').get(0));
        $.ajax($(this).attr('action'), {
            type: 'post',
            processData: false,
            contentType: false,
            data: formData,
            success: document.getElementById('compUpload').innerHTML = 'アップロード中' // 送信に成功したとき

    }).done(function(response){
        let jsonbody = JSON.parse(response.body);
            console.log('succes!');  // レスポンスがあったとき
            //ローカルストレージにレスポンスのファイル名を格納
            var array = [];
            var obj = {
                'キー1': '値1',
                'キー2': '値2'
            };
            array.push(obj);
            var setjson = JSON.stringify(obj);
            localStorage.setItem('キー', jsonbody.message);
            document.getElementById('compUpload').innerHTML = 'アップロード完了'

    }).fail(function() {
            console.log('error!'); // エラーが発生したとき
        });
        return false;
    });
</script>
```

#S3 に画像保存用の Bucket 作成
Web から画像ファイルをアップロードする場所として S3 を使用します。AWS マネジメントコンソールから S3 を選択し、バケットを作成してください。この時指定するリージョンはのちの Lambda 作成時に指定するリージョンと同じにすることをおすすめします。(別のリージョンにすると設定の反映に時間がかかることがあるため)⇨[参考リンク](https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response)
作成時の設定はデフォルトで問題ありませんが、この記事では アクセス許可の設定で'パブリックアクセスを全てブロック'のチェックを外し、パブリックアクセスを許可する設定にします。（Lambda でのレスポンスをウェブに表示させるため）　個別にアクセス許可を設定したい場合作成後のバケットポリシーから設定してください。
![スクリーンショット 2020-03-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/f807018a-d618-ec8c-a968-436fea5e5a1e.png)


ここで作成したバケットのパスを後々 Lambda で指定することで、アップロードした画像が落ちることになります。

#Lambda を作成(API Gatewayを含む)
##Lambda 関数の作成
Lambda では Web 側での画像 POST をトリガーとして、S3 に画像を保存する関数を走らせます。そのためにまず AWS マネジメントコンソールから Lambda を選択し、 関数の作成をクリックします。作成の際、この記事ではバージニア北部のリージョンで作成しています。(S3 で作成したリージョンと同じにすることを推奨)

![スクリーンショット 2020-03-09-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/63366c2b-5b7e-9abd-aa95-70e50c5b3454.png)

関数の作成画面で、
・一から作成
・関数名（各々好きな関数名を入れてください）
・ランタイム（今回はPython 3.7を使用）
・実行ロール（基本的な Lambda アクセス権限で新しいロールを作成を選択）

これらを決定した後関数の作成をクリックします。ここでは関数名は myFunctionName としています。
実行ロールでは S3 と Cloud Watch へのアクセスを許可しますが、まずは基本の Lambda アクセス権限で作成し、後の手順で追加します。
<img width="1084" alt="スクリーンショット 2020-03-09 11.18.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/7d9d377d-2004-fb66-abd7-187d3d43b7ab.png">

##API Gateway の作成
関数作成が完了したら関数の編集画面が開くので、Designer ウィンドウの中にある **トリガーを追加** をクリックします。ウィンドウが切り替わるのでそこで **API Gateway** を選択し、細かい設定をしていきます。

![スクリーンショット 2020-03-09 11.46..png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/199ce548-1a08-8af5-ee67-205725567991.png)

トリガーを追加の画面ではそれぞれのドロップダウンボックスとラジオボタンの選択肢で
・API ⇨ 新規 API の作成
・テンプレートを選択 ⇨ REST API
・セキュリティ ⇨ オープン
・追加の設定をクリック
　⇨バイナリメディアタイプに **image/png**、 **multipart/form-data** を追加
その後画面右下の　**追加** をクリックしてください。

<img width="958" alt="スクリーンショット 2020-03-11 17.30.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/3c09fa8a-108f-21c2-8e50-ebbedfcb44ad.png">
<img width="958" alt="スクリーンショット 2020-03-11 17.32.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/6044edd5-b46f-4730-e1bc-91ab2e12c026.png">

そうすると下の画像のように Designer に API Gateway が追加されているかと思います。これは API Gateway が Web 側の POST を受け取り、それをトリガーに Lambda 関数が動き出すという仕組みです。

![スクリーンショット 2020-03-09 13.54..png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/ffbb9148-c62c-eaae-d615-839abfd49c97.png)

Designer で API Gateway をクリックすると、https から始まる **API エンドポイント**が表示されるので、この URL を [Web サイト作成の手順で不確定だった部分](#下記のコードが-url-指定部分)に入力します。

![スクリーンショット 2020-03-09 14.50.2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/aa1347c3-32c2-a3aa-ca1a-8f6dc50f742f.png)

次にAPI エンドポイントの真上に表示されている　"関数名-API"　のリンクをクリックしAPI のメソッドを作成していきます。
![スクリーンショット 2020-03-09 14.50.26.mo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/3f4e4f2f-fbff-20b2-f579-5100dba1534a.png)

##API Gateway POST メソッドの設定
"関数名-API"　のリンクをクリックすると API Gateway の設定のページが開きます。
ここで Web サイト から json 形式のファイルを受け取れるように API POST メソッドを設定します。

1. アクションから**メソッドの作成**をクリックし、セレクトボックスを表示させる（関数名の右にある折りたたみの矢印をクリック）
2. セレクトボックスから **POST** を選択し、チェックマークをクリック
3. POST を選択し、表示される画面で使用している Lambda リージョンを選択した後 **Lambda 関数**の入力欄に先ほど作成した関数名を入力し保存をクリック
4. Lambda 関数に権限を追加のウィンドウが表示される場合OKをクリック
5. アクションから CORS の有効化を選択し、アドバンスドの Access-Control-Allow-Credentials に 'true' と入力(''も含める)し、CORS を有効にして既存の CORS ヘッダーを置換をクリックし値を追加
6. **POST** を再び選択し、メソッドの実行画面で**統合リクエスト**をクリックし、マッピングテンプレート⇨テンプレートが定義されていない場合 (推奨)⇨マッピングテンプレートの追加⇨ **multipart/form-data** を入力⇨テンプレートの生成で **メソッドリクエストのパススルー** を選択し保存

**※６追記.** POST - メソッドレスポンスに **Access-Control-Allow-Credentials** のヘッダーを手動で追加し、POST - 統合レスポンスの Access-Control-Allow-Credentials のマッピングの値に **'true'**を追加してください。 この手順を省くと CORS エラーが出る可能性があります。

７. 保存ボタンをクリック後、ANY の上の**関数名**を選択し、アクションから API のデプロイを選択し、デプロイされるステージに default を選択後デプロイボタンをクリック

これで API の設定は終了です。手順が多く間違いやすい部分なので注意してください。
次は関数コードの設定に移るので、Lambda 関数 の設定画面に戻ってください。サービス ⇨ Lambda ⇨ 関数名で戻れます。



①
<img width="1435" alt="スクリーンショット 2020-03-10 10.20.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/64b78892-cf22-9552-9444-fd4c580c3cd4.png">

②
<img width="1237" alt="スクリーンショット 2020-03-10 10.38.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/9b34641a-6ba2-fa86-c613-72e7d19bb06c.png">

③
<img width="1237" alt="スクリーンショット 2020-03-10 10.33.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/16352744-a736-798d-fc5e-6a0bb8508391.png">

④
![スクリーンショット 2020-03-10 10.43.59.mo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/3d372d01-d439-13a2-1093-9899449581be.png)

⑤
<img width="1077" alt="スクリーンショット 2020-03-12 9.07.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/f84419d4-de1e-f7bc-e7cb-69b2363f1882.png">

⑥
![スクリーンショット 2020-03-10 11.05.43.mo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/5c32590b-caa3-d18b-b7cf-20da8c5c927c.png)

<img width="1131" alt="スクリーンショット 2020-03-12 9.21.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/0c96884d-0f04-008b-55c8-7b40726c3154.png">

![スクリーンショット 2021-04-26 18.39.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/743dfe0f-e7e3-13e5-8b18-06e38d84ea6d.png)

![スクリーンショット 2021-04-26 18.49.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/a51bd416-a6c2-d458-93f1-ae0552fe2437.png)



⑦
<img width="1131" alt="スクリーンショット 2020-03-12 9.26.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/d22a7fec-f290-816e-0424-54adb5f5e8c1.png">


##Lambda の関数コード記述
Designer にある関数名をクリックしスクロールすると、関数コードを記述する項目があるのでここにコードを Python で記述していきます。ここでは画像のデコードと保存場所の設定、画像名を Web 側で取得する為のレスポンスなどを記述しています。

###関数コード全文

```python:lambda_function.py
import json
import boto3
import base64
from datetime import datetime, timezone

def convert_b64_string_to_bynary(s):
    """base64をデコードする"""

def lambda_handler(event, context):
    #画像保存場所の設定
    s3 = boto3.resource('s3')
    bucket = s3.Bucket('xxx画像を保存するバケットの名前xxx')

    # バイナリがBase64にエンコードされているので、ここでデコード
    imageBody = base64.b64decode(event['body-json'])
    TextTime = datetime.now().strftime('%Y-%m-%d-%H-%M-%S') #アップロードした画像ファイル名をその時の時間にする
    images = imageBody.split(b'\r\n',4)#必要な部分だけをsplitで切り取る

    key = TextTime +".png"
    bucket.put_object(
        Body = images[4],
        Key = key
    )    
    #画像名を Web側で取得するための戻り値
    return {
        'isBase64Encoded': False,
        'statusCode': 200,
        'headers': {'Access-Control-Allow-Origin' : '*' ,
        'Content-Type' :'application/json'},
        'body': '{"message":"'+ key + '"}'
    }
```

コードを貼り付けたらS3 のバケット名を下記の部分に入力し、画面右上の保存ボタンで状態を保存します。

```python:lambda_function.py
bucket = s3.Bucket('xxx画像を保存するバケットの名前xxx')
```

##環境変数の設定
タイムスタンプを日本の時間に調整するため環境変数を設定します。関数コードの画面から少し下にスクロールすると、環境変数の項目があります。以下の手順で環境変数を設定します。

1. 環境変数項目右上の編集ボタンクリック
2. 環境変数の編集ページに飛ぶのでそこで環境変数の追加をクリック
3. キーに "TZ" 値に "Asia/Tokyo" と入力
4. 右下の保存ボタンをクリック

<img width="1405" alt="スクリーンショット 2020-03-09 16.15.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/c5060300-dcab-cb02-5e31-5ad4d064d8f6.png">

<img width="985" alt="スクリーンショット 2020-03-09 16.24.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/028f5bf1-f7fa-cc7a-41aa-14be174d3907.png">

##実行ロールの設定
環境変数の項目から少し下にスクロールすると実行ロールの設定項目があります。以下の手順で実行ロールを設定します。

1. 最初のプルダウンで 既存のロールを使用する を選択します
2. 既存のロールに現在編集中の関数名が含まれたポリシーが選択されていることを確認します（service-role/関数名-...）
3. その下のリンク IAM コンソールで**関数名ロールを表示**しますをクリック
4. 新しいウィンドウが開くのでそこで **ポリシーをアッタッチします** をクリック
5. 検索欄に S3 と入力し、AmazonS3FullAccess にチェックを入れ **ポリシーのアタッチ** をクリック

![スクリーンショット 2020-03-09 16.36.28.mo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/8855519a-a51e-1502-8fe1-3c8f19ccc4ce.png)

![スクリーンショット 2020-03-09 16.55.30.mo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/af5aec00-4a0a-b75a-b3f2-a4345b9020f3.png)

![スクリーンショット 2020-03-09 17.04.07.mo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/8917b6bc-f48b-533b-1076-c6aa9c7b3bef.png)

以上で全ての設定が完了です。Web の画面から画像をアップロードしてみて S3 の該当バケット に画像が保存されていれば成功です。おまけとして開発ツールでローカルストレージにアップロード時間でファイル名が保存されていることも確認できます。アップロードしたファイルを指定するサービスを使用する場合などに活用してください。

<img width="1201" alt="スクリーンショット 2020-03-12 9.56.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505753/d609ccf9-a775-fe04-d44f-01021e8d9864.png">

#終わりに
今回は AWS の Amazon Rekognition Custom Labels というサービス で S3 の画像を参照することがあったのでこのような機能を実装してみました。もし興味があれば下の記事に使用感をまとめていますのでどうぞ。
[Amazon Rekognition Custom Labels の使用感について](https://qiita.com/Ted-qiita/items/ef24dce0974b93bbec45)

#参考リンク
[Amazon S3 から HTTP 307 Temporary Redirect レスポンスが返るのはなぜですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response)
[FormでPOST処理をしつつ画面遷移させない方法](https://qiita.com/mczkzk/items/f82284d612ca17d1b2c4)