やさしいZIPダウンロード機能
概要
WEBアプリケーションに、複数ファイルの一括ダウンロード機能(ZIPダウンロード機能)を実装したら色々気になることが出てきた。その問題点や解決法を考えてみる。
読むの面倒な人向けまとめ
ZIPのストリーミングダウンロードは、(圧縮後サイズが不明なのでContentsLengthがセット出来ず)残り時間が出なくて困る。だが圧縮方式がSTORE(無圧縮)であれば圧縮後サイズを計算できる。
それには、
- 全ファイルのファイルサイズの合計
- 全ファイルのファイル名のバイトサイズの合計の2倍
- 全ファイルのファイル数 * 76 バイト
- 固定の 22 バイト
を足し合わせば良い。
※ Comment, ExtraField, DataDescriptor, 暗号化, 64bit対応 がない前提
詳細は以下に
ZIPダウンロード機能について
ブラウザでは1回のダウンロード(リクエスト)で1つのファイルしか送信出来ない。そこでWEBアプリケーションから複数ファイルをダウンロードさせる為、ファイルをZIP圧縮して送るケースがままある。
少量のファイルであれば一度サーバー側に一時的にZIPファイル作成し、そのZIPファイルを送信するという方法が簡単だ。しかしファイルが大量であると、ZIPファイルを作成する時間・ディスク容量・一時ファイルの管理などが問題になるだろう。
そしてこの問題は、ストリーミング機能を使えば解決する。送りたいファイルを少しずつ読み込み、圧縮されたバイナリができあがり次第チョロチョロと送ってあげればよい。ストリーミングは多くのZIPライブラリで用意されているハズ。
ただこの方式では、(当たり前ではあるが)圧縮前に圧縮後のファイルサイズがわからない。送信するデータサイズが不明だと、ブラウザでダウンロードしている間に残り時間が表示されない。特に大容量のダウンロードになるといつ終わるかわからず不安になるかもしれない。
ZIPダウンロードの方式 | 準備時間 | 一時ファイル | 残り時間表示 |
---|---|---|---|
ZIPファイル事前作成 | 長い | 必要 | 可能 |
ZIPストリーミング | 短い | 不要 | 不可能 |
圧縮前に圧縮後サイズを取得する方法
圧縮方式がSTORE(無圧縮)の場合、ファイルがそのままZIPファイルに入っている。あとはZIP内ファイル毎のヘッダと、最後に固定長のフッタが付くだけだ。ファイルサイズがわかっているならば計算できそうである。
というわけで冒頭のまとめで記述した内容になる。
圧縮方式がSTORE(無圧縮)であれば圧縮後サイズを計算できる。
それには、
- 全ファイルのファイルサイズの合計
- 全ファイルのファイル名のバイトサイズの合計の2倍
- 全ファイルのファイル数 * 76 バイト
- 固定の 22 バイト
を足し合わせば良い。
※ Comment, ExtraField, DataDescriptor, 暗号化, 64bit対応 がない前提
ただ、※印に色々書いて有るように上記の計算が成り立つのには条件がある。
- まず圧縮方式がSTORE(無圧縮)であること。DEFLATEDだと実際に圧縮してみて圧縮後サイズを調べる必要がある。
- Comment が空であること。実はZIPはファイル毎にコメントを入力出来る。
- ExtraField が存在しないこと。ライブラリによっては拡張フィールドを利用してより正確な日付情報を付けるなどする場合がある。
- DataDescriptor が存在しないこと。DataDescriptor はファイルデータの直後に追加される 12 or 16バイトのCRC・ファイルサイズ情報。
- 暗号化されていないこと。暗号化の為のフィールドが追加される為。
- ZIP64(64bit対応のZIP)でないこと。ヘッダ・フッタのサイズが変化する為。個々のファイルサイズ・合計ファイルサイズ・ファイル数が32bit以内なら問題無いはず。
実装例
maennchen/ZipStream-PHP を利用した例
<?php
require 'vendor/autoload.php';
# 送信したいファイル一覧
$files = glob("*.jpg");
$size = 22;
foreach($files as $path) {
$size += filesize($path);
$size += strlen($path) * 2;
$size += 76;
}
header("Content-Length:" . $size); //ZIPファイルサイズの計算値
# 以下はZipStream-PHPの処理
$options = new ZipStream\Option\Archive();
$options->setSendHttpHeaders(true);
$fileopt = new ZipStream\Option\File();
$fileopt->setMethod(ZipStream\Option\Method::STORE()); //無圧縮
$zip = new ZipStream\ZipStream('download.zip', $options);
foreach($files as $path) {
$zip->addFileFromPath($path, $path, $fileopt);
}
$zip->finish();
他の言語・ライブラリでも似たような感じで実装出来るハズ。Javaなら commons-compress が良いか。