"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

PHPとHTMLで作る/Webサーバーからのファイルダウンロード処理サンプル

f:id:arakan_no_boku:20220122133805p:plain

目次

Webサーバーからファイルをダウンロードするサンプル

Webサーバーからのファイルダウンロード機能を実装するサンプルです。

PHPHTML5を使います。

サーバーにファイルが既にあるか、どうかで以下の2パターンがあります。

  • サーバーに置いたファイルをダウンロード
  • サーバーでファイル(例:CSV)を生成してダウンロード

 

サーバーに置いたファイルをダウンロードするサンプル

PHPを使うパターンと、HTML5のみで実現する2パターンをやります。

  1. PHPで作るダウンロード処理
  2. HTML5のみで実現するダウンロード処理
1:PHPで作るダウンロード処理

サーバーに配置したファイルをダウンロードするPHPプログラムです。

<?php
 $file = "";
 // 対象ファイルの受け取り。GETまたはPOSTで渡される想定
 if (isset($_GET['file'])){
 	$file = $_GET['file'];
 }else{
 	if (isset($_POST['file'])){
 		$file = $_POST['file'];
 	}else{
 		die("ダウンロード対象ファイルが指定されていません."); 		
 	}
 }
 
 
 // ファイルの存在確認をして
 if (file_exists($file)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.basename($file).'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    while (ob_get_level()) { ob_end_clean(); }
    readfile($file);
    exit;
 }else{
 	die("ダウンロード対象ファイルが見つかりません");
 }
?>

必要最低限の処理だけですが、一部、補足します。

 while (ob_get_level()) { ob_end_clean(); }

は、大きなファイルで「readfile」が"out of memory"エラーを発生しないように出力バッファを破棄&無効にするために必要です。

Content-Type: application/octet-stream

を使っているのは「あえてファイルの種類を通知しない」意図です。

このへんは、用途によって変更を検討してください。

 

PHPプログラムを「GET」で使うサンプル

PHPをとりあえず「download_sample.php」という名前でHTMLと同じフォルダに保存してあり、ダウンロードするデータはドキュメントルートに「data/sample」というサブフォルダを作っておいた「samplezip.zip」とします。

リンクにURLを指定してダウンロードする(GET)場合は以下のように書きます。

<a href="http://localhost/download_sample.php?file=data/sample/samplezip.zip">PHP版:GETでパラメータを渡してファイルダウンロード</a>

対象ファイル名は「file」パラメータで指定してます。

 

PHPプログラムを「POST]で使うサンプル

同じPHPを使って、Submitでダウンロードする場合です。

対象ファイル名は「file」のhiddenタグにセットして渡します。

こんな感じ。

<form method="POST" action="download_sample.php">
<input type="hidden" name="file" value="data/sample/samplezip.zip" />
PHP版:POSTでパラメータを渡してダウンロード<input type="submit" />
</form>

 

2.HTML5のみで実現するダウンロード処理

PHPを使わないで、HTMLのタグだけでもできます。

HTML5限定ですけど、主要ブラウザはほぼ対応していますので、古いブラウザを捨ててもよければ十分使える方法です。

やり方は簡単で、<a></a>タグに「download」をつけるだけ。

<a href="data/sample/samplezip.zip" download>HTML5版:ファイル名を変更しない(普通)のダウンロード</a>
            

デフォルトファイル名を指定もできます。

download=の後ろに適用したいファイル名を書くだけです。

こんな感じ。

<a href="data/sample/samplezip.zip" download="20220122_samplezip.zip">HTML5版:ファイル名を変更してダウンロード</a>

こうすると、元ファイル名に関係なく「20220122_samplezip.zip」というファイル名でダウンロードしてくれます。

 

サーバーでファイル(例:CSV)を生成してダウンロード

PHPプログラムの中でデータベースから取得したデータでCSVファイルを作成してダウンロードする・・みたいなイメージの処理です。

2通りのやり方があります。

  1. 実ファイルを作成しないやり方
  2. 実ファイルを作成してからダウンロードするやり方

 

1.実ファイルを作成しないサンプル

fopen('php://output', 'w')を使う方法です。

これで生成したファイルハンドルに、データを書き込み、HTTPヘッダーをセットしてfflush()する(exit()してもいけますけど)と、ダウンロードされます。

さて、ソースは以下です。

データは「create_dummy_data()」で適当に生成しています。

<?php
function create_dummy_data(){
    $rows_ = array();
    for($i = 0; $i <= 100; $i++){
        $row_ = array('id'=>'9999999',
            'name'=>'XXXXXXXXXXXXXXXXXX',
            'addr'=>'YYYYYYYYYYYYYYYYYY',
            'date'=>'2022-04-01');
        array_push($rows_,$row_);
    }    
    return $rows_;
}

$fh = fopen('php://output', 'w');
ob_start();

// CSVファイルを作成r
$header = array('id', 'name', 'addr', 'date');
fputcsv($fh, $header);

$rows = create_dummy_data();
foreach ($rows as $row) {
    $line = array($row['id'], $row['name'], $row['addr'], $row['date']);
    fputcsv($fh, $line);
}

$filename = "test_download.csv";

header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '";');
header('Content-Transfer-Encoding: binary');

fflush($fh);
exit();

?>

このやり方は簡単ですが、ファイルサイズが大きくなると、「fputcsv()」でメモリアロケーションエラーを引き起こして、異常動作をおこします。

その挙動はいろいろです。

例えば、僕が経験したのは「書き込み中のファイルの内容がブラウザ上に表示されてしまう」とか「エラー画面に遷移して止まってしまう」とか・・でした。

どの程度のサイズでエラーになるのかも環境に依存します。

僕の経験では、本番環境で40万件程度でエラーの報告をうけて、再現テストするのにローカルに環境を作ったら再現するのに、120万件くらいのデータが必要だったことがありました。

だから、絶対に大きなファイルにならない見通しがないときは、この方法を採用するのはリスクが高いことを意識したほうがいいです。

 

2.実ファイルを作成してからダウンロードするサンプル 

上記の方法でエラーになったときの解決策です。

考え方は簡単です。

一回、ファイルを作ってしまってから、それをダウンロードするだけです。

ソースサンプルです。

<?php
function create_dummy_data(){
    $rows_ = array();
    for($i = 0; $i <= 100; $i++){
        $row_ = array('id'=>'9999999',
            'name'=>'XXXXXXXXXXXXXXXXXX',
            'addr'=>'YYYYYYYYYYYYYYYYYY',
            'date'=>'2022-04-01');
        array_push($rows_,$row_);
    }    
    return $rows_;
}

$filename = "c:/tmp/test_download.csv";
$fh = fopen($filename, 'w');

$header = array('id', 'name', 'addr', 'date');
fputcsv($fh, $header);

$rows = create_dummy_data();
foreach ($rows as $row) {
    $line = array($row['id'], $row['name'], $row['addr'], $row['date']);
    fputcsv($fh, $line);
}

fclose($fh);

// Output Http headers
header('Pragma: public');
header('Expires: 0');
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($filename).'"');
header('Cache-Control: must-revalidate');
header('Content-Length: ' . filesize($filename));
while (ob_get_level()) { ob_end_clean(); }
readfile($filename);

exit();

?>

この方法だとファイルサイズが大きくなっても対応できます。 

でも、実ファイルをテンポラリに作ってしまうので、その処理を考えておかないと知らぬ間にディスクが圧迫されていたなんてことになりかねないので注意が必要です。

テンポラリファイル名を固定にして上書き再利用するようにしたら、その心配はないですが、同時にダウンロード処理が起動されたときの競合の問題とかありますし・・、そのへんは運用にあわせて検討したほうがいいと思います。

 

以上・・ですかね。

ではでは。