Передать файл браузеру php
I think the way an array of attachments works is kind of cumbersome. Usually the PHP guys are right on the money, but this is just counter-intuitive. It should have been more like:
Array
(
[0] => Array
(
[name] => facepalm.jpg
[type] => image/jpeg
[tmp_name] => /tmp/phpn3FmFr
[error] => 0
[size] => 15476
)
Anyways, here is a fuller example than the sparce one in the documentation above:
foreach ( $_FILES [ «attachment» ][ «error» ] as $key => $error )
$tmp_name = $_FILES [ «attachment» ][ «tmp_name» ][ $key ];
if (! $tmp_name ) continue;
$name = basename ( $_FILES [ «attachment» ][ «name» ][ $key ]);
if ( $error == UPLOAD_ERR_OK )
if ( move_uploaded_file ( $tmp_name , «/tmp/» . $name ) )
$uploaded_array [] .= «Uploaded file ‘» . $name . «‘.
\n» ;
else
$errormsg .= «Could not move uploaded file ‘» . $tmp_name . «‘ to ‘» . $name . «‘
\n» ;
>
else $errormsg .= «Upload error. [» . $error . «] on file ‘» . $name . «‘
\n» ;
>
?>
Do not use Coreywelch or Daevid’s way, because their methods can handle only within two-dimensional structure. $_FILES can consist of any hierarchy, such as 3d or 4d structure.
The following example form breaks their codes:
As the solution, you should use PSR-7 based zendframework/zend-diactoros.
use Psr \ Http \ Message \ UploadedFileInterface ;
use Zend \ Diactoros \ ServerRequestFactory ;
$request = ServerRequestFactory :: fromGlobals ();
if ( $request -> getMethod () !== ‘POST’ ) http_response_code ( 405 );
exit( ‘Use POST method.’ );
>
$uploaded_files = $request -> getUploadedFiles ();
if (
!isset( $uploaded_files [ ‘files’ ][ ‘x’ ][ ‘y’ ][ ‘z’ ]) ||
! $uploaded_files [ ‘files’ ][ ‘x’ ][ ‘y’ ][ ‘z’ ] instanceof UploadedFileInterface
) http_response_code ( 400 );
exit( ‘Invalid request body.’ );
>
$file = $uploaded_files [ ‘files’ ][ ‘x’ ][ ‘y’ ][ ‘z’ ];
if ( $file -> getError () !== UPLOAD_ERR_OK ) http_response_code ( 400 );
exit( ‘File uploading failed.’ );
>
$file -> moveTo ( ‘/path/to/new/file’ );
The documentation doesn’t have any details about how the HTML array feature formats the $_FILES array.
Array
(
[document] => Array
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)
)
Multi-files with HTML array feature —
Array
(
[documents] => Array
(
[name] => Array
(
[0] => sample-file.doc
[1] => sample-file.doc
)
(
[0] => application/msword
[1] => application/msword
) [tmp_name] => Array
(
[0] => /tmp/path/phpVGCDAJ
[1] => /tmp/path/phpVGCDAJ
)
The problem occurs when you have a form that uses both single file and HTML array feature. The array isn’t normalized and tends to make coding for it really sloppy. I have included a nice method to normalize the $_FILES array.
function normalize_files_array ( $files = [])
foreach( $files as $index => $file )
if (! is_array ( $file [ ‘name’ ])) $normalized_array [ $index ][] = $file ;
continue;
>
foreach( $file [ ‘name’ ] as $idx => $name ) $normalized_array [ $index ][ $idx ] = [
‘name’ => $name ,
‘type’ => $file [ ‘type’ ][ $idx ],
‘tmp_name’ => $file [ ‘tmp_name’ ][ $idx ],
‘error’ => $file [ ‘error’ ][ $idx ],
‘size’ => $file [ ‘size’ ][ $idx ]
];
>
?>
The following is the output from the above method.
Array
(
[document] => Array
(
[0] => Array
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)
(
[0] => Array
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
) [1] => Array
(
[name] => sample-file.doc
[type] => application/msword
[tmp_name] => /tmp/path/phpVGCDAJ
[error] => 0
[size] => 0
)
Отдаем файлы эффективно с помощью PHP
Метод хорош тем, что работает с коробки. Надо только написать свою функцию отправки файла (немного измененный пример из официальной документации):
function file_force_download($file) < if (file_exists($file)) < // сбрасываем буфер вывода PHP, чтобы избежать переполнения памяти выделенной под скрипт // если этого не сделать файл будет читаться в память полностью! 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); exit; > >
Таким способом можно отправлять даже большие файлы, так как PHP будет читать файл и сразу отдавать его пользователю по частям. В документации четко сказано, что readfile() не должен создавать проблемы с памятью.
- Скрипт ждет пока весь файл будет прочитан и отдан пользователю.
- Файл читается в внутренний буфер функции readfile(), размер которого составляет 8кБ (спасибо 2fast4rabbit)
2. Читаем и отправляем файл вручную
Метод использует тот же Drupal при отправке файлов из приватной файловой системы (файлы недоступны напрямую по ссылкам):
function file_force_download($file) < if (file_exists($file)) < // сбрасываем буфер вывода PHP, чтобы избежать переполнения памяти выделенной под скрипт // если этого не сделать файл будет читаться в память полностью! 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); > exit; > >
- Скрипт ждет пока весь файл будет прочитан и отдан пользователю.
- Позволяет сэкономить память сервера
3. Используем модуль веб сервера
3a. Apache
Модуль XSendFile позволяет с помощью специального заголовка передать отправку файла самому Apache. Существуют версии по Unix и Windows, под версии 2.0.*, 2.2.* и 2.4.*
В настройках хоста нужно включить перехват заголовка с помощью директивы:
Также можно указать белый список директорий, файлы в которых могут быть обработаны. Важно: если у Вас сервер на базе Windows путь должен включать букву диска в верхнем регистре.
Описание возможных опций на сайте разработчика: https://tn123.org/mod_xsendfile/
function file_force_download($file) < if (file_exists($file)) < header('X-SendFile: ' . realpath($file)); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); exit; >>
3b. Nginx
Nginx умеет отправлять файлы из коробки через специальный заголовок.
Для корректной работы нужно запретить доступ к папку напрямую через конфигурационный файл:
Пример отправки файла (файл должен находиться в директории /some/path/protected):
function file_force_download($file) < if (file_exists($file)) < header('X-Accel-Redirect: ' . $file); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . basename($file)); exit; >>
Больше информации на странице официальной документации
- Скрипт завершается сразу после выполнения всех инструкций
- Физически файл отправляется модулем самого веб сервера, а не PHP
- Минимальное потребление памяти и ресурсов сервера
- Максимальное быстродействие
Update: Хабраюзер ilyaplot дает дельный совет, что лучше слать не application/octet-stream , а реальный mime type файла. Например, это позволит браузеру подставить нужные программы в диалог сохранение файла.
Как отдать пользователю файл скриптом
Зачастую бывает необходимость в том, чтобы сайт умел отдавать файлы не просто на скачивание, а поддерживать возможность скачивания в несколько потоков и докачки файла в случае обрыва соединения.
Для начала попробуем просто отдать файл браузеру:
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