27-12-2022

Отдаём файлы с помощью PHP, которые превышают memory_limit с помощью HTTP-протокола

Отдаём файлы с помощью PHP, которые превышают memory_limit с помощью HTTP-протокола
NJ Soft

Если ваш проект на PHP и перед отправкой файла (скачиванием пользователем файла) требуется проверить доступ к этому файлу у пользователя, а также не давать возможность скачивать «разные части» файла без доступа к этому файлу у пользователя, то реализовать это можно с помощью PHP скрипта/кода.

В самом простом случае можно отдать файл так

return file_get_contents($pathToBigVideoFile);

Где $pathToBigVideoFile — это путь до файл на сервере. Предположим, что ограничение на memory_limit = 128МБ, а размер файла = 256МБ. Тогда код выше будет выдавать ошибку

Fatal error: Allowed memory size

Чтобы обойти это ограничение, будем отдавать файл по частям, но нужно разобраться как сказать «браузеру» о таком способе передачи файла. И для этого воспользуемся знаниями о протоколе HTTP.

В RFC есть код ответа = 206 → https://httpwg.org/specs/rfc9110.html#status.206 , который позволяет сообщить клиенту (в нашем случае это будет браузер) о том что отдаваемый контент является «частичным». А чтобы перейти в «частичное представление» (т.е чтобы перевести браузер в режим частичного чтения контента), необходимо отдать специаильный заголовок Accept-Ranges → https://httpwg.org/specs/rfc9110.html#field.accept-ranges

Отдавая заголвок Accept-Ranges: bytes, мы сообщим клиенту инфомрацию о том что последующие ответы будут содержать код ответа 206 и будут частичными, на что клиент (браузер) должен отреагировать и отправлять нам заголовок Range.

Далее надо понять что будет содержаться в Range и как мы должны обработать этот заголвок своим PHP-скриптом.

Подробно про Range можно прочитать тут https://httpwg.org/specs/rfc9110.html#range.requests

Нас интересует byte ranges → https://httpwg.org/specs/rfc9110.html#byte.ranges . Это будут диапозоны байт которые клиент запрашивает у сервера, и по этим диапозонам мы сможем читать части файла на сервере и отдавать их клиенту.

Когда PHP-скрипт получит запрос с заголовком Range, то на этот заголовок нужно ответить заголовком Content-Range https://httpwg.org/specs/rfc9110.html#field.content-range в котором нужно будет указать диапозон в байтах, в котором содержится «содержимое файла» и задать 206 код ответа. А также «Content-Range»-заголовок необходимо указывать с «Content-length»- заголовокм, в котором будет размер файла в байтах или «*» если размер файла неизвестен.

Ещё важным моменто будет указание заголовка «Content-type» это прописано тут https://httpwg.org/specs/rfc9110.html#status.206 т.к помимо указания диапозона байт в котором находится контент (Content-Range) треубется ещё и указание mime-типа этого контента. Поэтому важно указать верный mime-тип. Если это mp4-видео файл, то без Content-type: video/mp4 браузер не запустит «режим partial content». Теперь всё это можно реализовать в PHP-скрипте. Ниже пример кода.


 отдаст ошибку Fatal error: Allowed memory size...
* http://0.0.0.0:8181/?stream=1 -> воспроизведёт файл
*/
ini_set('display_errors', 1);
ini_set('error_reporting', E_ALL);
// Ваш путь до файла
$pathToBigVideoFile = __DIR__ . '/video.mp4';
if(!file_exists($pathToBigVideoFile)) {
die("Файл не найден {$pathToBigVideoFile}");
}
$fileSize = filesize($pathToBigVideoFile);
if($_GET['stream'])
{
// 1. Говорим браузеру принимать ответ по частям
header('Accept-Ranges: bytes');
header('Content-Length', $fileSize);
header('Content-type: video/mp4');
// 2. Проверяем если браузер запрашивает по частям, отадём по частям
if(isset($_SERVER['HTTP_RANGE']) && $_SERVER['HTTP_RANGE'])
{
header("HTTP/1.1 206 Partial Content");
$start = 0;
$end = $fileSize - 1;
$range = $_SERVER['HTTP_RANGE'];
if (str_starts_with($range, 'bytes='))
{
$rangeExp = array_filter(explode('bytes=', $range));
[$start, $end] = explode('-', implode('', $rangeExp));
$start = (int) $start;
$end = (int) $end;
if($start > $end || empty($end))
{
$end = $fileSize - 1;
}
}
// Вычисляем на основе входных данных от браузера размера данных который нужно отдать клиенту
$chunkSizeBytes = $start - $end <= 0 ? 1024 : $start - $end;
header("Content-range: bytes {$start}-{$end}/{$fileSize}");
// 3. По частям читаем файл и отдаём даныне в буфер, по достижению размеров Content-range, браузер
// будет запрашивать контент по новой с другим Range
$fd = fopen($pathToBigVideoFile, 'r');
// move to begining
fseek($fd, $start);
ob_flush();
while(!feof($fd))
{
$content = fread($fd, $chunkSizeBytes);
echo $content;
flush();
}
fclose($fd);
}
die;
}
elseif($_GET['file_get_contents'])
{
return file_get_contents($pathToBigVideoFile);
}
echo "Размер memory_limit: ".ini_get('memory_limit'). "\n";
echo "Размер файла: " . filesize($pathToBigVideoFile). "\n";
die();
 

Чтобы проверить работу скрипта. В нёмдостаточно подставить корректный путь до файла в переменную $pathToBigVideoFile и запустить встроенный в PHP, а дальше пройти по ссылка в комментариях скрипта. Таким образом PHP работающий с минимальным использованием памяти способен отдавать большие файлы в браузер или любой HTTP-клиент, который поддерживает работу с partial content.

dev php