続・FuelPHPで巨大ファイルのダウンロードさせる
2013年05月01日 18時10分
前の記事でFuelPHPでのファイルのダウンロードが上手く動いたと書いたのですが本番環境だけ以下のようなエラーが出て悩んでおりました。
1 |
Fatal error: Exception thrown without a stack frame in Unknown on line 0 |
ダウンロードしたファイルの最後に上のエラーメッセージが付いてしまっていました。
ダウンロードファイルがzipファイルでエラーメッセージが付いていても正常に展開できていたので気付くのが遅れてしまいました。
後、ob_startが絡んでいるのかFuelPHPのログに出てなかったんですよね。
このエラーメッセージは何?とGoogle先生に聞いてみるとFuelPHPでこのエラーの場合はtimezoneが設定されていないのでconfig.phpで設定しましょうとばかり出てきます。
その設定は既にしているので今回は別原因です。
とりあえずはてぶさんのコメントにreadfileのドキュメントをよく読みましょう(拡大解釈)というコメントがあったので読んでみました。
PHP: readfile – Manual
http://www.php.net/manual/ja/function.readfile.php
確かに注意という項目に書いてあります。
1 2 |
注意: readfile() 自体にはメモリに関する問題はなく、 巨大なファイルを送ってもかまいません。 |
では何故out of memoryエラーが発生したのか。
答えは続きに書いてありました。
1 2 3 |
注意: readfile() 自体にはメモリに関する問題はなく、 巨大なファイルを送ってもかまいません。 out of memoryエラーが出る場合は、 ob_get_level() で出力バッファリングを無効にしてください。 |
確かに処理していた部分でob_get_level()をしてみると0ではなく2と帰ってきました。
ですのでご要望通りにバッファレベルを0にしてみたらreadfileだけで問題なくダウンロードできました。
1 2 3 4 5 6 7 |
$response = Response::forge(); $response->set_header('Content-Type', 'application/octet-stream'); $response->set_header('Content-Disposition', 'attachment; filename="download_file.zip"'); $response->send(true); while (@ob_end_flush()); File::read($download_file_path); // ← 2.5GBの巨大ファイルでもエラーなし! |
ですが最初に記載したエラーメッセージは相変わらずダウンロードしたzipファイルの最後に付いてきます。
ここで心が折れそうになりましたが今回再確認したマニュアルをよく読みましょうの原点に戻り、
FuelPHPのdocsを読んでみるとなんとFile::downloadというメソッドが用意されているではありませんか。
FuelPHP File Usage Classes
http://fuelphp.com/docs/classes/file/usage.html#/method_download
ということでその一行に修正。
1 2 3 4 5 6 7 |
- $response = Response::forge(); - $response->set_header('Content-Type', 'application/octet-stream'); - $response->set_header('Content-Disposition', 'attachment; filename="download_file.zip"'); - $response->send(true); - while (@ob_end_flush()); File::download($download_file_path, 'download_file.zip'); |
上記の修正で無事にエラーメッセージが付かない綺麗なzipファイルがダウンロードされました。
しかし今回の戦いはまだ続きます。
ダウンロードさせるだけならこれで問題ないのですがダウンロードさせる為に作成したファイルをダウンロード終了後に削除するという処理があるのです。
ファイルをダウンロード後にまた何か処理をしたい場合は自前でやるしか無理そうだったのでFile::downloadの中身を参考に以下のように実装してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
Event::register('shutdown', function() use($mime_type, $download_file_path, $display_name, $trigger) { $response = Response::forge(); $response->set_header('Content-Type', $mime_type); $response->set_header('Content-Disposition', 'attachment; filename="'.$display_name.'"'); // キャッシュ無し $response->set_header('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate'); $response->set_header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT'); $response->set_header('Pragma', 'no-cache'); $response->send_headers(); // 出力バッファレベルを0に変更 while (ob_get_level() > 0) { ob_end_clean(); } ob_start(); // 内容出力 if ($fp = fopen($download_file_path, 'rb')) { while(!feof($fp) and (connection_status() == 0)) { echo fread($fp, 8192); ob_flush(); } ob_flush(); fclose($fp); } ob_end_clean(); // ダウンロード後に何か処理したい場合はEvent::registerで登録しておく if (!is_null($trigger)) { Event::trigger($trigger, array( 'download_file_path' => $download_file_path, 'display_name' => $display_name )); } } ); |
これでダウンロード後にファイルを削除するという処理をEvent::registerで登録すれば望む形の処理を実装することができました。
何かとはまることの多い処理だったので同じことで悩んでる人の参考になれば幸いです。