[PHP]passthruで巨大ファイルをダウンロードさせる


PHPで巨大なファイルをブラウザにbinary-streamとして返すのに苦労したのでその時のメモです。
結果、passthruで解決しました



PHPで巨大なファイルをダウンロードさせる方法としては、

  • fopen()でファイルを開いて
  • fread()でチョイチョイ読みながら出力

ってのがよくあります
大抵の場合はこれでOK(max_execution_timeの制限はあるが)なのですが、5GBを超える場合等、巨大なファイルの場合だとうまくいかないようです
CentOS6.x(32bit)での話ですが、途中までうまくいっているのにいつの間にか死んでました。。。
で、64bitのOSに変えてみみても、やっぱりダメ

うーむ。困った。

passthruで解決

readfile($filename)とか使ってみてもやっぱりダメ
で、最終的に、passthru($command)にたどり着きました
コードは以下

$uri = '/path/to/file';
passthru('/bin/cat ' . $uri);

catコマンドを使って、ファイルをクライアントに送出するわけです

まあ、こんな大きなファイルをダウンロードさせるのが間違いって気もしますが、請負い開発してるとどうしてもこういうのがテストケースででてくるんすよね
で、テストNGとなりドツボにはまる・・・

ちなみに、session_start()してる状態でこの処理を行うと、ダウンロード中にブラウザから別の操作(例えば別機能へのリンクをクリック)してもダウンロードが完了するまで待たされます
なぜならsession_start()でセッションがロックされている為、開放されるまで同一セッションのリクエストはサーバ側で待たされるのです
待たされないようにするには、session_write_close()を事前に行う必要があります
それ以外にも、レスポンスヘッダーやもろもろ考慮して、最終的なコードは以下

$uri = '/path/to/file';
set_time_limit(0);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($uri));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . @filesize($uri));  // 32bitOSだと、ファイルサイズが4GBを超えると0を返すはずなので注意
ob_end_flush();
session_write_close();    // session_start()中であれば必須
passthru('/bin/cat ' . $uri);

なお、Windows版PHPの場合はどーなのか知りません(catコマンドとか無いし)w