特定のファイルをダウンロードさせるPHPスクリプト

特定のディレクトリ以下の有るファイルをブラウザからダウンロードさせるPHPスクリプト
たまに、PHP-users ML とか出てくる FAQ 的な話なんだけど、

  • Content-Disposition ヘッダの書式
  • output buffering のクリア
  • でかいファイルのときに readfile 使うとメモリ制限にひっかかるために chunk に分けて出力する

とか、細かいことがあるのでメモ。

<?php
define('FILE_DIR', 'files');

function quote_string($str) 
{
    $search = array('\\', '\"');
    $replace = array('\\\\', '\\\"');

    return str_replace($search, $replace, $str);
}

function path_join()
{
    $args = func_get_args();
    return implode(DIRECTORY_SEPARATOR, $args);
}

function readfile_chunked($filename, $chunksize=8192)
{
    $buffer = '';
    $cnt = 0;
    $fp = fopen($filename, 'rb');
    if ($fp == false) {
        return false;
    }

    while (!feof($fp)) {
        $buffer = fread($fp, $chunksize);
        echo $buffer;
        flush();
        $cnt += strlen($buffer);
    }

    fclose($fp);
    return $cnt;
}

function main() 
{
    $file = $_GET['file'];
    if (strstr('/', $file) !== false || $file[0] == '.') {
        header('HTTP/1.0 403 Forbidden');
        exit();
    }

    $filename = path_join(FILE_DIR, $file);

    if (!file_exists($filename)) {
        header('HTTP/1.0 404 Not Found');
        exit();
    } elseif (!is_readable($filename)) {
        header('HTTP/1.0 403 Forbidden');
        exit();
    }

    $content_disposition 
        = sprintf('attachment; filename="%s"', quote_string($file));

    header('Content-type: application/download');
    header('Content-Disposition: ' . $content_disposition);
    header('Content-Length: ' . filesize($filename));

    if (in_array($_SERVER['REQUEST_METHOD'], array('GET', 'POST'))) {
        flush();
        while (ob_get_level()) {
            ob_end_flush();
        }
        readfile_chunked($filename);
    }
}

main();
?>