hogehoge foobar Blog Style Beta

Web,Mac,Linux,JavaScript,Perl,PHP,RegExp,Git,Vim,Redmineなど技術的なことのメモや、ちょっと便利そうなものの紹介をしています。

PHPでファイルをダウンロードさせる方法

サイトからファイルをダウンロードさせるには、「<a href="ダウンロードさせたいファイル名"・・・」のようにファイルに直接リンクさせることでも可能ですが、以下の2点が気になります。

  • ダウンロードさせるファイルは公開ディレクトリ(htdocsとかhtml以下)におく必要がある。
  • ブラウザによっては「右クリック→名前をつけて保存」など操作がちょっと面倒。

これを解消するために、PHPを通してファイルをダウンロードさせる方法をやってみました。

PHPのちょっとしたTIPSさんのページの様に「try/catch」を使わなくても実装出来るのですが、今回はあえて「try/catch」を使用した実装を試してみました。

エラーハンドラの設定

まず、ダウンロードをさせるにあたってファイル操作を行うので、ファイルオープン等の例外をキャッチするためにエラーハンドラを設定します。
ユーザー定義のエラーハンドラとして、例外をスローさせる関数を作成します。(throw new Exception)
あとは作成した関数を「set_error_handler」でエラーハンドラとして設定します。

<?php
/* 無名関数を使用する場合(PHP5.3以降のみ可) */
$errHandler = function($errno, $errstr, $errfile, $errline){
    throw new Exception($errstr, $errno);
};
set_error_handler($errHandler);

/* 通常の関数を使用する場合 */
function errHandler($errno, $errstr, $errfile, $errline){
    throw new Exception($errstr, $errno);
}
set_error_handler('errHandler');
?>

ダウンロード可能なディレクトリとファイル拡張子のチェック

PHPからのダウンロードの場合、公開ディレクトリ(htdocs,html)以外のディレクトリからもファイルをダウンロードすることが出来てしまいます。
そこで、特定のディレクトリにあるファイル以外はダウンロードさせないように、ダウンロードが可能なディレクトリと、ダウンロードが可能なファイル拡張子のチェックを行ないます。
正規表現を使用してチェックを行ないますが、ディレクトリパスの判定として「/(スラッシュ)」を使用するため、正規表現のデリミタを「#」にしています。

特定のディレクトリ以外のファイルをダウンロードしようとした場合(チェックでエラーとなった場合)は、「trigger_error」を使用してエラーメッセージ('Error: File is not permitted.')と共に例外をスローします。

<?php
/* Download Directory */
define("DOWNLOAD_DIR", "/var/download");

/* Directory and Extension Check */
if ( !preg_match('#^'.DOWNLOAD_DIR.'.*\.(tsv|csv|txt)$#', $file) ) {
    trigger_error('Error: File is not permitted.');
}
?>

ダウンロードファイルの読み込み&出力

「file_get_contents」を使用してファイルの中身(データ)を一括で読み込みます。ここでファイルがオープンできない場合は自動的に例外がスローされます。

「file_get_contents」でのファイルデータの読み込みに成功したら、ダウンロード用のHTMLヘッダーを出力します。
出力するHTMLヘッダーは以下の通りになります。

HTMLヘッダー名 設定値
Content-Disposition inline; filename="ダウンロードするファイル名(パス無し)"
Content-Length ダウンロードさせるファイルのバイト数
Content-Type application/octet-stream(固定)

HTMLヘッダー出力後は、「file_get_contents」を使用して読み込んだファイルデータを「echo」で出力します。

<?php
/* File Read */
$read_data = file_get_contents($file);

/* Output HTTP Header */
header('Content-Disposition: inline; filename="'.basename($file).'"');
header('Content-Length: '.$content_length);
header('Content-Type: application/octet-stream');

/* Output File Data */
echo $read_data;
?>

catch部分

try/catchのcatchの部分です。
例外となった場合、ここにExceptionクラスのインスタンスが渡されてきます。Exceptionクラスの「getMessage()」を使用してエラーメッセージを取得します。
エラーメッセージについては、HTMLとして出力されることを考慮してを「htmlentities」でHTMLエスケープし、「die」に渡します。

ちなみに「die」は「exit」のエイリアスとなっています。

<?php
catch ( Exception $e ) {
    /* Process end. HTML Escapes in the Error message. */
    die( htmlentities($e->getMessage()) );
}
?>

サンプルコード(全体)

上記の内容をすべて踏まえたサンプルコードが以下になります。

<?php
/* Download Directory */
define("DOWNLOAD_DIR", "/var/download");

/*
 * File Download Function
 * @param string $file (AbsolutePath + FileName)
 * @return none
 */
function download($file)
{
    $errHandler = function($errno, $errstr, $errfile, $errline){
        throw new Exception($errstr, $errno);
    };
    set_error_handler($errHandler);

    try {
        /* Directory and Extension Check */
        if ( !preg_match('#^'.DOWNLOAD_DIR.'.*\.(tsv|csv|txt)$#', $file) ) {
            trigger_error('Error: File is not permitted.');
        }
        /* File Existence Check */
        else if ( !file_exists($file) ) {
            trigger_error('Error: File('.$file.') does not exist');
        }
        /* File Size 0(Zero) Check */
        else if ( ($content_length = filesize($file)) == 0 ) {
            trigger_error('Error: File size is 0.('.$file.')');
        }
        /* File Read */
        $read_data = file_get_contents($file);

        /* Output HTTP Header */
        header('Content-Disposition: inline; filename="'.basename($file).'"');
        header('Content-Length: '.$content_length);
        header('Content-Type: application/octet-stream');

        /* Output File Data */
        echo $read_data;
        #unlink($file);
    }
    catch ( Exception $e ) {
        /* Process end. HTML Escapes in the Error message. */
        die( htmlentities($e->getMessage()) );
    }

}
?>

今回参考にしたページ

任意のファイルをダウンロードさせる - PHPのちょっとしたTIPS
http://www.spencernetwork.org/memo/tips-5.php

PHPでクリックした時にファイルをダウンロードさせる設定 - #OPQR.jp
http://opqr.jp/2007/09/php.html

/(スラッシュ)を含む正規表現を見やすくする方法。- kimihiko Tech.htm
http://tech.kimihiko.jp/article/13153562.html

PHPの例外ってどれぐらい使われているのでしょうか - 一人WEBサービス屋メモ
http://d.hatena.ne.jp/uratch/20100303/1267587165