Самый быстрый способ подачи файла с помощью PHP
Я пытаюсь собрать функцию, которая получает путь к файлу, определяет, что это такое, устанавливает соответствующие заголовки и служит ему так же, как Apache.
Причина, по которой я делаю это, - это то, что мне нужно использовать PHP для обработки некоторой информации о запросе перед тем, как обслуживать файл.
Скорость критическая
virtual() не вариант
Должен работать в среде совместного размещения, где пользователь не имеет контроля над веб-сервером (Apache/nginx и т.д.)
Вот что у меня до сих пор:
File::output($path);
<?php
class File {
static function output($path) {
// Check if the file exists
if(!File::exists($path)) {
header('HTTP/1.0 404 Not Found');
exit();
}
// Set the content-type header
header('Content-Type: '.File::mimeType($path));
// Handle caching
$fileModificationTime = gmdate('D, d M Y H:i:s', File::modificationTime($path)).' GMT';
$headers = getallheaders();
if(isset($headers['If-Modified-Since']) && $headers['If-Modified-Since'] == $fileModificationTime) {
header('HTTP/1.1 304 Not Modified');
exit();
}
header('Last-Modified: '.$fileModificationTime);
// Read the file
readfile($path);
exit();
}
static function mimeType($path) {
preg_match("|\.([a-z0-9]{2,4})$|i", $path, $fileSuffix);
switch(strtolower($fileSuffix[1])) {
case 'js' :
return 'application/x-javascript';
case 'json' :
return 'application/json';
case 'jpg' :
case 'jpeg' :
case 'jpe' :
return 'image/jpg';
case 'png' :
case 'gif' :
case 'bmp' :
case 'tiff' :
return 'image/'.strtolower($fileSuffix[1]);
case 'css' :
return 'text/css';
case 'xml' :
return 'application/xml';
case 'doc' :
case 'docx' :
return 'application/msword';
case 'xls' :
case 'xlt' :
case 'xlm' :
case 'xld' :
case 'xla' :
case 'xlc' :
case 'xlw' :
case 'xll' :
return 'application/vnd.ms-excel';
case 'ppt' :
case 'pps' :
return 'application/vnd.ms-powerpoint';
case 'rtf' :
return 'application/rtf';
case 'pdf' :
return 'application/pdf';
case 'html' :
case 'htm' :
case 'php' :
return 'text/html';
case 'txt' :
return 'text/plain';
case 'mpeg' :
case 'mpg' :
case 'mpe' :
return 'video/mpeg';
case 'mp3' :
return 'audio/mpeg3';
case 'wav' :
return 'audio/wav';
case 'aiff' :
case 'aif' :
return 'audio/aiff';
case 'avi' :
return 'video/msvideo';
case 'wmv' :
return 'video/x-ms-wmv';
case 'mov' :
return 'video/quicktime';
case 'zip' :
return 'application/zip';
case 'tar' :
return 'application/x-tar';
case 'swf' :
return 'application/x-shockwave-flash';
default :
if(function_exists('mime_content_type')) {
$fileSuffix = mime_content_type($path);
}
return 'unknown/' . trim($fileSuffix[0], '.');
}
}
}
?>
Ответы
Ответ 1
Мой предыдущий ответ был частичным и не совсем документированным, вот обновленная версия с резюме решений от него и других в обсуждении.
Решения заказываются от лучшего решения до наихудшего, но также от решения, требующего наибольшего контроля над веб-сервером для тех, кому требуется меньше. Кажется, что нет простого способа иметь одно решение, быстрое и повсеместное.
Использование заголовка X-SendFile
Как документировано другими, это действительно лучший способ. Основа заключается в том, что вы выполняете свой контроль доступа в php, а затем вместо отправки файла сами скажете веб-серверу сделать это.
Основной PHP-код:
header("X-Sendfile: $file_name");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file_name) . '"');
Где $file_name
- полный путь к файловой системе.
Основная проблема с этим решением заключается в том, что он должен быть разрешен веб-сервером и либо не установлен по умолчанию (apache), либо неактивен по умолчанию (lighttpd), либо нуждается в конкретной конфигурации (nginx).
Apache
В apache, если вы используете mod_php, вам необходимо установить модуль под названием mod_xsendfile, затем настройте его (либо в конфигурации apache, либо в .htaccess if вы разрешаете это)
XSendFile on
XSendFilePath /home/www/example.com/htdocs/files/
С помощью этого модуля путь к файлу может быть либо абсолютным, либо относительным к указанному XSendFilePath
.
Lighttpd
Поддержка mod_fastcgi при настройке с помощью
"allow-x-send-file" => "enable"
Документация по этой функции находится в lighttpd wiki, они документируют заголовок X-LIGHTTPD-send-file
, но имя X-Sendfile
также работает
Nginx
В Nginx вы не можете использовать заголовок X-Sendfile
, вы должны использовать свой собственный заголовок с именем X-Accel-Redirect
. Он включен по умолчанию, и единственное реальное различие в том, что его аргумент должен быть URI, а не файловой системой. Следствием этого является то, что вы должны определить местоположение, помеченное как внутреннее в вашей конфигурации, чтобы клиенты не находили действительный URL-адрес файла и не переходили непосредственно к нему, их wiki содержит хорошее объяснение этого.
Обозначение символов и заголовка
Вы можете использовать символические ссылки и перенаправить их, просто создайте символические ссылки на свой файл со случайными именами, когда пользователь имеет право доступа к файлу и перенаправить пользователя к нему, используя:
header("Location: " . $url_of_symlink);
Очевидно, вам понадобится способ обрезать их либо при создании script для их создания, либо через cron (на компьютере, если у вас есть доступ или через какую-либо службу webcron)
Под apache вам нужно включить FollowSymLinks
в .htaccess
или в конфигурации apache.
Контроль доступа по IP и заголовку местоположения
Еще один взлом - это создание файлов доступа apache с php, позволяющих явный IP-адрес пользователя. Под apache это означает использование mod_authz_host
(mod_access
) Allow from
команд.
Проблема заключается в том, что блокировка доступа к файлу (поскольку несколько пользователей могут захотеть сделать это в одно и то же время) является нетривиальной и может привести к тому, что некоторые пользователи ожидают долгое время. И все равно вам все равно нужно обрезать файл.
Очевидно, что еще одна проблема заключается в том, что несколько человек за одним и тем же IP могут потенциально получить доступ к файлу.
Когда все остальное не работает
Если у вас действительно нет способа заставить ваш веб-сервер вам помочь, единственным оставшимся решением является readfile, которое доступно во всех версиях php, которые в настоящее время используются и работают очень хорошо (но не очень эффективны).
Объединение решений
В лучшем случае лучший способ отправить файл очень быстро, если вы хотите, чтобы ваш php-код был полезен повсюду, должен иметь где-то настраиваемый параметр, с инструкциями по его активации в зависимости от веб-сервера и, возможно, с автоматическим обнаружением в вашей установке script.
Это очень похоже на то, что сделано во многих программах для
- Очистить URL-адреса (
mod_rewrite
на apache)
- Функции криптографии (
mcrypt
php module)
- Поддержка многобайтовой строки (
mbstring
php module)
Ответ 2
Самый быстрый способ: не надо. Посмотрите на заголовок x-sendfile для nginx, есть и другие вещи для других веб-серверов. Это означает, что вы все еще можете управлять доступом и т.д. В php, но делегировать фактическую отправку файла на веб-сервер, предназначенный для этого.
P.S: Я получаю озноб, просто думая о том, насколько эффективнее использовать это с nginx, по сравнению с чтением и отправкой файла в php. Подумайте, если 100 человек загружают файл: с php + apache, будучи щедрым, возможно, 100 * 15 мб = 1,5 ГБ (примерно, стрелять мне), там, где есть. Nginx просто передаст файл в ядро, а затем загрузится непосредственно с диска в сетевые буферы. Speedy!
P.P.S: И с помощью этого метода вы все равно можете выполнить весь контроль доступа, нужный материал базы данных.
Ответ 3
Здесь идет чистое PHP-решение. Я адаптировал следующую функцию
Код настолько эффективен, насколько это возможно, он закрывает обработчик сеанса, так что другие скрипты PHP могут запускаться одновременно для одного и того же пользователя/сеанса. Он также поддерживает загрузку загрузок в диапазонах (что также делает Apache по умолчанию, как я подозреваю), чтобы люди могли приостановить/возобновить загрузку, а также получить более высокую скорость загрузки с помощью ускорителей загрузки. Он также позволяет указать максимальную скорость (в Кбит/с), при которой загрузка (часть) должна быть подана с помощью аргумента $speed
.
Ответ 4
header('Location: ' . $path);
exit(0);
Пусть Apache выполнит вашу работу.
Ответ 5
если у вас есть возможность добавить расширения PECL на ваш php, вы можете просто использовать функции из Fileinfo package для определения типа содержимого а затем отправить соответствующие заголовки...
Ответ 6
Функция PHP Download
, упомянутая здесь, вызывала некоторую задержку до того, как файл начал загружаться. Я не знаю, вызвано ли это использованием кеша лака или что, но для меня это помогло полностью удалить sleep(1);
и установить $speed
на 1024
. Теперь он работает без каких-либо проблем, как быстро, как черт. Возможно, вы тоже можете изменить эту функцию, потому что я видел, как она использовалась по всему Интернету.
Ответ 7
Лучшая реализация с поддержкой кеша, настраиваемые заголовки http.
serveStaticFile($fn, array(
'headers'=>array(
'Content-Type' => 'image/x-icon',
'Cache-Control' => 'public, max-age=604800',
'Expires' => gmdate("D, d M Y H:i:s", time() + 30 * 86400) . " GMT",
)
));
function serveStaticFile($path, $options = array()) {
$path = realpath($path);
if (is_file($path)) {
if(session_id())
session_write_close();
header_remove();
set_time_limit(0);
$size = filesize($path);
$lastModifiedTime = filemtime($path);
$fp = @fopen($path, 'rb');
$range = array(0, $size - 1);
header('Last-Modified: ' . gmdate("D, d M Y H:i:s", $lastModifiedTime)." GMT");
if (( ! empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModifiedTime ) ) {
header("HTTP/1.1 304 Not Modified", true, 304);
return true;
}
if (isset($_SERVER['HTTP_RANGE'])) {
//$valid = preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE']);
if(substr($_SERVER['HTTP_RANGE'], 0, 6) != 'bytes=') {
header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416);
header('Content-Range: bytes */' . $size); // Required in 416.
return false;
}
$ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
$range = explode('-', $ranges[0]); // to do: only support the first range now.
if ($range[0] === '') $range[0] = 0;
if ($range[1] === '') $range[1] = $size - 1;
if (($range[0] >= 0) && ($range[1] <= $size - 1) && ($range[0] <= $range[1])) {
header('HTTP/1.1 206 Partial Content', true, 206);
header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size));
}
else {
header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416);
header('Content-Range: bytes */' . $size);
return false;
}
}
$contentLength = $range[1] - $range[0] + 1;
//header('Content-Disposition: attachment; filename="xxxxx"');
$headers = array(
'Accept-Ranges' => 'bytes',
'Content-Length' => $contentLength,
'Content-Type' => 'application/octet-stream',
);
if(!empty($options['headers'])) {
$headers = array_merge($headers, $options['headers']);
}
foreach($headers as $k=>$v) {
header("$k: $v", true);
}
if ($range[0] > 0) {
fseek($fp, $range[0]);
}
$sentSize = 0;
while (!feof($fp) && (connection_status() === CONNECTION_NORMAL)) {
$readingSize = $contentLength - $sentSize;
$readingSize = min($readingSize, 512 * 1024);
if($readingSize <= 0) break;
$data = fread($fp, $readingSize);
if(!$data) break;
$sentSize += strlen($data);
echo $data;
flush();
}
fclose($fp);
return true;
}
else {
header('HTTP/1.1 404 Not Found', true, 404);
return false;
}
}
Ответ 8
Если вы хотите скрыть место нахождения файла, а люди с определенными привилегиями могут загрузить файл, тогда рекомендуется использовать PHP как ретрансляцию, и вы должны пожертвовать некоторым временем процессора, чтобы получить больше безопасности и контроля.