Скачивание файлов в PHP

Многие ресурсы используют файловые хранилища. Кроме возможностей загрузки и хранения файлов, бывает необходимо организовать их скачивание. Одно дело когда файлы лежат в открытом доступе, но и тогда может потребоваться передача файла через PHP. Например, администратору ресурса может быть нужна информация о количестве скачиваний. Для файлов большого объема до сих пор требуется возможность докачки, что пожалуй и является самым трудным моментом для серверных скриптов. Посмотрим как можно организовать работу скрипта на PHP, который позволяет реализовать все вышеуказанные возможности.

Начнем с простого способа. Пусть наш скрипт получает имя файла через какой-либо из параметров запроса. Это может быть реально набранный URL, а может быть и переписанный сервером при помощи mod_rewrite. Скрипт вызывает функцию file_download() с параметром $filename. Кроме прямой передачи в запросе $filename может также вычисляться на основе исходного идентификатора из запроса или дополняться путем в дереве папок сервера.

Самый легкий способ обработки запросов к скачиваемым файлам — это простое перенаправление на них.

function file_download($filename) {
// Проверяем существование файла
  if (file_exists($filename)) {
// Здесь пишем код, который будет обрабатывать каждую загрузку файла.

//  Перенаправляем клиента на файл.
    header(‘Location: ‘ . $filename);
  } else {
// Если файл не найден, сообщаем клиенту об этом через заголовки HTTP
    header($_SERVER[«SERVER_PROTOCOL»] . ‘ 404 Not Found’);
    header(‘Status: 404 Not Found’);
  }
// Прерываем дальнейшее выполнение скрипта, чтобы не отправлять мусор в ответе клиенту
  exit;
}

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

Для этого потребуется усложнить нашу функцию закачки.

function file_download($filename, $mimetype=’application/octet-stream’) {
  if (file_exists($filename)) {
// Отправляем требуемые заголовки
    header($_SERVER[«SERVER_PROTOCOL»] . ‘ 200 OK’);
// Тип содержимого. Может быть взят из заголовков полученных от клиента
// при закачке файла на сервер. Может быть получен при помощи расширения PHP Fileinfo.
    header(‘Content-Type: ‘ . $mimetype);
// Дата последней модификации файла       
    header(‘Last-Modified: ‘ . gmdate(‘r’, filemtime($filename)));
// Отправляем уникальный идентификатор документа,
// значение которого меняется при его изменении.
// В нижеприведенном коде вычисление этого заголовка производится так же,
// как и в программном обеспечении сервера Apache
    header(‘ETag: ‘ . sprintf(‘%x-%x-%x’, fileinode($filename), filesize($filename), filemtime($filename)));
// Размер файла
    header(‘Content-Length: ‘ . (filesize($filename)));
    header(‘Connection: close’);
// Имя файла, как он будет сохранен в браузере или в программе закачки.
// Без этого заголовка будет использоваться базовое имя скрипта PHP.
// Но этот заголовок не нужен, если вы используете mod_rewrite для
// перенаправления запросов к серверу на PHP-скрипт
    header(‘Content-Disposition: attachment; filename=»‘ . basename($filename) . ‘»;’);
// Отдаем содержимое файла
    echo file_get_contents($filename);
  } else {
    header($_SERVER[«SERVER_PROTOCOL»] . ‘ 404 Not Found’);
    header(‘Status: 404 Not Found’);
  }
  exit;

Теперь мы можем скрыть реальный адрес файла, взять его из папки, недоступной для браузеров и программ закачки. Недостатком данной функции является полная загрузка всего файла в память, что при больших его размерах может привести к ее переполнению. Чтобы избежать этой проблемы, можно считывать файл блоками небольшого размера.

function file_download($filename, $mimetype=’application/octet-stream’) {
  if (file_exists($filename)) {
    header($_SERVER[«SERVER_PROTOCOL»] . ‘ 200 OK’);
    header(‘Content-Type: ‘ . $mimetype);
    header(‘Last-Modified: ‘ . gmdate(‘r’, filemtime($filename)));
    header(‘ETag: ‘ . sprintf(‘%x-%x-%x’, fileinode($filename), filesize($filename), filemtime($filename)));
    header(‘Content-Length: ‘ . (filesize($filename)));
    header(‘Connection: close’);
    header(‘Content-Disposition: attachment; filename=»‘ . basename($filename) . ‘»;’);
// Открываем искомый файл
    $f=fopen($filename, ‘r’);
    while(!feof($f)) {
// Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
      echo fread($f, 1024);
      flush();
    }
// Закрываем файл
    fclose($f);
  } else {
    header($_SERVER[«SERVER_PROTOCOL»] . ‘ 404 Not Found’);
    header(‘Status: 404 Not Found’);
  }
  exit;
}