Laravelで複数ファイルをZIPでまとめてストリーミングダウンロードさせる方法

はじめに

ファイルを大量にまとめてダウンロードさせたかった時の話。

高スペックなサーバなら普通にZipArchiveを使ってZIPファイルを作成してからダウンロードさせればいいけど、低スペックなサーバで大量のファイルをZIPにしようとすると普通にファイルサイズ分のメモリを消費し、メモリー不足で止まってしまう。ZIP格納だけでなく圧縮もしようとするとCPUの消費も半端ない。

そこで、できるだけメモリとCPUを消費しない方法を調べてみた。

すると、ZIPファイルを作成しながら出来たところから同時にストリーミング形式でダウンロードさせると良いという事がわかりました。そして「stechstudio/laravel-zipstream」というLaravel用のライブラリを発見。これは、「maennchen/ZipStream-PHP」というライブラリをLaravel仕様にしてくれたものらしい。

今回は、このライブラリを使ってメモリとCPU消費を抑えた、ZIPのストリーミングダウンロードを実装してみたいと思います。

stechstudio/laravel-zipstreamの使い方

まずはライブラリをインストール。

あとは以下のようにするだけで簡単に実装できる。(公式から引用)

ポイントは、create()を直接returnしているところ。これによってストリーミングされるっぽい。

一度作成されたZIPに以下のようにadd()を使ってファイルを追加することもできるが、この場合はストリーミングされなかった。

サンプル

自分は以下のように実装してみました。

ZIPに格納するファイルの名前を個別に設定したい場合は、$filePathsを連想配列にして、keyにパス、valueにファイル名を入れて渡す。

ファイルパスは、「https://~」のURLでも可で、AWSのS3にも対応している(AWSのSDKが必要)。また、生のファイルデータを直接渡すこともできるらしい。

補足

デフォルトでは、ファイルを圧縮せずにそのままZIPに格納される。圧縮するようにもできるがその場合はCPUの消費が大きくなるので今回はしない。他にオプションとして以下の機能がある。

作成したZIPファイルをサーバ内に保存

キャッシュファイルを残してレジュームダウンロードさせる

その他の機能

HTTPヘッダーを自動で付けてくれるので自分で作って付けなくても良い。さらに、ZIP完成後のファイルサイズを自動計算してくれてヘッダーに付けてくれるので、ユーザーはダウンロード時にブラウザで進捗状況を確認できる。圧縮オプションを有効にした場合は確認できない。

あと、ダウンロード開始イベントや完了イベントも受け取れるらしい。今回は実装しないので必要な方は公式マニュアルを参照してください。

ファイル名に日本語を使いたい場合

とても良いライブラリなのですが、格納するファイルの名前に日本語が含まれている場合、ファイル名から削除されてしまいます。どうしても日本語に対応させたかったので、ファイル名を扱っている部分を探して修正してみました。

ファイル名を扱っている箇所は、「vendor/stechstudio/laravel-zipstream/src/Models/File.php」でした。以下の箇所を以下のように修正してみました。(93行目あたり)

おそらくファイル名にディレクトリを含んでいる場合の処理などが入っているが、今回は使わないのでそのまま返すようにしたら日本語が消されなくなった。文字コードを変換する必要がある場合や、何か処理をしたい場合はここに書いたら良さそう。必要に応じて書き換えてみてください。

vendor内ファイルのオーバーライド

上記の修正をすると一応日本語対応になるのですが、修正ファイルがvendor内なので本番環境へデプロイした時などは反映されません。そこで、今回のライブラリディレクトリをすべてappディレクトリ以下にコピーしてから修正を行います。(「Override」というディレクトリを作ってそこへ入れました)

そして、File.phpをLaravelが見に行く時に、vendor内ではなくapp内のFile.phpを見に行くように「composer.json」のautoloadに以下のように追記します。

「composer.json」修正後は、以下のコマンドで反映させます。

以上で完了です。