FlaskでRest API(json形式)のファイルアップロードを実現する方法

## 1. はじめに

今回はflaskで`Rest API(json形式)`におけるファイルアップロードの方法について説明したいと思います。一般的なファイルアップロードである`multipart/form-data`を利用したファイルアップロードについては[前回の記事](https://qiita.com/5zm/items/ac8c9d1d74d012e682b4)を参照ください。
なお、クライアント側については「[Pythonのrequestsを利用してRest API(json形式)にファイルアップロードする方法](https://qiita.com/5zm/items/8dc6189940b42e241210)」を参照ください。

## 2. ソースコード

```python:jsonFileUploadApp.py
# -*- coding: utf-8 -*-
from flask import Flask, request, make_response, jsonify
import os
import werkzeug
import base64
from datetime import datetime

# flask
app = Flask(__name__)

# ★ポイント1
# limit upload file size : 1MB
# ex) set MAX_JSON_CONTENT_LENGTH=1048576
MAX_JSON_CONTENT_LENGTH = int(os.getenv("MAX_JSON_CONTENT_LENGTH", default="0"))

# ★ポイント2
# ex) set UPLOAD_DIR_PATH=C:/tmp/flaskUploadDir
UPLOAD_DIR = os.getenv("UPLOAD_DIR_PATH")

# rest api
# case 2 : json with base64 encoding
#   {
#     'fileName': 'ファイル名',
#     'contentType': 'mimetype',
#     'contentData': 'バイナリデータをbase64でエンコードしたascii文字列'
#   }
@app.route('/data/json/upload', methods=['POST'])
def upload_rest_json():

    # ★ポイント3
    jsonData = request.json
    fileName = jsonData.get("fileName")
    contentType = jsonData.get("contentType")
    contentDataAscii = jsonData.get("contentData")

    # ★ポイント4
    contentData = base64.b64decode(contentDataAscii)
    
    # ★ポイント5
    contentDataSize = len(contentData)
    if MAX_JSON_CONTENT_LENGTH > 0:
        if MAX_JSON_CONTENT_LENGTH < contentDataSize:
            raise werkzeug.exceptions.RequestEntityTooLarge( \
                "json content length over : {0}".format(contentDataSize))

    # ★ポイント6
    saveFileName = datetime.now().strftime("%Y%m%d_%H%M%S_") \
        + werkzeug.utils.secure_filename(fileName)
    with open(os.path.join(UPLOAD_DIR, saveFileName), 'wb') as saveFile:
        saveFile.write(contentData)
    return make_response(jsonify({'result':'upload json OK.'}))

# ★ポイント7
@app.errorhandler(werkzeug.exceptions.RequestEntityTooLarge)
def handle_over_max_file_size(error):
    print("werkzeug.exceptions.RequestEntityTooLarge")
    return 'result : file size is overed.'

# main
if __name__ == "__main__":
    print app.url_map
    app.run(host='localhost', port=3000)
```

**★ポイント1**

ファイルアップロードにおけるセキュリティ対策としてデータサイズの制限は重要です。
`multipart/form-data`の場合はflaskの標準機能（[MAX_CONTENT_LENGTH](http://flask.pocoo.org/docs/0.12/config/)）で制限することができました。
しかし`Rest API(json形式)`の場合、この機能が利用できないため、データサイズを制限する機能を独自に実装する必要があります。
サンプルでは環境変数からデータサイズ（単位はByte）を制限できるようにしました。今回は1MBを上限としました。

**★ポイント2**

アップロードされたファイルをファイルシステムに保存する場合、Webからアクセス可能な場所にするかアクセス不可能な場所にするか業務要件によって変わるかと思います。
サンプルでは環境に応じて変更できるように、環境変数から取得するようにしました。

**★ポイント3**

`request.json`で送信されたデータをjsonとしてアクセスします。
この時点ではファイルデータは単なるascii文字列になっているため、特別なことはありません。

* fileNameフィールド　：　アップロードファイルのファイル名
* contentTypeフィールド　：　アップロードファイルのMIMEタイプ
* contentDataフィールド　：　アップロードファイルのファイルデータ

**（注意）**
APIの仕様としてそのように定義しているだけです。HTTPの仕様のようにFIXしているわけではないので注意が必要です。

**★ポイント4**

今回の記事の最大ポイントです。jsonではバイナリデータを設定できないため、base64でエンコードしたascii文字列で送られてきます。
`base64.b64decode()`を利用してバイナリデータにデコードします。

**★ポイント5**

アップロードされたファイルのデータサイズが★ポイント1で定義した値を超過していないかチェックを行います。
ファイルデータはascii文字列のバイト数ではなく、デコードしたバイナリデータのバイト数でチェックします。ascii文字列の場合は[データサイズが30％ほど大きく](https://ja.wikipedia.org/wiki/Base64)なってしまい、正しくチェックすることができないためです。
制限を超過した場合、`multipart/form-data`のサイズ超過とエラーハンドリングを同じにするため、`werkzeug.exceptions.RequestEntityTooLarge`例外を発生させることにしました。

**（注意）**

サンプルの仕様では★ポイント4でデコードした後にデータサイズをチェックするため、制限サイズ以上のファイルでもメモリに展開することになります。
利用者が制限された環境以外の場合、HTTPリクエストのデータサイズ等で想定以上に大きなリクエストは事前にリジェクトさせる仕組みを検討する必要があります。

**★ポイント6**

★ポイント4で取得したバイナリデータをファイルとして保存します。普通のファイル保存の処理です。
サンプルでは★ポイント2で取得したディレクトリに、[werkzeug.utils.secure_filename()](http://werkzeug.pocoo.org/docs/0.14/utils/#werkzeug.utils.secure_filename)を利用してOSセーフなファイル名にコンバートした後、年月日時分秒のプレフィックスを付与して保存しています。

**★ポイント7**

★ポイント5のデータサイズチェックにより、★ポイント1で設定した`MAX_JSON_CONTENT_LENGTH`を超過したファイルデータの場合、[werkzeug.exceptions.RequestEntityTooLarge](http://werkzeug.pocoo.org/docs/0.14/exceptions/#werkzeug.exceptions.RequestEntityTooLarge) 例外が発生します。
必要に応じて`RequestEntityTooLarge`のエラーハンドリングを行ってください。

## 3. さいごに

今回はflaskで`Rest API(json形式)`におけるファイルアップロードの方法について説明しました。
`multipart/form-data`とは異なりAPIの仕様に依存するため、flask（werkzeug）の標準機能だけでは実現できないことを理解できたかと思います。
といっても独自に検討するのはbase64の扱いと、データサイズのチェックをどうするかくらいなので難しくはなかったと思います。