Передача файлов из PHP: подходы и реализация
Основные подходы к передаче файла в PHP
Как организовать скачивание файла с помощью readfile?
Наиболее эффективный способ передачи файла в PHP для скачивания - использование функции readfile в сочетании с правильными HTTP-заголовками. Этот метод читает файл и сразу отправляет его в вывод, не загружая всё содержимое в память.
<?php
$file = 'document.pdf';
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
} else {
http_response_code(404);
echo 'Файл не найден.';
}
?>
Php передать файл (передача файла в php (output, скачивание))
Код проверяет существование файла, устанавливает заголовки для корректной передачи (тип контента, имя файла в Content-Disposition, размер), затем вызывает readfile. После отправки скрипт завершается с помощью exit, чтобы не выводить лишние данные.
Типичная ошибка: если перед вызовом header уже был выведен текст (например, пробел или HTML), браузер не примет заголовки. Решение - убедиться, что нет вывода до первого вызова header, или использовать буферизацию.
Как передать файл через fopen и fpassthru?
Если необходимо более детально контролировать процесс чтения (например, отправлять данные частями), используется связка fopen, fread и fpassthru. Это полезно при работе с файлами, не поддерживаемыми readfile (например, потоки).
<?php
$file = 'video.mp4';
$handle = fopen($file, 'rb');
if ($handle) {
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename="video.mp4"');
header('Content-Length: ' . filesize($file));
fpassthru($handle);
fclose($handle);
exit;
}
?>
Php загрузки файлов на сервер (загрузка файлов на сервер в php)
Функция fpassthru читает оставшиеся данные из потока до конца и выводит их. Альтернатива - последовательные вызовы fread в цикле для ограничения потребления памяти.
Проблема: при очень больших файлах (гигабайты) однократный вызов fpassthru может превысить лимит времени выполнения. Решение - увеличить max_execution_time или передавать данные частями с помощью fread.
Как избежать переполнения памяти при передаче больших файлов?
Для файлов размером более нескольких десятков мегабайт не рекомендуется загружать всё содержимое в память. Вместо file_get_contents или readfile (который всё равно читает сразу) используют чтение блоками.
<?php
$file = 'large_file.iso';
$chunk_size = 1024 * 1024; // 1 МБ
$handle = fopen($file, 'rb');
if ($handle) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));
while (!feof($handle)) {
echo fread($handle, $chunk_size);
ob_flush();
flush();
}
fclose($handle);
exit;
}
?>
Php files mysql (работа с файлами и mysql в php)
Такой подход позволяет отправлять файл постепенно, снижая нагрузку на память. Важно вызывать ob_flush и flush для немедленной отправки данных браузеру.
Ошибка: если не отключать буферизацию вывода (например, через ob_end_flush), данные будут накапливаться в буфере и не отправляться. Решение - вызвать ob_implicit_flush(true) или использовать ob_end_clean перед циклом.
Как реализовать поддержку докачки файла (HTTP Range)?
Для частичной загрузки (например, в менеджерах загрузок) необходимо обрабатывать заголовок Range и отправлять только запрошенный фрагмент. Это требует чтения с позиции и отправки соответствующих заголовков.
<?php
$file = 'archive.zip';
$file_size = filesize($file);
$handle = fopen($file, 'rb');
if (!$handle) { die('Ошибка открытия файла'); }
header('Accept-Ranges: bytes');
if (isset($_SERVER['HTTP_RANGE'])) {
preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches);
$start = intval($matches[1]);
$end = $matches[2] === '' ? $file_size - 1 : intval($matches[2]);
fseek($handle, $start);
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $file_size);
header('Content-Length: ' . ($end - $start + 1));
} else {
header('Content-Length: ' . $file_size);
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="archive.zip"');
$chunk = 8192;
$pos = $start;
while ($pos <= $end) {
$size = min($chunk, $end - $pos + 1);
echo fread($handle, $size);
$pos += $size;
}
fclose($handle);
?>
Обработка диапазона позволяет возобновлять скачивание после обрыва. Для упрощения можно использовать готовые библиотеки, но данный пример демонстрирует основную логику.
Типичная ошибка: неправильный расчёт Content-Length при частичном ответе приводит к зависанию загрузки. Необходимо точно вычислить количество байт от $start до $end включительно.
Как использовать X-Sendfile для быстрой отдачи файла?
Если веб-сервер (Apache с mod_xsendfile, Nginx, Lighttpd) поддерживает заголовок X-Sendfile, можно делегировать отправку файла серверу. Это снижает нагрузку на PHP.
<?php
$file = '/path/to/protected/secret.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="secret.pdf"');
header('X-Sendfile: ' . $file);
exit;
?>
Сервер самостоятельно вычитывает файл и отправляет его клиенту. PHP при этом не расходует память на большой файл. Требуется включение модуля mod_xsendfile или аналогичной возможности в Nginx.
Проблема: если модуль не активирован, заголовок будет выведен как обычный, а файл не отправится. Рекомендуется проверять наличие заголовка и применять резервный метод, например, readfile.
Как вывести файл с переименованием без смены имени на сервере?
Установка атрибута filename в заголовке Content-Disposition позволяет задать произвольное имя, которое будет предложено пользователю при скачивании.
<?php
$file = 'data_202503.csv';
$download_name = 'report_' . date('Ymd') . '.csv';
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . $download_name . '"');
header('Content-Length: ' . filesize($file));
readfile($file);
?>
Файл на сервере остаётся с исходным именем, а клиент получает файл с новым именем. Это удобно для автоматического формирования меток даты или версий.
Расширенные примеры передачи файлов в PHP
Ниже представлены более сложные сценарии, включая безопасную раздачу, передачу файлов из БД, использование потоков и работу с кастомными источниками.
Пример 1. Защищённое скачивание только для авторизованных пользователей
<?php
session_start();
if (!isset($_SESSION['user'])) {
http_response_code(403);
die('Доступ запрещён');
}
$file = 'private_data.zip';
if (!file_exists($file)) {
http_response_code(404);
die('Файл не найден');
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="private_data.zip"');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
?>
Результат: скрипт проверяет сессию, и только при наличии авторизации отдаёт файл. В противном случае возвращается код 403.
Пример 2. Отдача файла из базы данных (BLOB)
Иногда файлы хранятся непосредственно в базе (например, в MySQL). Извлечение и отправка потребует чтения всего содержимого в память, что ограничивает размер.
<?php
$id = (int)$_GET['id'];
$pdo = new PDO('mysql:host=localhost;dbname=files', 'user', 'pass');
$stmt = $pdo->prepare('SELECT name, type, size, data FROM files WHERE id = ?');
$stmt->execute([$id]);
$file = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$file) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $file['type']);
header('Content-Disposition: attachment; filename="' . $file['name'] . '"');
header('Content-Length: ' . $file['size']);
echo $file['data']; // данные из BLOB
?>
Для больших BLOB-полей лучше использовать потоки (например, PDO::ATTR_STRINGIFY_FETCHES не поможет). В таких случаях файл лучше хранить на диске, а в БД - путь.
Пример 3. Передача файла с использованием потокового фильтра
Можно передавать не исходный файл, а его обработанную версию, например, сжатый gzip на лету или с заменой строк.
<?php
$file = 'log.txt';
header('Content-Type: application/gzip');
header('Content-Disposition: attachment; filename="log.txt.gz"');
$handle = fopen($file, 'rb');
$gz = gzopen('php://output', 'wb9');
stream_copy_to_stream($handle, $gz);
gzclose($gz);
fclose($handle);
?>
В этом примере данные из файла log.txt сжимаются с помощью gzopen и сразу отправляются клиенту как gzip-архив без создания промежуточного файла.
Пример 4. Использование cURL для проксирования скачивания с удалённого сервера
Если файл находится на другом сервере, можно передавать его через PHP как посредника, не сохраняя локально.
<?php
$url = 'https://example.com/remote_file.mp4';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0); // сразу выводим
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
// Можно передать заголовки от PHP (Content-Type, Content-Disposition) вручную
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename="remote.mp4"');
curl_exec($ch);
curl_close($ch);
?>
Но в этом случае PHP не знает размер удалённого файла, поэтому Content-Length не указывается (браузер будет показывать прогресс не точно). Для большей точности можно сначала получить размер через curl_getinfo.
Пример 5. Передача файла с лимитом скорости
Для предотвращения захвата пропускной способности можно искусственно замедлить отправку, ограничивая размер чанка и добавляя задержки.
<?php
$file = 'bigfile.mp4';
$handle = fopen($file, 'rb');
$limit = 100 * 1024; // 100 КБ в секунду
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename="bigfile.mp4"');
header('Content-Length: ' . filesize($file));
while (!feof($handle)) {
echo fread($handle, $limit);
ob_flush();
flush();
usleep(1000000); // 1 секунда
}
fclose($handle);
?>
Каждую секунду отправляется 100 КБ. Это лишь демонстрация; на практике используют более точные алгоритмы с учётом времени.
Пример 6. Отдача файла с кастомным Content-Type по расширению
<?php
$file = 'report.xlsx';
$mime_types = [
'pdf' => 'application/pdf',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'zip' => 'application/zip',
];
$ext = pathinfo($file, PATHINFO_EXTENSION);
$mime = $mime_types[$ext] ?? 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));
readfile($file);
?>
Этот подход помогает браузеру правильно интерпретировать файл при открытии (например, Excel может открыть .xlsx напрямую). Без указания MIME-типа браузер скачает файл как неизвестный.