Рубрика «PHP»

Как отдать пользователю файл скриптом

Я уже затрагивал эту тему в одной из предыдущих заметок. Сегодня мы рассмотрим этот вопрос более подробно - не только как отдать файл на загрузку, но и как реализовать поддержку докачки.

Файл download.php

<?php
if(isset($_SERVER['HTTP_USER_AGENT']) and strpos($_SERVER['HTTP_USER_AGENT'],'MSIE'))
  header('Content-Type: application/force-download');
else
  header('Content-Type: application/octet-stream');
header('Accept-Ranges: bytes');
header('Content-disposition: attachment; filename="pricelist.zip"');
?>

Тогда пользователь зашедший на download.php предстанет перед выбором, что делать с файлом pricelist.zip: сохранить на диск, открыть или отменить скачивание. Заголовок

Content-Type: application/force-download

нужен для старых версий MS IE.

С поддержкой докачки:

<?php

$filename = 'pricelist.zip';
 
// если файла нет
if (!file_exists($filename)) {
  header ("HTTP/1.0 404 Not Found");
  exit;
}
 
// получим размер файла
$fsize = filesize($filename);
// дата модификации файла для кеширования
$ftime = date("D, d M Y H:i:s T", filemtime($filename));
// смещение от начала файла
$range = 0;
 
// пробуем открыть
$handle = @fopen($filename, "rb");

// если не удалось
if (!$handle){
  header ("HTTP/1.0 403 Forbidden");
  exit;
}
 
// Если запрашивающий агент поддерживает докачку
if ($_SERVER["HTTP_RANGE"]) {
  $range = $_SERVER["HTTP_RANGE"];
  $range = str_replace("bytes=", "", $range);
  $range = str_replace("-", "", $range);
  // смещаемся по файлу на нужное смещение
  if ($range) fseek($handle, $range);
}
 
// если есть смещение
if ($range) {
  header("HTTP/1.1 206 Partial Content");
} else {
  header("HTTP/1.1 200 OK");
}
 
header("Content-Disposition: attachment; filename=\"{$filename}\"");
header("Last-Modified: {$ftime}");
header("Content-Length: ".($fsize-$range));
header("Accept-Ranges: bytes");
header("Content-Range: bytes {$range}-".($fsize - 1)."/".$fsize);
 
// подправляем под IE что б не умничал
if(isset($_SERVER['HTTP_USER_AGENT']) and strpos($_SERVER['HTTP_USER_AGENT'],'MSIE'))
  Header('Content-Type: application/force-download');
else
  Header('Content-Type: application/octet-stream');
 
while(!feof($handle)) {
  $buf = fread($handle,512);
  print($buf);
}
 
fclose($handle);
 
?>

Чтобы получить от сервера не весь документ, а только оставшуюся часть, клиент должен отправить заголовок

Range: bytes=num-

где num - смещение в байтах от начала файла.

Сервер в свою очередь устанавливает переменную окружения HTTP_RANGE и должен отправить заголовок

HTTP/1.1 206 Partial Content

который дает клиенту понять, что отдается часть контента. Далее сервер должен отдать клиенту ту часть контента, которую тот запрашивал с соответствующими заголовками:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename=имя_файла
Last-Modified: время_модификации_файла
Accept-Ranges: bytes
Content-Length: длина_отдаваемой_части
Content-Range: от-до/размер

Поясню последний заголовок на примере: имеем файл размером 4000 байт, отдаем кусок с 5 по 288 байт. Тогда заголовок будет выглядеть так:

Content-Range: 5-288/4000

В общем случае, протокол HTTP 1.1 позволяет клиенту запросить только часть объекта (от и до). Если клиент посылает заголовок

Range-Unit: 2015 | 1024

это означает, что клиент хочет получить кусок документа, начиная с 2015 байта и длиной в 1 килобайт. Если сервер поддерживает докачку и документ не является динамическим, то будет выдана запрашиваемая часть. В противном случае сервер вернёт ошибку о том, что действие не поддерживается или начнёт выдавать документ полностью.

Кэширование в HTTP

Механизм кэширования применяется с целью опртимизации пересылки данных между клиентом и сервером. Запрошенный пользователем по HTTP-протоколу документ может быть сохранен в кэше промежуточного сервера или браузера, и при повторном его запросе будет выдаваться без обращения к источнику.

Кэши принято подразделять на два вида: локальные и глобальные. Локальный кэш создается браузером клиента, тогда как глобальный располагается на прокси-сервере провыйдера (или организации, в которой имеется свой внутренний прокси-сервер).

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

В большинстве случаев кэширование позволяет ускорить работу с Интернетом и значительно снизить трафик, но иногда кэширование может мешать работе web-приложений. Если посетители часто загружают страницы web-сайта с редко изменяющейся информацией, кэширование полезно, поскольку экономит трафик и сокращает время выполнения запроса. Если же посетитель загружает динамические страницы, например, активного форума, кэширование, без сомнения вредно.

Для подавления кэширования можно использовать HTTP-заголовки:

<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Дата в прошлом
header("Last-Modified: ".gmdate("D, d M Y H:i:s")."GMT");
header("Cache-Control: no-cache, must-revalidate"); // HTTP 1.1
header("Pragma: no-cache"); // HTTP 1.0
?>

HTTP-заголовок Expires задает дату, по достижению которой документ считается устаревшим, поэтому задание для этого заголовка уже прошедшей даты предотвращает кэширование данной страницы.

HTTP-заголовок Last-Modified определяет дату последнего изменения страницы. Если с момента последнего обращения к ней прокси-сервера значение этого заголовка изменилось, происходит происходит повторная загрузка страницы, поэтому присвоение этой директиве текущего времени прикаждом обращении предотвращает кэширование.

HTTP-заголовок Cache-Control предназначен для управления кэшированием, и указание его значения равным no-cache также приводит к запрету кэширования. В устаревшем стандарте HTTP 1.0 для запрета кэширования нужно присвоить значение HTTP-заголовку Pragma.

Уменьшение количества цветов в изображении

Часто для оптимизацации изображения или достижения плакатного эффекта количество цветов в гамме сокращают. В библиотеке GDLib для этого предназначена функция imagetruecolortopalette(). Напишем небольшой скрипт, который принимает единственный параметр $_GET['color'], равный количеству цветов в конечном изображении.

<?php
if ( !isset( $_GET['color'] ) )
  $color = 256;
else
  $color = (int)$_GET['color'];
// Проверяем корректность параметра $_GET['color']
if( $color < 2 ) $color = 2;
if( $color > 256 ) $color = 256;
// Создаем изображение на основе существующего
$image = imagecreatefromjpeg( 'image.jpg' );
if( $image ) {
  // Уменьшаем количество цветов
  imagetruecolortopalette( $image, true, $color );
  // Отправляем HTTP-заголовок Content-type
  header( 'Content-type: image/gif' );
  // Выводим изображение в браузер
  imagegif( $image );
  // Уничтожаем изображение в памяти
  imagedestroy( $image );
}
?>

Файл index.html демонстрирует работу скрипта:

<img src="image.jpg" alt="" />
<img src="image.php?color=60" alt="" /><br/>
<img src="image.php?color=30" alt="" />
<img src="image.php?color=15" alt="" /><br/>
<img src="image.php?color=5" alt="" />
<img src="image.php?color=2" alt="" />