[Python] Bottleでzipをストリーミングダウンロード
適当なキーワードでパッケージを探したら本当にあった
Rubyにziplineというgemがある。ファイルのリストを渡すとzipに固めてストリーミングダウンロードしてくれるというRails向けのgemだ。今回同じことをPythonで行いたく、似たような機能のパッケージ探してみた。
すると全く同じ名前のパッケージを見つけたけど、これziplineアルゴリズムトレードライブラリだ!
しょうがないからgoogleで「python zip stream」あたりのキーワードで適当に検索を掛けてみると、ドンピシャなパッケージpython-zipstreamが一発ヒットしてビビる。
これは使えそうだ
ダミーファイル作成
まずダウンロードテスト用のダミーファイルを作る
dd if=/dev/random of=dummy1.txt bs=1M count=100
for i in {1..10} ; do dd if=/dev/random of=dummy${i}.txt bs=1M count=100; done
参考:https://qiita.com/ytyng/items/d7afe80ef9da7aa8b721
これでdummy1.txt
~dummy10.txt
まで100MBのファイルが10個作成された。
zipをストリーミングダウンロード
先に成功したコードを挙げておく。
これで開発環境の場合はhttp://localhost:3030/zip
にアクセスすると、dummy1.txt
~dummy10.txt
をzipしたfiles.zip
がストリーミングでダウンロードされる。
import zipstream
from pathlib import Path
@route('/zip')
def zip():
""" download zip with streaming """
path = Path('<temp files dir>')
def generator():
zip = zipstream.ZipFile(mode='w', compression=zipstream.ZIP_DEFLATED)
zip.write((path / 'dummy1.txt').as_posix(), arcname='1.txt')
zip.write((path / 'dummy2.txt').as_posix(), arcname='2.txt')
zip.write((path / 'dummy3.txt').as_posix(), arcname='3.txt')
zip.write((path / 'dummy4.txt').as_posix(), arcname='4.txt')
zip.write((path / 'dummy5.txt').as_posix(), arcname='5.txt')
zip.write((path / 'dummy6.txt').as_posix(), arcname='6.txt')
zip.write((path / 'dummy7.txt').as_posix(), arcname='7.txt')
zip.write((path / 'dummy8.txt').as_posix(), arcname='8.txt')
zip.write((path / 'dummy9.txt').as_posix(), arcname='9.txt')
for chunk in zip:
yield chunk
zip_filename = 'files.zip'
response.content_type = 'application/zip'
response.set_header('Content-Disposition', f'attachment; filename="{zip_filename}"')
response.body = generator()
return response
本家のREADME
を読んだ感じ、使い方がよく理解できなかったので軽く解説を入れてみる。
zip = zipstream.ZipFile(mode='w', compression=zipstream.ZIP_DEFLATED)
まずはZipFileのインスタンスを作る。zipstream.ZipFile
はzipfile.ZipFile
を継承したラッパークラスなので、共通のインターフェースになっている。ここを読むと分かりやすいかもしれない。
mode='w'
を指定しているが、ここはデフォルトも'w'
だし、'w'
が含まれないとRuntimeError
になる。
compression
はデフォルトZIP_STORED
なのでデフォルトだと圧縮されない。取り合えず圧縮したいレベルならZIP_DEFLATED
でおK。
- ZIP_STORED : 圧縮無しでファイルをまとめるだけ
- ZIP_DEFLATED : 通常のZIP圧縮、
zlib
モジュールが必要 - ZIP_BZIP2 : BZIP2での圧縮を行う、
bz2
モジュールが必要 - ZIP_LZMA : LZMAでの圧縮を行う、
lzma
モジュールが必要
zip.write((path / 'dummy1.txt').as_posix(), arcname='1.txt')
ZipFileインスタンスにファイルを渡していく。ここもzipfile.ZipFile#write
とインターフェースは同じで、write('ファイルパス', arcname='アーカイブされるファイル名', compress_type='個別の圧縮指定')
になる。
ここでディレクトリを渡してしまうと空のディレクトリが入ってしまうので注意が必要。
write_iter(arcname, iterable, compress_type)
メソッドではファイルパスではなくイテレータを渡すこともできるようだ。
.as_posix()
してるのはファイルパスとしてpathlib
を受け付けないから…残念!
for chunk in zip:
yield chunk
zipstream.ZipFile
はイテレータで圧縮済みデータを返してくれる。
具体的にはwrite
したファイルのリストからファイルを順次読み出し、1024 * 8
バイト読んで圧縮して都度データを返す。
zip_filename = 'files.zip'
response.content_type = 'application/zip'
response.set_header('Content-Disposition', f'attachment; filename="{zip_filename}"')
response.body = generator()
return response
最後はbottle
のresponse
部分だ。Content-Type
は'application/zip'
を指定する。Content-Disposition
にはファイルをアタッチすることで即ダウンロードする動きをしてくれる。response.body
はジェネレータを受けることができるので、generator()
関数をそのままセットするとストリーム処理するようになってくれる、ありがたい。
Content-Lengthは?
Content-Length
を指定しないとダウンロードの残り時間表示が「不明」になってしまうが、圧縮後のサイズが事前に分からないのでここはしょうがない。
試しにContent-Length
に適当な値を設定してみたら、ダウンロードが強制中断になってしまった。
実行環境
- Python 3.6.3
- bottle 0.12.13
- zipstream 1.1.4