AWS S3からファイルを取得したFlask APIがファイルをレスポンスとして返戻し、Vueでダウンロードする

# はじめに
忙しいタイトルをしていて、大変申し訳ありません。
現在、以下のアプリ構成でお仕事をしています。
**フロントエンド： Vue.js**
**バックエンド: Flask (python)**

Flaskの方については、これまで全く触ったことがなかったので、新鮮さを噛みしめつつ、日々触っています。
そもそもpythonの方も、ファイルを読み込んでcsvを吐き出してくれるというプチツールを作ったことがあるぐらいですので、多くのことを勉強させてもらっています。

# 本記事で書くこと
タイトルそのままですが、以下を実現します。
 1. Flask(Api)が、AWS SDK for Python (Boto3)を使って、AWS S3からファイルを取得する。
 2. 取得したファイルデータをレスポンスに詰め、クライアント（Vue.js）に返却する。
 3. クライアント（Vue.js）がレスポンスからファイルをダウンロードする。
~~Lambda + API GateWayで良いのでは・・・~~
こんなこともできるんだというぐらいの気持ちで読んでください。

# 本記事で書かないこと
 - Lambda + API GateWayで上記の流れを実現すること
 - Flaskについてのあれこれ
 - pythonについてのあれこれ
 - aws関連についてのあれこれ
 - Vue.jsについてあれこれ
 - fetch APIのあれこれ
 
おそらく上記は色々な方が実践済みだと思うので。

# なぜやろうと思ったのか
そもそも、今回の機能は [amplify](https://aws.amazon.com/jp/amplify/) で実現する予定でした。
が、「Internet Explorer」の場合は、なぜか、aws credentialの認証情報がamplifyにわたってくれず、s3からファイルを取得しようしたときに、403エラーになるという状況に・・・
そのため、このように実装することにしました。
ですので、「Internet Explorer」を対象外とする場合は、素直にamplifyを使うことをオススメします・・・

# 環境
 - vue: 2.6.11
 - boto3: 1.12.38
 - Flask: 1.1.2
 - python: 3系

# 事前準備
 - awscli、boto3をインストールしておく
 - `aws configure`で、awscliを使えるようにしておく

# それでは内容へ
## 1. Flask(Api)が、AWS SDK for Python (Boto3)を使って、AWS S3からファイルを取得する。
s3からファイルを取得する場合に、以下二つの方法があります。
 
``` python:ローカルにダウンロード
boto3.resource('s3').Bucket(BUCKET).download_file(Filename=KEY, Key=KEY)
```

``` python:メモリ上にダウンロード
boto3.client('s3').get_object(Bucket=bucket, Key=key)
```
Flaskのローカル（アプリケーション上）にファイルがダウンロードされてしまうと、そのファイルを消したりしないといけないのが面倒なので、後者のメモリ上にダウンロードする方法を採用しました。

### ソースコード

``` python:s3からファイルを取得する
@app.route('/file', methods=['GET'])
def get_file_from_s3():
    s3 = boto3.client('s3')
    bucket = 'YOUR_BUCKET_NAME'
    key = 'YOUR_FILE_NAME'

    obj = s3.get_object(Bucket=bucket, Key=key)
```

これで、`/file`のエンドポイントを叩くことで、s3からファイルオブジェクトを取得することができました。
この`obj`の内、`Body`のバイト配列を今回は利用します。

## 2. 取得したファイルデータをレスポンスに詰め、クライアントに返却する。
それでは、取得したファイルオブジェクトからレスポンスをつくりましょう。
レスポンスは[make_response()](https://flask.palletsprojects.com/en/0.12.x/api/#flask.make_response)でつくります。

### ソースコード

``` python:取得したファイルオブジェクトからレスポンスをつくって返却してあげる
@app.route('/file', methods=['GET'])
def get_file_from_s3():
    s3 = boto3.client('s3')
    bucket = 'YOUR_BUCKET_NAME'
    key = 'YOUR_FILE_NAME'

    obj = s3.get_object(Bucket=bucket, Key=key)
    
    response = make_response()

    # レスポンスobjectのdataに取得したファイル情報を設定します。
    # read()で、ファイルを読み込みます。
    response.data = obj['Body'].read()

    # ダウンロード時のファイル名を定義します。
    # quoted_filenameでURLエンコードします。また、日本語のファイル名でもできるように、UTF-8エンコーディングもします。
    quoted_filename = urllib.parse.quote(name)
    response.headers['Content-Disposition'] = "attachment; filename='{}'; filename*=UTF-8''{}".format(quoted_filename, quoted_filename)

    return response
```
## 3. クライアントがレスポンス情報をダウンロードする。
それでは、クライアント（Vue.js）でレスポンスのファイル情報をダウンロードしましょう。
今回は[Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)を利用します。
### ソースコード
```javascript:レスポンスからファイルをダウンロードする
    fetch('http://XXXXXX/file', {
      method: 'GET',
    }).then((response) = >{
      // blob()で、Blob形式にします
      return response.blob();
    }).then((blob) => {
      // IEの場合は、msSaveBlobを使います
      if (window.navigator.msSaveBlob) {
        window.navigator.msSaveBlob(blob, 'ファイル名.拡張子');
      } else {
        const a = document.createElement('a');
        a.download = 'ファイル名.拡張子';
        a.href = URL.createObjectURL(blob);
        a.click();
      }
    })
```
これで、`ファイル名.拡張子`のファイルをダウンロードできます。
`ファイル名.拡張子`を`Content-Disposition`のfilenameとしたい場合は、
**response.headers.get('Content-Disposition)**で`Content-Disposition`を取得できるので、そこから抽出して使うと良いと思います。

## 参考にさせていただいた記事
[Flaskでファイルダウンロードを実現する３つの方法](https://qiita.com/5zm/items/760000cf63b176be544c)
[Python の boto3 で S3 とダウンロード/アップロードする](https://sig9.hatenablog.com/entry/2020/02/02/000000)
[Djnagoメモ Content-Dispositionのfilenameに日本語をセットする](https://qiita.com/sin_tanaka/items/d57ea67351efc1994c48)