Организация загрузки файлов через PHP: от простого к сложному
Скачивание файлов через PHP: варианты реализации
Основной метод: readfile() с заголовками
Как инициировать скачивание файла в браузере с минимальным кодом?
Функция readfile() читает файл и выводит его напрямую в выходной поток. Для корректной работы необходимо предварительно отправить HTTP-заголовки, которые подскажут браузеру, как обрабатывать файл.
$file = 'document.pdf';
$filename = basename($file);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;Upload files php (загрузка файлов на сервер php)
Пояснения: заголовок Content-Type может быть установлен в application/octet-stream для любого файла либо в конкретный MIME-тип. Content-Disposition: attachment заставляет браузер показать диалог сохранения. Content-Length позволяет отображать прогресс. После readfile() скрипт завершается через exit, чтобы не добавлять лишних данных.
Типичные ошибки:
- Вывод пробелов или HTML до отправки заголовков - вызывает ошибку Headers already sent. Решение: проверять, нет ли вывода до header(), и использовать буферизацию.
- Неверный путь к файлу: readfile() возвращает false, поток пуст. Нужно проверять существование файла через file_exists().
- Большие файлы: readfile() читает весь файл в память. При файлах > 100 МБ возможен переполнение памяти. Для этого лучше использовать пофрагментную отправку.
Цели использования: быстрая и простая отправка небольших и средних файлов (до нескольких десятков мегабайт). Подходит для файлов, которые не требуют дополнительной обработки.
Пофрагментная отправка через fopen/fread
Как скачать большой файл без загрузки всего в память?
Файл открывается через fopen() и читается блоками фиксированного размера. Каждый блок сразу отправляется в вывод. Это экономит память сервера.
$file = 'bigfile.zip';
$size = filesize($file);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . $size);
$handle = fopen($file, 'rb');
while (!feof($handle)) {
echo fread($handle, 8192);
ob_flush();
flush();
}
fclose($handle);
exit;Php download file (скачивание файлов через php)
Размер блока (8 КБ) можно менять. Команды ob_flush() и flush() обеспечивают немедленную отправку данных клиенту.
Проблемы: при очень больших файлах (гигабайты) скрипт может выполняться дольше ограничения времени PHP. Решение - увеличить max_execution_time или использовать функцию set_time_limit(0). Также возможны сбои соединения, для них нужна обработка ошибок.
Поддержка HTTP Range (докачка и частичная загрузка)
Как реализовать возможность докачать файл с прерванной загрузки или начать воспроизведение видео с произвольного места?
Многие менеджеры загрузок и видеоплееры отправляют запрос с заголовком Range. Скрипт должен обработать этот заголовок, установить статус 206 Partial Content и выдать только запрошенный диапазон.
$file = 'movie.mp4';
$size = filesize($file);
$etag = md5_file($file);
header('Accept-Ranges: bytes');
header('ETag: "' . $etag . '"');
if (isset($_SERVER['HTTP_RANGE'])) {
preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches);
$start = intval($matches[1]);
$end = $matches[2] === '' ? $size - 1 : intval($matches[2]);
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
header('Content-Length: ' . ($end - $start + 1));
$handle = fopen($file, 'rb');
fseek($handle, $start);
while ($pos = ftell($handle), $pos <= $end) {
$length = min(8192, $end - $pos + 1);
echo fread($handle, $length);
ob_flush(); flush();
}
fclose($handle);
} else {
header('Content-Length: ' . $size);
readfile($file);
}
exit;
Проблемы: неправильный парсинг диапазона, ошибки с индексацией при пустом конце. Необходимо также поддерживать множественные диапазоны (multipart), что сложнее.
Скачивание удалённого файла через cURL
Как передать пользователю файл, расположенный на другом сервере, не сохраняя его локально?
Можно использовать cURL для получения файла и одновременно отправлять его браузеру. Это называют «прозрачным проксированием».
$url = 'https://example.com/remote.zip';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
// передаём заголовки дальше? можно анализировать Content-Type
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="remote.zip"');
curl_exec($ch);
curl_close($ch);
exit;
Этот метод не использует дополнительную память на сервере, но зависит от скорости внешнего канала.
Ошибки: если удалённый сервер требует авторизацию, нужно добавить опции CURLOPT_USERPWD. Также возможны таймауты - установите CURLOPT_TIMEOUT.
Ограничение скорости загрузки
Как отдавать файл с заранее заданной скоростью, чтобы не перегружать канал?
В цикле пофрагментной отправки вставляется пауза через usleep(). Размер блока и задержка определяют скорость.
$chunk = 1024; // 1 КБ
$delay = 100000; // 0.1 сек
$file = 'file.bin';
$handle = fopen($file, 'rb');
header('...');
while (!feof($handle)) {
echo fread($handle, $chunk);
ob_flush(); flush();
usleep($delay);
}
fclose($handle);
exit;
Скорость = $chunk / ($delay / 1 000 000) байт/с. При $chunk = 1024 и $delay = 100000 скорость ~10 КБ/с.
Использование X-Sendfile
Как передать файл через веб-сервер, минуя PHP, для максимальной производительности?
Модули mod_xsendfile (Apache) или X-Accel-Redirect (Nginx) позволяют PHP только указать путь к файлу, а сервер сам отдаёт его с правильными заголовками. Необходим соответствующий модуль и настройка.
header('X-Sendfile: /path/to/protected/file.zip');
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="file.zip"');
exit;
Преимущества: скорость, низкая нагрузка на PHP. Проблемы: неправильная настройка разрешений (файл должен быть доступен для чтения веб-серверу), отсутствие поддержки на некоторых хостингах.
Скачивание с авторизацией
Как отдавать файл только после проверки прав пользователя?
Перед отправкой файла выполняется проверка сессии, cookies или токена. Если доступ запрещён, возвращается 403 ошибка.
session_start();
if (!isset($_SESSION['user'])) {
http_response_code(403);
echo 'Доступ запрещён';
exit;
}
$file = 'secret.doc';
if (!file_exists($file)) {
http_response_code(404);
exit;
}
// далее стандартный readfile
header('Content-Type: ...');
header('Content-Disposition: ...');
readfile($file);
exit;
Сжатие перед отправкой
Как автоматически сжать файл (gzip) при скачивании для уменьшения трафика?
Текстовые файлы можно сжимать «на лету» с помощью ob_gzhandler или gzencode. Важно выставить заголовок Content-Encoding: gzip.
$file = 'large_log.txt';
$data = file_get_contents($file);
$compressed = gzencode($data, 9);
header('Content-Type: text/plain');
header('Content-Disposition: attachment; filename="large_log.txt.gz"');
header('Content-Encoding: gzip');
header('Content-Length: ' . strlen($compressed));
echo $compressed;
exit;
Не подходит для уже сжатых файлов (zip, jpg).
Создание временного архива и его скачивание
Как собрать несколько файлов в ZIP-архив и сразу отдать пользователю без сохранения на диске?
Используется класс ZipArchive с потоком вывода (php://output).
$zip = new ZipArchive();
$zipName = 'archive.zip';
if ($zip->open($zipName, ZipArchive::CREATE) !== true) {
exit('Не удалось создать архив');
}
$zip->addFile('file1.txt');
$zip->addFile('file2.txt');
$zip->close();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $zipName . '"');
header('Content-Length: ' . filesize($zipName));
readfile($zipName);
unlink($zipName);
exit;
// Альтернатива: создать временный файл и удалить после отправки
Выбор конкретного решения зависит от размера файлов, требований к безопасности, производительности и возможностей сервера.
Подробные примеры с пояснениями
Полноценный скрипт скачивания с обработкой ошибок и поддержкой Range
Как написать универсальный скрипт, который обрабатывает частичные запросы, большие файлы и информирует о ошибках?
$file = '/var/www/downloads/video.mp4';
if (!file_exists($file)) {
http_response_code(404);
exit('Файл не найден');
}
$size = filesize($file);
$etag = md5_file($file);
$mime = mime_content_type($file) ?: 'application/octet-stream';
header('Accept-Ranges: bytes');
header('ETag: "' . $etag . '"');
$start = 0;
$end = $size - 1;
$status = 200;
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) {
$start = intval($matches[1]);
$end = ($matches[2] === '') ? $size - 1 : intval($matches[2]);
if ($start > $end || $start >= $size) {
http_response_code(416);
header('Content-Range: bytes */' . $size);
exit;
}
$status = 206;
}
}
if ($status === 206) {
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
} else {
header('HTTP/1.1 200 OK');
}
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . ($end - $start + 1));
$handle = fopen($file, 'rb');
fseek($handle, $start);
$chunkSize = 8192;
while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
if (connection_aborted()) break;
$bytesToRead = min($chunkSize, $end - $pos + 1);
echo fread($handle, $bytesToRead);
ob_flush(); flush();
}
fclose($handle);
exit;
(При запросе файла без Range сервер вернёт 200 OK и полный файл; при запросе с Range bytes=100-199 вернётся 206 и 100 байт.)
Скачивание директории как архива без сохранения на диск
Как создать ZIP-архив из списка файлов в памяти и отдать его пользователю?
$files = ['/tmp/doc1.pdf', '/tmp/img.jpg'];
$zip = new ZipArchive();
$tmp = tempnam(sys_get_temp_dir(), 'zip');
if ($zip->open($tmp, ZipArchive::OVERWRITE) !== true) {
exit('Не могу создать архив');
}
foreach ($files as $f) {
if (file_exists($f)) {
$zip->addFile($f, basename($f));
}
}
$zip->close();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="download.zip"');
header('Content-Length: ' . filesize($tmp));
readfile($tmp);
unlink($tmp);
exit;
(Пользователь получает файл download.zip, временный файл удаляется после отправки.)
Проксирующее скачивание с cURL и ретрансляцией заголовков
Как передать пользователю файл с другого сервера, сохраняя исходные заголовки (Content-Type, Content-Disposition) и обеспечивая докачку?
$remoteUrl = 'https://mirror.example.org/large.iso';
$ch = curl_init($remoteUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $headerLine) {
$trimmed = trim($headerLine);
// Пропускаем заголовки статуса и заголовки, которые могут быть добавлены PHP
if (stripos($trimmed, 'HTTP/') === 0) return strlen($headerLine);
if (preg_match('/^Content-(Type|Disposition|Length):/i', $trimmed)) {
header($trimmed);
}
return strlen($headerLine);
});
// Передаём заголовок Range, если он был у клиента
if (isset($_SERVER['HTTP_RANGE'])) {
curl_setopt($ch, CURLOPT_RANGE, $_SERVER['HTTP_RANGE']);
}
curl_exec($ch);
curl_close($ch);
exit;
(Скрипт транслирует удалённый файл, при этом клиент видит те же заголовки, что и при прямом скачивании.)