etiquetas - inicio y fin en php
La forma más rápida de servir un archivo usando PHP (8)
Intento armar una función que recibe una ruta de archivo, identifica qué es, establece los encabezados apropiados y la sirve como lo haría Apache.
La razón por la que estoy haciendo esto es porque necesito usar PHP para procesar cierta información sobre la solicitud antes de servir el archivo.
La velocidad es crítica
virtual () no es una opción
Debe funcionar en un entorno de alojamiento compartido donde el usuario no tiene control del servidor web (Apache / nginx, etc.)
Esto es lo que tengo hasta ahora:
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], ''.'');
}
}
}
?>
Aquí va una solución pura de PHP. He adaptado la siguiente función desde mi marco personal :
function Download($path, $speed = null, $multipart = true)
{
while (ob_get_level() > 0)
{
ob_end_clean();
}
if (is_file($path = realpath($path)) === true)
{
$file = @fopen($path, ''rb'');
$size = sprintf(''%u'', filesize($path));
$speed = (empty($speed) === true) ? 1024 : floatval($speed);
if (is_resource($file) === true)
{
set_time_limit(0);
if (strlen(session_id()) > 0)
{
session_write_close();
}
if ($multipart === true)
{
$range = array(0, $size - 1);
if (array_key_exists(''HTTP_RANGE'', $_SERVER) === true)
{
$range = array_map(''intval'', explode(''-'', preg_replace(''~.*=([^,]*).*~'', ''$1'', $_SERVER[''HTTP_RANGE''])));
if (empty($range[1]) === true)
{
$range[1] = $size - 1;
}
foreach ($range as $key => $value)
{
$range[$key] = max(0, min($value, $size - 1));
}
if (($range[0] > 0) || ($range[1] < ($size - 1)))
{
header(sprintf(''%s %03u %s'', ''HTTP/1.1'', 206, ''Partial Content''), true, 206);
}
}
header(''Accept-Ranges: bytes'');
header(''Content-Range: bytes '' . sprintf(''%u-%u/%u'', $range[0], $range[1], $size));
}
else
{
$range = array(0, $size - 1);
}
header(''Pragma: public'');
header(''Cache-Control: public, no-cache'');
header(''Content-Type: application/octet-stream'');
header(''Content-Length: '' . sprintf(''%u'', $range[1] - $range[0] + 1));
header(''Content-Disposition: attachment; filename="'' . basename($path) . ''"'');
header(''Content-Transfer-Encoding: binary'');
if ($range[0] > 0)
{
fseek($file, $range[0]);
}
while ((feof($file) !== true) && (connection_status() === CONNECTION_NORMAL))
{
echo fread($file, round($speed * 1024)); flush(); sleep(1);
}
fclose($file);
}
exit();
}
else
{
header(sprintf(''%s %03u %s'', ''HTTP/1.1'', 404, ''Not Found''), true, 404);
}
return false;
}
El código es tan eficiente como puede ser, cierra el controlador de sesión para que otros scripts PHP puedan ejecutarse simultáneamente para el mismo usuario / sesión. También admite el servicio de descargas en rangos (que también es lo que Apache hace por defecto, sospecho), para que las personas puedan pausar / reanudar las descargas y también se beneficien de mayores velocidades de descarga con aceleradores de descarga. También le permite especificar la velocidad máxima (en Kbps) a la que la descarga (parte) debe ser servida a través del argumento de $speed
.
La función de Download
PHP mencionada aquí causaba cierta demora antes de que el archivo realmente comenzara a descargarse. No sé si esto fue causado por el uso de caché de barniz o qué, pero para mí me ayudó a eliminar el sleep(1);
completamente y establecer $speed
a 1024
. Ahora funciona sin ningún problema, tan rápido como el infierno. Tal vez podrías modificar esa función también, porque vi que se usaba en todo Internet.
La manera más rápida: no. Mire en el encabezado x-sendfile para nginx , también hay cosas similares para otros servidores web. Esto significa que todavía puede hacer control de acceso, etc. en php, pero delegar el envío real del archivo a un servidor web diseñado para eso.
PD: Me da escalofríos solo de pensar en cuánto más eficiente es usar esto con nginx, en comparación con leer y enviar el archivo en php. Solo piense si 100 personas están descargando un archivo: con php + apache, siendo generoso, eso probablemente sea 100 * 15mb = 1.5GB (aproximadamente, dispárame), de ram justo allí. Nginx simplemente entregará el envío del archivo al kernel, y luego se cargará directamente desde el disco en los búferes de la red. ¡Rápido!
PPS: Y, con este método, aún puede hacer todo el control de acceso, las cosas de la base de datos que desee.
Si desea ocultar dónde se encuentra el archivo y las personas con privilegios específicos pueden descargar el archivo, entonces es una buena idea usar PHP como retransmisor, y debe sacrificar tiempo de CPU para obtener más seguridad y control.
Si tiene la posibilidad de agregar extensiones PECL a su php, puede simplemente usar las funciones del paquete Fileinfo para determinar el tipo de contenido y luego enviar los encabezados adecuados ...
Una mejor implementación, con soporte de caché, encabezados http personalizados.
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;
}
}
Mi respuesta anterior fue parcial y no está bien documentada, aquí hay una actualización con un resumen de las soluciones de la misma y de otros en la discusión.
Las soluciones se ordenan desde la mejor solución hasta la peor, pero también desde la solución que necesita más control sobre el servidor web hasta la que necesita menos. No parece haber una manera fácil de tener una solución que sea rápida y funcione en todas partes.
Usando el encabezado X-SendFile
Según lo documentado por otros, de hecho es la mejor manera. La base es que haces tu control de acceso en php y luego, en lugar de enviar el archivo tú mismo, le dices al servidor web que lo haga.
El código php básico es:
header("X-Sendfile: $file_name");
header("Content-type: application/octet-stream");
header(''Content-Disposition: attachment; filename="'' . basename($file_name) . ''"'');
Donde $file_name
es la ruta completa en el sistema de archivos.
El principal problema con esta solución es que debe ser permitido por el servidor web y no está instalado por defecto (apache), no está activo por defecto (lighttpd) o necesita una configuración específica (nginx).
apache
En Apache, si usa mod_php, necesita instalar un módulo llamado mod_xsendfile luego configurarlo (ya sea en apache config o .htaccess si lo permite)
XSendFile on
XSendFilePath /home/www/example.com/htdocs/files/
Con este módulo, la ruta del archivo podría ser absoluta o relativa al XSendFilePath
especificado.
Lighttpd
El mod_fastcgi admite esto cuando se configura con
"allow-x-send-file" => "enable"
La documentación para la función está en la wiki lighttpd que documentan el X-LIGHTTPD-send-file
pero el nombre X-Sendfile
también funciona
Nginx
En Nginx no puede usar el encabezado X-Sendfile
, debe usar su propio encabezado que se denomina X-Accel-Redirect
. Está habilitado por defecto y la única diferencia real es que su argumento debe ser un URI, no un sistema de archivos. La consecuencia es que debe definir una ubicación marcada como interna en su configuración para evitar que los clientes encuentren la url de archivo real e ir directamente a ella, su wiki contiene una buena explicación de esto.
Symlinks y encabezado de ubicación
Puede usar symlinks y redireccionar a ellos, simplemente cree enlaces simbólicos a su archivo con nombres aleatorios cuando un usuario esté autorizado para acceder a un archivo y redirigir al usuario a él usando:
header("Location: " . $url_of_symlink);
Obviamente, necesitará una forma de eliminarlos cuando se llame al script para crearlos o mediante cron (en el equipo si tiene acceso o a través de algún servicio webcron);
En apache, debe poder habilitar FollowSymLinks
en .htaccess
o en la configuración de apache.
Control de acceso por IP y cabecera de ubicación
Otro truco consiste en generar archivos de acceso de apache desde php que permitan la IP explícita del usuario. En Apache, significa usar mod_authz_host
( mod_access
) Allow from
comandos.
El problema es que bloquear el acceso al archivo (ya que varios usuarios pueden querer hacer esto al mismo tiempo) no es trivial y podría llevar a que algunos usuarios esperen mucho tiempo. Y aún necesita podar el archivo de todos modos.
Obviamente, otro problema sería que varias personas detrás de la misma IP podrían acceder al archivo.
Cuando todo lo demás falla
Si realmente no tiene forma de que su servidor web lo ayude, la única solución que queda es readfile , está disponible en todas las versiones de php actualmente en uso y funciona bastante bien (pero no es realmente eficiente).
Combinando soluciones
En definitiva, la mejor manera de enviar un archivo realmente rápido si desea que su código php sea utilizable en cualquier lugar es tener una opción configurable en alguna parte, con instrucciones sobre cómo activarla dependiendo del servidor web y tal vez una detección automática en su instalación guión.
Es bastante similar a lo que se hace en un montón de software para
- Limpiar urls (
mod_rewrite
en apache) - Funciones Crypto (módulo
mcrypt
php) - Soporte de cadenas multibyte (módulo
mbstring
php)
header(''Location: '' . $path);
exit(0);
Deja que Apache haga el trabajo por ti.