Flaskでmultipart/form-dataのファイルアップロードを実現する方法

## 1. はじめに

今回はflaskで`multipart/form-data`によるファイルアップロードの方法について説明したいと思います。わざわざ`multipart/form-data`と書いているのは`Rest API(json形式)`におけるファイルアップロードという選択肢もあるからです。こちらについては次回説明する予定です。
なお、クライアント側については「[Pythonのrequestsを利用してmultipart/form-dataのFormにファイルアップロードする方法](https://qiita.com/5zm/items/92cde9e043813e02eb68)」を参照ください。

## 2. ソースコード

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

# flask
app = Flask(__name__)

# ★ポイント1
# limit upload file size : 1MB
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024

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

# rest api : request.files with multipart/form-data
# <form action="/data/upload" method="post" enctype="multipart/form-data">
#   <input type="file" name="uploadFile"/>
#   <input type="submit" value="submit"/>
# </form>
@app.route('/data/upload', methods=['POST'])
def upload_multipart():

    # ★ポイント3
    if 'uploadFile' not in request.files:
        make_response(jsonify({'result':'uploadFile is required.'}))

    file = request.files['uploadFile']
    fileName = file.filename
    if '' == fileName:
        make_response(jsonify({'result':'filename must not empty.'}))

    # ★ポイント4
    saveFileName = datetime.now().strftime("%Y%m%d_%H%M%S_") \
        + werkzeug.utils.secure_filename(fileName)
    file.save(os.path.join(UPLOAD_DIR, saveFileName))
    return make_response(jsonify({'result':'upload OK.'}))

# ★ポイント5
@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)
```

```html:static/upload.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<!-- ★ポイント6 -->
<form action="/data/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="uploadFile"/>
  <input type="submit" value="submit"/>
</form>
</body>
</html>
```

**★ポイント1**

ファイルアップロードにおけるセキュリティ対策としてデータサイズの制限は重要です。
flaskでは標準の機能でデータサイズの上限（正確にはHTTPリクエストの`Content-Length`）を設定することができます。
単位はByteで、サンプルでは1MBを上限としました。

* [MAX_CONTENT_LENGTHの説明](http://flask.pocoo.org/docs/0.12/config/)

> If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by returning a 413 status code.

**★ポイント2**

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

**★ポイント3**

`<input type="file">`のデータはflaskでは`request.files`に格納されます。Formのフィールド名（input要素のname属性の値）でデータが存在する（送信されている）か確認します。
特定のアップロードファイルには`request.files`からFormのフィールド名をキーとしてアクセスできます。
ファイルの実体は`werkzeug.datastructures.FileStorage`です。`FileStorage`にはファイル名やmimetype等の情報が格納されています。

* filename　：　送信時のファイル名
* name　：　Formのフィールド名
* headers　：　HTTPリクエストのヘッダ情報（flaskのheaderオブジェクト）
* content_length　：　HTTPリクエストのcontent-length
* mimetype　：　mimetype

詳しくは[公式ガイドライン](http://werkzeug.pocoo.org/docs/0.14/datastructures/#werkzeug.datastructures.FileStorage)を参照ください。
サンプルではファイル名が設定されているかチェックしています。

**★ポイント4**

`save()`でアップロードされたファイルを、引数で指定したファイルパスにファイルとして保存します。この際、ファイル名の重複すると上書きになるため注意が必要です。
サンプルでは★ポイント2で取得したディレクトリに、[werkzeug.utils.secure_filename()](http://werkzeug.pocoo.org/docs/0.14/utils/#werkzeug.utils.secure_filename)を利用してOSセーフなファイル名にコンバートした後、年月日時分秒のプレフィックスを付与して保存しています。

**（注意）完全に一意なファイル名で保存する場合、アップロードされたファイル名を利用せず、システムにて生成した値を利用すべきです。**

なお、ファイルのバイナリデータをDB等に格納したい場合、`save()`ではなく`stream`を利用して直接インプットストームを操作することができます。

**★ポイント5**

★ポイント1で設定した`MAX_CONTENT_LENGTH`を超過したHTTPリクエストの場合、[werkzeug.exceptions.RequestEntityTooLarge](http://werkzeug.pocoo.org/docs/0.14/exceptions/#werkzeug.exceptions.RequestEntityTooLarge) 例外が発生します。
必要に応じて`RequestEntityTooLarge`のエラーハンドリングを行ってください。

**★ポイント6**

flaskのデフォルト設定は`static`ディレクトリがWebに公開されます。詳細については「[Flaskで静的ファイルの格納ディレクトリとURLを変更する](https://qiita.com/5zm/items/db6acb96594e0ff7c549)」を参照ください。
動作確認用にこの機能を利用してファイルのアップロード画面（`static/upload.html`）を作成しました。

* アクションのURL　：　/data/upload
* HTTPメソッド　：　POST
* Formのエンコード　：　multipart/form-data
* fileのフィールド名　：　uploadFile

Webブラウザではなくpythonのプログラムでアップロードする場合、「[Pythonのrequestsを利用してmultipart/form-dataのFormにファイルアップロードする方法](https://qiita.com/5zm/items/92cde9e043813e02eb68)」を参照ください。

## 3. さいごに

今回はflaskで`multipart/form-data`によるファイルアップロードの方法について説明しました。
flask（werkzeug）の標準機能で対応できるため、簡単にファイルアップロードを実装できることが理解できたかと思います。
冒頭にも書きましたが、次回はFlaskで`Rest API(json形式)`によるファイルアップロードを実現する方法について説明したいと思います。