Descarga
Aunque no me atrevo a darla por terminada, esta colección de clases me está siendo de gran ayuda cada vez que
tengo que convencer al navegador de que se descargue algo. Todo el que lo haya intentado sabrá que a priori es
facilísimo y al final siempre casca algo en Internet Explorer. La idea es ir resolviendo los distintos bugs de
los navegadores y esconder todo en un recorte listo para usar.
El invento consta de una clase abstracta con la funcionalidad común y sendas clases derivadas que son las que se
instancian según el origen de los datos:
DescargaVariable obtiene el contenido desde una variable.
DescargaArchivo lee de un archivo en disco.
DescargaPuntero utiliza un puntero a archivo generado con anterioridad.
abstract class DescargaBase{
private $nombre, $tipo_mime;
private $hay_compresion_transparente = FALSE;
public function __construct($nombre, $tipo_mime='application/octet-stream'){
$this->nombre = $this->nombreValido($nombre);
$this->tipo_mime = $tipo_mime;
}
public function descargar($inline=FALSE){
header('Pragma: private');
header('Cache-Control: public, must-revalidate');
header('Content-Type: ' . $this->tipo_mime);
if( $inline ){
header('Content-Disposition: inline; filename="' . $this->nombre . '"');
}else{
header('Content-Disposition: attachment; filename="' . $this->nombre . '"');
}
header('Content-Transfer-Encoding: binary');
if( !$this->hay_compresion_transparente ){
header('Content-Length: ' . $this->obtenerTamano());
}
$this->volcar();
}
abstract protected function obtenerTamano();
abstract protected function volcar();
private function nombreValido($nombre){
return strtr($nombre, '\\/:*?"<>|', '---------');
}
}
class DescargaVariable extends DescargaBase{
private $contenido;
public function __construct(&$variable, $nombre, $tipo_mime='application/octet-stream'){
if( is_scalar($variable) ){
$this->contenido = &$variable;
parent::__construct($nombre, $tipo_mime);
}else{
throw new Exception('La variable con el contenido del archivo no es un escalar: ' . gettype($variable));
}
}
protected function obtenerTamano(){
return strlen($this->contenido);
}
protected function volcar(){
echo $this->contenido;
}
}
class DescargaArchivo extends DescargaBase{
private $archivo;
public function __construct($archivo, $nombre, $tipo_mime='application/octet-stream'){
if( is_readable($archivo) ){
$this->archivo = $archivo;
parent::__construct($nombre, $tipo_mime);
}else{
throw new Exception('El archivo no existe o no se puede leer: ' . $archivo);
}
}
protected function obtenerTamano(){
return filesize($this->archivo);
}
protected function volcar(){
$fp = @fopen($this->archivo, 'rb');
if($fp){
while( !feof($fp) ){
echo fread($fp, 8192);
}
fclose($fp);
}else{
throw new Exception('Se ha producido un error al abrir el archivo: ' . $this->archivo);
}
}
}
class DescargaPuntero extends DescargaBase{
private $fp;
public function __construct($fp, $nombre, $tipo_mime='application/octet-stream'){
if( is_resource($fp) && (get_resource_type($fp)=='file' || get_resource_type($fp)=='stream') ){
$this->fp = $fp;
parent::__construct($nombre, $tipo_mime);
}else{
throw new Exception('El parámetro no es un puntero a un archivo: ' . gettype($fp));
}
}
protected function obtenerTamano(){
$stat = fstat($this->fp);
return $stat['size'];
}
protected function volcar(){
rewind($this->fp);
while( !feof($this->fp) ){
echo fread($this->fp, 8192);
}
fclose($this->fp);
}
}
Ejemplos de uso
$datos = '';
for($i=0; $i<10; $i++){
$datos .= mt_rand(0, 1000) . PHP_EOL;
}
$descarga = new DescargaVariable($datos, 'Números aleatorios.csv', 'text/csv');
$descarga->descargar();
exit;
$descarga = new DescargaArchivo(__FILE__, basename(__FILE__), 'application/x-httpd-php');
$descarga->descargar();
exit;
$fp = tmpfile();
for($i=0; $i<10; $i++){
fwrite($fp, mt_rand(0, 1000) . PHP_EOL);
}
$descarga = new DescargaPuntero($fp, 'Números aleatorios.csv', 'text/csv');
$descarga->descargar();
exit;
Nota final
Independientemente de todo esto, os aseguro que nunca está de más usar un poco de mod_rewrite para
enmascarar la ruta del script y hacer creer al navegador que se está bajando algo completamente estático:
/descargar.php?id_archivo=314 → /descargar/314/folleto.pdf
Historial
- v2011-06-21
- El método
descargar() ya no finaliza la ejecución del script.
- v2011-03-16
- Primera versión.