Flask サーバに保存せずにファイルをアップロード・ダウンロードする


Flaskの使い方の基本を知っていることを前提とする。
ファイルを処理するスクリプトを走らせたいが、ベタ張りで保存したくないのは当然のこと、データベースに格納する必要もないという状況にあった。そこで、一回のPOSTリクエストでファイルをアップロード、処理、ダウンロードまでやってしまえばいいのではということで調べて見た結果のメモ。

# アップロード
結論として、IOクラスのオブジェクトとして受け取れば良い[^1][^2][^3]
Formタグでファイルを受け取れば、StringIOのバイナリファイルとしてオブジェクトにできる。[^4]
この段階までを実行するプログラム例：

```app.py
from flask import Flask, render_template, request


app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html')
    else:
        f = request.files['file']
        return f'uploaded {f.filename}'


if __name__ == '__main__':
    app.run(debug=True)
```

```templates/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <body>
    <form action="/" method="POST" enctype="multipart/form-data">
      <input type="file" name="file">
      <input type="submit">
    </form>
  </body>
</html>
```


# ダウンロード
公式ドキュメント[^5]が一番参考になったが、結論として、ダウンロードというより、send_fileを使えば良い
ドキュメントの情報の注意点としては、
StringIOモジュールは3系ではなくなっており、ioクラスからio.StringIOを利用する必要がある。[^6]

また、Python3以降では、Flaskの要求でStringIOではなくBytesIOを用いる

```diff:app.py
+ from flask import Flask, render_template, request, send_file


app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html')
    else:
        f = request.files['file']
        # return f'uploaded {f.filename}'
+       return send_file(f,
+                        attachment_filename=f.filename,
+                        as_attachment=True)


if __name__ == '__main__':
    app.run(debug=True)
```
サンプルなので上げたファイルをまた落とすだけという中身空っぽな処理になっているが、`f = request.files['file']`と`return send_file(...)`の間に処理を書けばいい。


# デプロイ編
蛇足ながら、uwsgi + nginxで構成したサーバに実装した際に遭遇した問題とその解決を記す。若干手抜き。
## nginxでpermission denied

```
2017/11/01 06:46:19 [crit] 25062#0: *1 open() "/usr/local/var/run/nginx/client_body_temp/0000000001" failed (13: Permission denied), client: 124.27.107.250, server: (your_domain), request: "POST /sbi HTTP/1.1", host: "(your_domain)", referrer: "http://(your_domain)/(uri)"
```
error_logにこんなのが残っていました。
こちらの記事を参考にしました
https://qiita.com/paranishian/items/5ca43e13e9711a0a1ae6

## uwsgiでsend_fileがうまくいかない
エラー内容：

```
[2017-11-01 06:59:20,352] ERROR in app: Exception on /(uri) [POST]
io.UnsupportedOperation: fileno

The above exception was the direct cause of the following exception:

(中略)
SystemError: <built-in function uwsgi_sendfile> returned a result with an error set
[pid: 25653|app: 0|req: 3/3] 124.27.107.250 () {52 vars in 992 bytes}[Wed Nov  1 06:59:20 2017] POST /(uri) => generated 291 bytes in 81 msecs (HTT
P/1.1 500) 2 headers in 84 bytes (9 switches on core 0)
```

こちらを参考に、uwsgi.iniを編集しました
https://github.com/unbit/uwsgi/issues/1126#issuecomment-166687767

```uwsgi.ini
wsgi-disable-file-wrapper = true
```

---
### 参照・注釈
[^1]: 公式ドキュメント　http://flask.pocoo.org/docs/0.12/patterns/fileuploads/
[^2]: 参考にしたブログ　http://nekoyukimmm.hatenablog.com/entry/2016/05/27/162736
[^3]: 参考サイト　https://www.tutorialspoint.com/flask/flask_file_uploading.htm
[^4]: 正確にStringIOなのかBytesIOなのか理解が若干曖昧だが、`f = open(fname, 'r')`と同じように扱えることは確かだ。
[^5]: 公式ドキュメント　一番参考になった http://flask.pocoo.org/snippets/32/
[^6]: https://stackoverflow.com/a/18284900/8776028