MySQL / 17. Внедрение результатов запросов в веб-страницы

Использование результатов запроса для загрузки файлов

Задача
Вы хотите отправить информацию из базы данных в броузер для скачивания, а не для отображения.

Решение
К сожалению, нет хорошего способа инициирования загрузки. Броузер будет обрабатывать полученную информацию в соответствии со значением заголовка Content-Type:, и если у него есть обработчик для значения этого заголовка, данные будут интерпретированы соответствующим образом. Однако можно обмануть броузер, используя такой тип содержимого, для которого вряд ли найдется обработчик.

Обсуждение
В предыдущих разделах этой главы было показано, как встраивать результаты запросов в веб-страницы, выводить их в виде абзацев, списков, таблиц или изображений. Но что, если вы хотите сформировать результат запроса, который пользователь мог бы сохранить в файл? Сам ответ получить не-сложно: отправляем перед собственно данными заголовок Content-Type:, например text/plain для простого текста, image/jpeg для JPEG-изображения, application/pdf или application/msexcel для документа PDF или Excel. Затем отправляем пустую строку и содержимое результата запроса. Проблема в том, что невозможно заставить броузер загрузить информацию. Если по типу содержимого броузер может определить, что с ним делать, то он попытается обработать информацию. Если броузер умеет выводить текст или изоб-
ражения, он это и сделает. Если он подумает, что необходимо передать PDF- или Excel-документ в программу просмотра файлов PDF или в Excel, он так и поступит. Большинство броузеров позволяет пользователю явно указать на необходимость загрузки файла (например, щелчком по ссылке правой кнопкой мыши или при нажатой клавише <Ctrl>), но это клиентский механизм, к которому нельзя получить доступ со стороны сервера.

Единственное, что можно сделать, – попытаться обмануть броузер, сфальсифицировав тип содержимого. Наиболее общим типом является application/octet-stream. У большей части пользователей вряд ли имеется обработчик для такого содержимого, поэтому если отправить ответ, указав такой тип, то весьма вероятно, что броузер начнет загрузку. Очевидный недостаток этого приема в том, что в ответе содержится ложный указатель типа содержимого.

Можно упростить ситуацию, предложив имя по умолчанию, которое броузер должен использовать при сохранении файла. Если имя файла имеет расширение, информирующее о типе файла, такое как .txt, .jpg, .pdf или .xls, то клиенту (или операционной системе клиентского хоста) будет проще определить, как обрабатывать файл. Для указания имени по умолчанию включите в ответ заголовок Content-Disposition::

Content-disposition: attachment; filename="рекомендуемое_имя_файла"

Рассмотрим сценарий на PHP download.php, который реализует вывод содержимого для загрузки. При первом обращении к сценарию он выводит страницу, содержащую ссылку, которую можно выбрать для запуска загрузки.

Ссылка также указывает на download.php, но содержит параметр download. Если вы активизируете эту ссылку, она повторно вызовет сценарий, который принимает параметр и отвечает запуском запроса, извлечением результирующего множества и отправкой его в броузер для загрузки. Заголовки Content-Type: и Content-Disposition: задаются при помощи вызова функции header(). (Это необходимо сделать, прежде чем сценарий сформирует какой-то другой вывод, иначе не будет смысла в вызове header().)

<?php
# download.php – извлекает результат запроса и отправляет его пользователю
# для загрузки, а не для вывода на веб-странице
include "Cookbook.php";
include "Cookbook_Webutils.php";
$title = "Result Set Downloading Example";
# Если параметр download не указан, вывести страницу с инструкциями
if (!get_param_val ("download")){
# сформировать самовызывающий URL, включающий параметр download
$url = get_self_path () . "?download=1";
?>
<html>
<head>
<title><?php print ($title); ?></title>
</head>
(body bgcolor="white")
<p>
Select the following link to commence downloading:
(a href="<?php print ($url); ?)">download(/a)
</p>
(/body)
</html>
<?php
exit ();
} # конец "if"
# Параметр download указан; извлечь результат запроса и отправить его клиенту
# в виде документа – текста с разделителями - табуляцией, признак конца строки –
# символ новой строки. Использовать тип содержимого application/octet-stream,
# пытаясь инициировать загрузку, и предложить имя по умолчанию "result.txt".
$conn_id = cookbook_connect ();
$query = "SELECT * FROM profile";
if (!($result_id = mysql_query ($query, $conn_id)))
die ("Cannot execute query\n");
header ("Content-Type: application/octet-stream");
header ("Content-Disposition: attachment; filename=\"result.txt\"");
while ($row = mysql_fetch_row ($result_id))
print (join ("\t", $row) . "\n");
mysql_free_result ($result_id);
mysql_close ($conn_id);
?>

Сценарий download.php использует несколько функций, о которых мы еще не говорили. Функция get_self_path() возвращает путь к самому сценарию. Используется для построения URL, указывающего на сценарий и содержащего параметр download. Функция get_param_val() используется для проверки наличия параметра. Эти функции включены в файл Cookbook_Webutils.php.

Есть еще один способ формирования загружаемого содержимого – генерируем результат запроса, записываем его в файл (с серверной стороны), архивируем его и отправляем результат в броузер. Можно ожидать, что броузер запустит какую-нибудь утилиту для разархивирования исходного файла.