Flaskでファイルダウンロードを実現する３つの方法

## 1. はじめに

最近、PythonでExcelファイルを作成してPJメンバに配布していましたが、手間が掛ってきたのでFlaskでダウンロードできるようにしました。今回はその際に調べたダウンロードの方法について共有したいと思います。なお、Excelファイルの作り方については以下の記事を参照ください。

* [PythonでGitBucketのIssueをExcel出力する（Excel出力編）](https://qiita.com/5zm/items/282956dbc1b4f6813108)
* [PythonでGitBucketのIssueをExcel出力する（その後）](https://qiita.com/5zm/items/b0b620cdeb9596108418)
* [PythonのoepnpyxlでテンプレートファイルからExcel帳票を作成する](https://qiita.com/5zm/items/ed0bd0ea0cfbea49fb5c)


## 2. 方法１：send_file()を利用する

ファイルをダウンロードさせる１つ目の方法はflaskの[`send_file()`](http://flask.pocoo.org/docs/0.12/api/#flask.send_file) を利用することです。
この方法は`root_path`（デフォルトではflaskアプリが配置されたディレクトリ）からの相対パスで指定したファイルをダウンロードさせます。

```python:send_file()を利用する場合
# -*- coding: utf-8 -*-
from flask import Flask, send_file, make_response, send_from_directory

# ★ポイント1
# 2007 Office system ファイル形式の MIME タイプをサーバーで登録する
# https://technet.microsoft.com/ja-jp/library/ee309278(v=office.12).aspx
XLSX_MIMETYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'

# flask
app = Flask(__name__, static_folder=None)

# rest api
# case 1 : send_file()
@app.route('/report1/<string:report_id>', methods=['GET'])
def report1(report_id):

    # ★ポイント2
    downloadFileName = 'report1_' + report_id + '.xlsx'
    downloadFile = 'demo1.xlsx'

    # ★ポイント3
    return send_file(downloadFile, as_attachment = True, \
        attachment_filename = downloadFileName, \
        mimetype = XLSX_MIMETYPE)

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

**★ポイント1**
ファイルをダウンロードした際、クライアント側で適切にファイルを処理できるようにmimetypeを定義します。
Excelファイル（拡張子xlsx）のmimetypeは[マイクロソフトのサイト](https://technet.microsoft.com/ja-jp/library/ee309278(v=office.12).aspx)を参考にしました。

**★ポイント2**
ファイルをダウンロードした際、クライアントに伝える（Webブラウザだとダウンロードダイアログに表示される）ファイル名を定義します。
実際にダウンロードさせるファイル名を`root_path`からの相対パスで定義します。サンプルでは`root_path`に配置されている`demo1.xlsx`ファイルをダウンロードさせることにします。

**★ポイント3**
`send_file()`で`root_path`配下に配置されているファイルをダウンロードさせます。第１引数はダウンロード対象のファイル、第２引数以降はオプションになります。

* 第１引数　：　ダウンロード対象のファイル（`root_path`からの相対パスで指定）
* `mimetype`オプション　：　ダウンロードファイルのmimetypeを設定
* `as_attachment`オプション　：　`True`か`False`を設定。`Content-Disposition: attachment`ヘッダを利用する場合は`True`を設定する必要がある
* `attachment_filename`オプション　：　第１引数のファイル名からファイルを変更したい場合、そのファイル名を設定

## 3. 方法２：send_from_directory()を利用する

ファイルをダウンロードさせる２つ目の方法は[send_from_directory()](http://flask.pocoo.org/docs/0.12/api/#flask.send_from_directory) を利用することです。
`send_from_directory()`の場合、`root_path`からの相対パスではなく、引数で指定したディレクトリからの相対パスでダウンロードさせるファイルを指定します。

```python:send_from_directory()を利用する場合
# case 2 : send_from_directory()
# ★ポイント1
# ex) set DOWNLOAD_DIR_PATH=C:/tmp/flaskDownloadDir
DOWNLOAD_DIR_PATH = os.getenv("DOWNLOAD_DIR_PATH")

@app.route('/report2/<string:report_id>', methods=['GET'])
def report2(report_id):

    # ★ポイント2
    downloadFileName = 'report2_' + report_id + '.xlsx'
    downloadFile = 'demo2.xlsx'

    # ★ポイント3
    return send_from_directory(DOWNLOAD_DIR_PATH, downloadFile, \
        as_attachment = True, attachment_filename = downloadFileName, \
        mimetype = XLSX_MIMETYPE)
```

**★ポイント1**
`send_from_directory()`で指定するダウンロード対象のファイルが格納されたディレクトリを定義します。環境に応じて変更できるように、サンプルでは環境変数から取得するようにしました。

**★ポイント2**
`send_file()`と同様にダウンロード時のファイル名とダウンロード対象のファイルを定義します。ファイルは★ポイント1で説明したディレクトリからの相対パスで定義します。

**★ポイント3**
`send_from_directory()`で指定したディレクトリに配置されているファイルをダウンロードさせます。第１引数はファイルが配置されているディレクトリのパス、第２引数はダウンロード対象のファイル、第３引数以降はオプション（`send_file()`を参照）になります。

## 4. 方法３：make_response()を利用する
ファイルをダウンロードさせる３つ目の方法は[make_response()](http://flask.pocoo.org/docs/0.12/api/#flask.make_response) を利用することです。
HTTPレスポンスを設定できるため、ファイルに限らずデータベースに格納されているバイナリデータ等でもダウンロードさせることができます。

```python:make_response()を利用する場合
# case 3 : make_response()
@app.route('/report3/<string:report_id>', methods=['GET'])
def report3(report_id):

    # ★ポイント1
    response = make_response()

    # ★ポイント2
    response.data = open("demo3.xlsx", "rb").read()

    # ★ポイント3
    downloadFileName = 'report3_' + report_id + '.xlsx'    
    response.headers['Content-Disposition'] = 'attachment; filename=' + downloadFileName

    # ★ポイント4
    response.mimetype = XLSX_MIMETYPE
    return response
```

**★ポイント1**
`make_response()`でレスポンスオブジェクトを作成します。

**★ポイント2**
ダウンロードデータをレスポンスオブジェクトの`data`に設定します。
サンプルではファイル（demo3.xlsx）から読み込んだバイナリデータを設定していますが、データベースや連携システムから取得したデータでも可能です。

**★ポイント3**
前述の２つの方法と同じくダウンロード時のファイル名を定義します。
レスポンスヘッダは設定されないため`Content-Disposition: attachment`ヘッダを手動で設定します。

**★ポイント4**
レスポンスオブジェクトの`mimetype`にダウンロードファイルの`mimetype`を設定し、作成したレスポンスオブジェクトを戻り値として返却します。

## 5. さいごに

今回はFlaskでファイルダウンロードを実現する３つの方法について説明しました。簡単に使えるものからローレベルなものまで様々であったと思います。アプリケーションの仕様や前提を踏まえて、適切な方法を選択してください。
次回はファイルダウンロードと一緒に利用されることの多いファイルアップロードについて説明したいと思います。