Python + Bottle でファイルのダウンロードを実装


[Bottle](http://bottlepy.org/docs/dev/index.html)はPythonの軽量Webアプリケーションフレームワークです。
Webアプリでは、ファイルをサーバーサイドで生成してダウンロードさせたりすることがありますよね。
今回はBottleでファイルダウンロードの実装について書いてみます。

# 静的ファイルディレクトリ

Webアプリケーションでは`/static/`みたいな画像/CSS/JSファイルを集めたディレクトリを作る事が多いと思いますが、このようなケースでは`route`関数でリクエストハンドラをいちいち作っていられないので、以下のように`bottle`の`static_file`関数を利用するのが便利です。

この例では`http://localhost:8080/static/hoge.txt`にアクセスすると実行時のカレントディレクトリからみた `./static` にある`hoge.txt`の内容が配信されます。

多くのWebアプリケーションの本番展開では静的ファイルを集めたディレクトリはnginxやapacheから直接配信するのが普通だと思います。ですがbottleで記述されたPythonアプリでも対応しておくことでローカルでの開発時に静的ファイルを読み込む事ができはかどります。(当社比)

```python
# -*- coding: utf-8 -*-
from bottle import route, run, static_file


@route('/static/<file_path:path>')
def static(file_path):
    return static_file(file_path, root='./static')


run(host='localhost', port=8080)
```


[bottleのソース](https://github.com/bottlepy/bottle/blob/master/bottle.py#L2651)をみたところ、メジャーなファイル形式は拡張子によって`mimetypes`パッケージによって推測されているようです。画像やcssやzip/gzファイルなどの一般的なファイルは正しいmimetypeが`Content-Type`HTTPヘッダに入ってくれそうです.

# 表示じゃなくてダウンロードされて欲しいんだけど...

`static_file`関数の`download`オプションを`True`に設定しましょう。

```python
# -*- coding: utf-8 -*-
from bottle import route, run, static_file


@route('/static/<file_path:path>')
def static(file_path):
    return static_file(file_path, root='./static', download=True)


run(host='localhost', port=8080)
```

読み込むファイル名とブラウザが保存するファイル名を別にしたい場合は`download`オプションにブラウザに保存させたいファイル名を設定します。詳しくは「static_file関数の引数・オプション」のセクションで後述します。

# パターン1. メモリに乗るような小さなファイル

`static_file`関数がそのまま使えます。

```python
# -*- coding: utf-8 -*-
from bottle import route, run, static_file


@route('/sample_image.png')
def sample_image():
    return static_file('./my_sample_image.png', root='.')


run(host='localhost', port=8080)
```

`HTTPResponse`で実装するとすれば以下のようになるでしょう。
実行時にファイルの内容を編集して配信などのトリッキーなことをする必要がある場合などに使えそうです。
が、すでにあるファイルのダウンロードをさせるだけであれば基本的には`static_file`関数を使うのが簡単で確実でしょう。
`static_file`関数を使えばmimetypeの推定やファイルが存在しないときの404レスポンスの作成などの細かいところもおまかせにできます。


```python
# -*- coding: utf-8 -*-
from bottle import route, run, template, response


@route('/sample_image.png')
def sample_image():
    response.content_type = 'image/png'
    with open('./my_sample_image.png', 'rb') as fh:
        content = fh.read()
    response.set_header('Content-Length', str(len(content)))
    return content


run(host='localhost', port=8080)
```

これで`http://localhost:8080/sample_image.png`にアクセスすると画像が表示されます。

Bottleから呼び出されるHTTPリクエストに対するハンドラは, ハンドラの返り値がそのままレスポンスのbodyになる性質を利用しています。
HTTPレスポンスヘッダーはbottleパッケージのresponseオブジェクトをそのままいじることで変えています。
個人的にはどちらかというと、以下のような明示的にHTTPResponseオブジェクトを組み立てて返すやり方が好みです。


```python
# -*- coding: utf-8 -*-
from bottle import route, run, template, HTTPResponse


@route('/sample_image.png')
def sample_image():
    with open('./my_sample_image.png', 'rb') as fh:
        content = fh.read()
    resp = HTTPResponse(status=200, body=content)
    resp.content_type = 'image/png'
    resp.set_header('Content-Length', str(len(content)))
    return resp


run(host='localhost', port=8080)
```

Bottleから呼び出されるHTTPリクエストに対するハンドラは, ハンドラの返り値がHTTPResponseの場合そのHTTPResponseオブジェクトの内容に沿ってレスポンスを返してくれます。
HTTPResponseの詳細については[BottleのRequest/Responseオブジェクトをマスター](http://qiita.com/tomotaka_ito/items/62fc4d58d1be7867a158)に詳しく書いてみましたので、参考にしてみてください。

# パターン2. メモリに乗らないような大きなファイル

メモリに乗らないような大きなファイルは少しずつファイルから読み込んでソケットに書き込んで行く必要があります。
このようなケースでも`static_file`が活用できます。`static_file`はファイルのコンテンツがまるごと読み込まれたレスポンスではなく、内部的にファイルを少しずつ読み込んで配信する実装になっています。

調べてみたところ、Bottleのハンドラはジェネレータになった場合、ジェネレータが生成する各要素をbodyの断片としてchunkedでクライアントに送信してくれるという仕様になっているようです。`static_file`の[ソース](https://github.com/bottlepy/bottle/blob/master/bottle.py#L2725)でも`_file_iter_range`というジェネレータ関数によってコンテンツが少しずつ読み込まれてそれがレスポンスになっているようです。

もし自前で実装するのであれば以下のようになるでしょう。

```python
# -*- coding: utf-8 -*-
from bottle import route, run, template, response


@route('/sample_image.png')
def sample_image():
    response.content_type = 'image/png'
    bufsize = 1024  # 1KB
    with open('./my_sample_image.png', 'rb') as fh:
        while True:
            buf = fh.read(bufsize)
            if len(buf) == 0:
                break  # EOF
            yield buf


run(host='localhost', port=8080)
```

`static_file`があるからこのやり方は覚えなくてもいいのでは？という話もありますが、
データベースからファイルを経由せずにオンザフライで読み込みながらCSVファイル形式に加工しながらクライアントへ送信、というような実装がありえると思っています。というかそういうケースでやり方を調べたのが執筆のきっかけでしたので他の誰かのお役に立てばうれしいです。

`HTTPResponse`オブジェクトを`return`する形で実装しようと思うと`HTTPResponse`オブジェクトの`body`にジェネレータを設定します。

```python
# -*- coding: utf-8 -*-
from bottle import route, run, template, HTTPResponse


@route('/sample_image.png')
def sample_image():
    resp = HTTPResponse(status=200)
    resp.content_type = 'image/png'

    def _file_content_iterator():
        bufsize = 1024  # 1KB
        with open('./my_sample_image.png', 'rb') as fh:
            while True:
                buf = fh.read(bufsize)
                if len(buf) == 0:
                    break  # EOF
                yield buf

    resp.body = _file_content_iterator()
    return resp


run(host='localhost', port=8080)
```

ただこのやり方も、関数内に関数を定義したりと一見わかりにくさがあるのでちょっとした気持ち悪さが残るかもしれません。これはお好みですね。

ジェネレータの実装はPythonを触り始めの方はわかりにくいと思いますが、Pythonのイテレータ/ジェネレータについて[Pythonのイテレータとジェネレータ](http://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09)という記事を書いてみたのでよかったらご覧ください。

# インライン表示でなく, ダウンロードさせたい

どちらかというとBottleの話というよりHTTPの話ですが、これは`Content-Disposition`ヘッダを使うとできます。`static_file`関数の利用であれば前述のように`download`オプションに`True`を設定すればOKです。ここでは`HTTPResponse`を使ったやり際の実装を紹介しておきます。

```python
# -*- coding: utf-8 -*-
from bottle import route, run, template, response


@route('/sample_image.png')
def sample_image():
    response.content_type = 'image/png'
    with open('./my_sample_image.png', 'rb') as fh:
        content = fh.read()
    response.set_header('Content-Length', str(len(content)))
    download_fname = 'hoge.png'
    response.set_header('Content-Disposition', 'attachment; filename="%s"' % download_fname.encode('utf-8'))
    return content


run(host='localhost', port=8080)
```

これで`http://localhost:8080/sample_image.png`にアクセスすると`hoge.png`というファイル名で画像が保存されます。

# static_file関数の引数・オプションの解説

`static_file`関数のシグネチャは以下のようになっています。

```python
def static_file(filename, root,
                mimetype='auto',
                download=False,
                charset='UTF-8'):
    ....
```

- `filename`: `root`にある配信したいコンテンツが含まれるファイルの名前。
- `root`: 配信したいコンテンツが含まれるファイル`filename`があるディレクトリ。
- `mimetype`: オプション。デフォルトでは`mimetypes`パッケージにより自動で推測される。
- `download`: オプション。保存ダイアログや自動的にダウンロードフォルダに保存するような形でダウンロードさせたい場合`True`またはクライアントサイドで保存される際の名前を文字列で指定。インラインで表示する形の場合は`False`。デフォルトは`False`
- `charset`: オプション。mimetypeが`text/`ではじまるまたは`application/javascript`の場合, `Content-Type`HTTPヘッダに `; charset=(charsetの値)`が追加される。

# 参考リンク

- [Recipes - Bttle 0.13-dev: KEEPALIVE REQUESTS](http://bottlepy.org/docs/dev/recipes.html#keep-alive-requests)
- [BottleのRequest/Responseオブジェクトをマスター](http://qiita.com/tomotaka_ito/items/62fc4d58d1be7867a158)
- [Pythonのイテレータとジェネレータ](http://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09)