Bottleは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アプリでも対応しておくことでローカルでの開発時に静的ファイルを読み込む事ができはかどります。(当社比)
# -*- 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のソースをみたところ、メジャーなファイル形式は拡張子によってmimetypes
パッケージによって推測されているようです。画像やcssやzip/gzファイルなどの一般的なファイルは正しいmimetypeがContent-Type
HTTPヘッダに入ってくれそうです.
表示じゃなくてダウンロードされて欲しいんだけど...
static_file
関数のdownload
オプションをTrue
に設定しましょう。
# -*- 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
関数がそのまま使えます。
# -*- 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レスポンスの作成などの細かいところもおまかせにできます。
# -*- 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オブジェクトを組み立てて返すやり方が好みです。
# -*- 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オブジェクトをマスターに詳しく書いてみましたので、参考にしてみてください。
パターン2. メモリに乗らないような大きなファイル
メモリに乗らないような大きなファイルは少しずつファイルから読み込んでソケットに書き込んで行く必要があります。
このようなケースでもstatic_file
が活用できます。static_file
はファイルのコンテンツがまるごと読み込まれたレスポンスではなく、内部的にファイルを少しずつ読み込んで配信する実装になっています。
調べてみたところ、Bottleのハンドラはジェネレータになった場合、ジェネレータが生成する各要素をbodyの断片としてchunkedでクライアントに送信してくれるという仕様になっているようです。static_file
のソースでも_file_iter_range
というジェネレータ関数によってコンテンツが少しずつ読み込まれてそれがレスポンスになっているようです。
もし自前で実装するのであれば以下のようになるでしょう。
# -*- 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
にジェネレータを設定します。
# -*- 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のイテレータとジェネレータという記事を書いてみたのでよかったらご覧ください。
インライン表示でなく, ダウンロードさせたい
どちらかというとBottleの話というよりHTTPの話ですが、これはContent-Disposition
ヘッダを使うとできます。static_file
関数の利用であれば前述のようにdownload
オプションにTrue
を設定すればOKです。ここではHTTPResponse
を使ったやり際の実装を紹介しておきます。
# -*- 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
関数のシグネチャは以下のようになっています。
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の値)
が追加される。