CodeIgniterのダウンロードヘルパで大きめのファイルをダウンロードさせるヘルパを作ってみる

少し問題になるケースがあったので、ヘルパを拡張してみます。

そもそもの発端

動画ファイルなど、比較的サイズの大きいファイルをCodeIgniterのdownload_helperでダウンロードさせようとしたときに、
「メモリ、足りないよ」と怒られたので、何とかしたいな〜ということで作ってみました。

download_helperの挙動と実際の動作

CIのdownload_helperには「force_download」というヘルパ関数があり、引数を渡すだけで簡単にダウンロードしてくれる便利な関数があります。仕様は、

force_download('filename', 'data')

データを強制的にデスクトップにダウンロードさせるためのサーバヘッダを生成します。 ファイルのダウンロードで使えます。 第1引数には、ダウンロードファイルにつけたい名前を指定し、第2引数には、ファイルのデータを指定します。

CodeIgniterユーザガイド日本語版 - ダウンロードヘルパ部分抜粋


ということで、第二引数には実際のファイルのデータ(Binaryも含む)を渡すことで、ヘッダの付加などをやってくれます。

なので、実際のファイルデータをfile_get_contents()などで読み込んでデータを渡すのですが、この時ファイルサイズが著しく大きい、つまりphp.iniのmemory_limitの値より大きいサイズを読み込むと、当然メモリが足りなくてダウンロードできません。

それほど大きなファイルをDLさせるケースを想定していない、ということなのでしょうが、seezooでは動画ファイルとかも扱ったりするので、これだとエラーが出てDLできませんでした。困りました。

考えた方法と対策した関数を拡張して実装する

対策は簡単で、


DLを始める前に、そのファイルのサイズを調べてから、memory_limitより大きい場合はsplitしながらダウンロードさせればいい


のですが、結構手間がかかるので、もう一つ関数を追加します。以下、関数のコードです。
MY_download_helper.phpとか作るといいかもです。


/**
* ファイルサイズを調べてから、ダウンロードをさせる関数
*/
if ( ! function_exists('split_force_download'))
{
function split_force_download($filename = '', $filepath = '')
{
// same codeigniter code:
if ($filename == '' OR $filepath == '')
{
return FALSE;
}

// Try to determine if the filename includes a file extension.
// We need it in order to set the MIME type
if (FALSE === strpos($filename, '.'))
{
return FALSE;
}

// Grab the file extension
$x = explode('.', $filename);
$extension = end($x);

// get PHP memory_limit
$ini_max = trim(ini_get('memory_limit'));
switch ( strtolower($ini_max[strlen($ini_max)-1]) )
{
case 'g':
$max_size = intval($ini_max * 1024 * 1024 * 1024);
break;
case 'm':
$max_size = intval($ini_max * 1024 * 1024);
break;
case 'k':
$max_size = intval($ini_max * 1024);
break;
default :
$max_size = intval($ini_max);
}
$file_size = filesize($filepath);

// Load the mime types
@include(APPPATH.'config/mimes'.EXT);

// Set a default mime if we can't find it
if ( ! isset($mimes[$extension]))
{
$mime = 'application/octet-stream';
}
else
{
$mime = (is_array($mimes[$extension])) ? $mimes[$extension][0] : $mimes[$extension];
}

// Generate the server headers
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE"))
{
// Internet Explorer filename should be SHIFT_JIS encoded filename.
$filename = mb_convert_encoding($filename, 'SHIFT_JIS', 'UTF-8');

header('Content-Type: "'.$mime.'"');
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header("Content-Transfer-Encoding: binary");
header('Pragma: public');
header("Content-Length: ".$file_size);
}
else
{
header('Content-Type: "'.$mime.'"');
header('Content-Disposition: attachment; filename="'.$filename.'"');
header("Content-Transfer-Encoding: binary");
header('Expires: 0');
header('Pragma: no-cache');
header("Content-Length: ".$file_size);
}

// if filesize greater than max_memory_limit, split download
if ($ini_max > 0 && $max_size < $file_size)
{
flush();
$fp = fopen($filepath, 'rb');
while(!feof($fp))
{
echo fread($fp, 4096);
flush();
}
fclose($fp);
exit();
}
else
{
exit(file_get_contents($filepath));
}

}
}


作った、と言っても既存のforce_download()を少しいじっただけのものですが・・・。
関数名にいつも悩むのですが、「split_force_download」にしました。青の部分が追記している部分です。
黒字の部分はforce_downloadを流用しています(同じ処理)。

コードについてなど

まず赤字の部分、関数の第二引数には読み込んだデータではなく、DLさせたいファイルへのパスを渡します。重要です。
その後、対象ファイルのサイズと、現在動作している環境のmemory_limitのサイズを比較して、大きければflush()→fread()の繰り返しで分割しながらダウンロードを継続させます。
それ以外の場合は通常どおり、ファイルを読み込んでダウンロードします。

あと、IEでforce_downloadに通すと日本語のファイル名が文字化けすることがあったので、SHIFT_JISに変換してダウンロードさせるようにもしていますね。

分割しているからと言って負荷が軽くなっているわけではないと思います。そもそもmemory_limitの値を大きくすればいい話なのですが、
それを変更できない環境もあるかもしれないので・・・ということで。

感想とか

ファイルアップロードの時点では、upload_max_filesizeの設定値が使われますが、

memory_limit < upload_max_filesize

のケースでもアップロードは成功するんですねー(ファイルの中身は見ないですもんね)。
よくよく考えると生データを引数に受けてDLというのはどうなのかな?とも思ったりしましたが、あまり気にしないことにしました。

ファイル共有アプリとかでは使える関数かもしれないですね。seezooでも動画ファイルのDLにはこちらの関数を使ってDLさせています。
Zip圧縮クラスを併用すると幸せになれそうですね^^