Как отдать пользователю файл скриптом
Зачастую бывает необходимость в том, чтобы сайт умел отдавать файлы не просто на скачивание, а поддерживать возможность скачивания в несколько потоков и докачки файла в случае обрыва соединения.
Для начала попробуем просто отдать файл браузеру:
header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename color:#FF0000">.basename($filename).'"'); readfile($filename); ?>
В этом примере мы сформировали два заголовка для браузера, первый из которых сообщает ему о типе содержимого (в данном случае — поток каких-то байтов), а второй заставляет его выдать нам окно с именем файла для его сохранения на локальном диске.
С поддержкой докачки
Заголовок Accept-Ranges: bytes , отправленный сервером, сообщает клиенту о том, что он может запрашивать данные с сервера фрагментами, указывая их смещение в байтах.
Зная эту возможность, браузер может передать серверу смещение в байтах, с которого необходимо начать передачу файла. Для этого браузер посылает заголовок Range :
где 500 — смещение в байтах от начала файла.
Сервер в свою очередь устанавливает переменную окружения 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: bytes от-до/размер
Поясню последний заголовок на примере: имеем файл размером 10000 байт, отдаем все, кроме первых 500 байт. Тогда заголовок будет выглядеть так:
Content-Range: bytes 500-9999/10000
// если файла нет 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 ( isset($_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 color:#FF0000">.basename($filename).'"'); header('Content-Type: application/octet-stream'); header('Last-Modified: '.$ftime); header('Accept-Ranges: bytes'); header('Content-Length: '.($fsize - $range)); header('Content-Range: bytes '.$range.'-'.($fsize - 1).'/'.$fsize); fpassthru($handle); fclose($handle); ?>
В несколько потоков
Если клиент скачивает в несколько потоков, он будет отправлять нам заголовки вида (для файла длиной 10000 байт):
- Range: bytes=0-499 — первые 500 байт
- Range: bytes=500-999 — вторые 500 байт
- Range: bytes=-500 или Range: bytes=9500- — последние 500 байт
- Range: bytes=500- или Range: bytes=-9500 — все, кроме первых 500 байт
- Range: bytes=0-0 — только первый байт
- Content-Range: bytes 0-499/10000 — отдаем первые 500 байт
- Content-Range: bytes 500-999/10000 — отдаем вторые 500 байт
- Content-Range: bytes 9500-9999/10000 — отдаем последние 500 байт
- Content-Range: bytes 500-9999/10000 — отдаем все, кроме первых 500 байт
- Content-Range: bytes 0-0/10000 — отдаем только первый байт
// если файла нет 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)); // пробуем открыть $handle = @fopen($filename, "rb"); // если не удалось if (!$handle) header ('HTTP/1.0 403 Forbidden'); exit; > // если запрашивающий агент поддерживает докачку if ( isset($_SERVER['HTTP_RANGE']) ) $range = $_SERVER['HTTP_RANGE']; $range = str_replace( 'bytes=', '', $range ); $range = explode( '-', $range ); if ( $range[0]=='0' && $range[1]=='0' ) // если bytes=0-0 $start = $stop = 0; > elseif ( !strlen( $range[0] ) ) // если bytes=-500 $start = $fsize - (int)$range[1]; $stop = $fsize - 1; > else // если bytes=500-999 или bytes=500- $stop = (int)$range[1]; if ( !$stop ) $stop = $fsize - 1; // bytes=500- $start = (int)$range[0]; if ( $start ) fseek( $fd, $start ); > $length = $stop - $start + 1; header('HTTP/1.1 206 Partial Content'); header('Content-Disposition: attachment; filename color:#FF0000">.basename($filename).'"'); header('Content-Type: application/octet-stream'); header('Last-Modified: '.$ftime); header('Accept-Ranges: bytes'); header('Content-Length: ' . $length); header('Content-Range: bytes '.$start.'-'.$stop.'/'.$fsize); echo fread($handle, $length); > else // запрашивающий агент не поддерживает докачку header('HTTP/1.1 200 OK' ); header('Content-Disposition: attachment; filename color:#FF0000">.basename($filename).'"'); header('Content-Type: application/octet-stream'); header('Last-Modified: '.$ftime); header('Accept-Ranges: bytes'); header('Content-Length: '.$fsize); fpassthru($handle); > fclose($handle); ?>
P.S. Этот код неполон, поскольку не обрабатывает мультидиапазонные запросы, когда клиент требует от сервера сразу несколько фрагментов:
Range: bytes=0-499,500-999,1000-1499
Отдаем файлы эффективно с помощью PHP
Когда вообще нужно отдавать файлы через PHP? Например, когда нужно собрать какую-то статистику скачиваний файла или осуществить запрет на скачивание файлов, позволив скачивать по ссылке только один раз и ограниченной группе пользователей.
Допустим, чтобы скачать какой-то файл, мы отправляем пользователя не на прямую ссылку с расположением файла:
а на адрес скрипта, который отдает пользователю файл через PHP:
/files/download.php?file=$file_id
Где, в параметр file, для скрипта можно передавать идентификатор файла, который требуется скачать, после чего можно выстраивать различные проверки и выдавать пользователю файл для скачивания:
Header('location: /uploads/files/' . $file);
Сделать это можно несколькими способами, о которых речь пойдет ниже.
Функция readfile()
Этот метод будем применять в специально созданной функции. Такая функция поможет отправлять даже большие файлы, PHP будет отдавать файл пользователю по частям. Функция ждет, когда файл будет прочтен и отдан.
/** * Отдача файла * Функция для отдачи файла через PHP * @param string $file путь к файлу на сервере * @return mixed */ function file_download($file) < if (file_exists($file)) < if (ob_get_level()) < ob_end_clean(); >header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($file)); readfile($file); return [ 'status' => 'success', 'message' => 'Файл успешно отдан' ]; >else < return [ 'status' =>'error', 'message' => 'Файл не найден' ]; > >
Чтение и отправка файла вручную
Эта функция аналогична той, которая описана выше, но для чтения и отдали файла используются: fopen, feof, fread, fclose. Функция ждет когда файл будет прочитан и отдан, также позволяет экономить память.
/** * Отдача файла * Функция для отдачи файла через PHP * @param string $file путь к файлу на сервере * @return mixed */ function file_download($file) < if (file_exists($file)) < if (ob_get_level()) < ob_end_clean(); >header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($file)); if ($fd = fopen($file, 'rb')) < while (!feof($fd)) < print fread($fd, 1024); >fclose($fd); > return [ 'status' => 'success', 'message' => 'Файл успешно отдан' ]; >else < return [ 'status' =>'error', 'message' => 'Файл не найден' ]; > >
Отдаем файл через сервер
Отдать файл можем не скриптом, через PHP, а с помощью Apache или Nginx. Отдача файла средствами сервера дает максимальное быстродействие, минимум потребляет памяти и ресурсов сервера.
Для Apache есть модуль XSendFile, который поможет с помощью специального заголовка сделать отправку файла Apache. В настройках хоста включитте директиву перехвата заголовка:
Функция для отправки файла будет следующей:
/** * Отдача файла * Функция для отдачи файла через PHP * @param string $file путь к файлу на сервере * @return mixed */ function file_download($file) < if (file_exists($file)) < header('X-SendFile: ' . realpath($file)); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); return [ 'status' =>'success', 'message' => 'Файл успешно отдан' ]; >else < return [ 'status' =>'error', 'message' => 'Файл не найден' ]; > >
Nginx умеет отправку файла из коробки, все что нужно, настроить конфиг, указав запрет на доступ к каталогу (my/path/protected/):
Функция отправки файла выглядит так:
/** * Отдача файла * Функция для отдачи файла через PHP * @param string $file путь к файлу на сервере * @return mixed */ function file_download($file) < if (file_exists($file)) < header('X-Accel-Redirect: ' . $file); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); return [ 'status' =>'success', 'message' => 'Файл успешно отдан' ]; >else < return [ 'status' =>'error', 'message' => 'Файл не найден' ]; > >