Cómo descargar archivos en Laravel: Guía de archivos públicos vs. privados

- Andrés Cruz - EN In english

Video thumbnail

¿Cómo descargar un archivo en Laravel? Es una de esas tareas que parecen sencillas una vez que ya sabemos cómo realizar la carga de archivos en Laravel… y lo son, siempre que entiendas desde dónde quieres servir ese archivo.

En mi caso, cuando empecé a trabajar con proyectos que distribuían libros digitales, descubrí rápidamente que no era lo mismo ofrecer un archivo público que uno protegido. Las implicaciones de seguridad son completamente distintas.

En esta guía te explico ambas opciones —y algunas más avanzadas— con ejemplos reales y consejos prácticos que uso en producción.

En todos los casos empleamos el método download(), que genera una respuesta HTTP que fuerza la descarga en el navegador del usuario sin exponer la ruta física real del archivo en el servidor.

Guía Rápida: errores frecuentes y soluciones

ErrorCausa probableSolución
File not foundPath mal formado o disco incorrecto.Verifica storage_path() o el nombre del disco en config/filesystems.php.
Descarga vacía/corruptaHay un echo o dd() antes del return.Elimina toda salida de texto previa a la respuesta.
Permission deniedFalta de permisos de lectura en la carpeta.Ajusta permisos con chmod o chown en el servidor.

1. Introducción: las distintas formas de descargar archivos en Laravel

Laravel nos ofrece varias estrategias para servir archivos al usuario, cada una adecuada según el contexto:

  • Desde la carpeta public: para archivos accesibles directamente por el navegador sin ninguna restricción.
  • Desde un disco de Storage: cuando necesitas control de acceso, privacidad o lógica de negocio antes de servir el archivo.
  • Por streaming o desde servicios externos como Amazon S3 o Google Cloud Storage: para archivos grandes o alojados fuera de tu servidor.

Tip para S3: Si usas Amazon S3, evita descargar el archivo a través de tu propia aplicación, ya que consume ancho de banda innecesariamente. En su lugar, genera una URL temporal con Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(5)) y redirige al usuario directamente. Es más rápido, más seguro y más económico.

Lo fundamental en todos los casos es que el método response()->download() genera una respuesta HTTP con las cabeceras correctas (Content-Disposition: attachment) para que el navegador fuerce la descarga en lugar de mostrar el archivo en pantalla.

2. Descarga de archivos desde la carpeta public

Este es el caso más simple. Si el archivo ya está dentro de la carpeta public, puedes construir su ruta absoluta con el helper public_path() y pasársela directamente al método download():

public function download($file_name)
{
    $file_path = public_path('files/' . $file_name);
    return response()->download($file_path);
}

Laravel convierte esa ruta en una respuesta lista para descargar. Es el método que suelo usar cuando se trata de recursos abiertos, como guías gratuitas, plantillas o documentos sin restricción de acceso.

Ventajas:

  • Simple y directo, sin configuración adicional.
  • Ideal para recursos públicos que no requieren autenticación.

Limitaciones:

  • No es adecuado para archivos sensibles o de pago.
  • Cualquier persona que conozca la URL directa puede acceder al archivo sin pasar por tu código.

3. Descarga desde un disco protegido (Laravel Storage)

Cuando el archivo debe estar protegido detrás de lógica de negocio —un pago, una suscripción, permisos de usuario—, la estrategia cambia: hay que sacarlo de la carpeta public y almacenarlo en un disco configurado en config/filesystems.php.

Recuerda que todas las carpetas fuera de public son privadas por diseño en Laravel: no son accesibles desde el navegador de forma directa, lo que las hace ideales para este tipo de archivos.

El primer paso es definir un disco personalizado en config/filesystems.php:

// config/filesystems.php
'files_disk' => [
    'driver' => 'local',
    'root'   => app()->storagePath(),
],

Con el disco configurado, el proceso de descarga es igual de conciso. Puedes pasar solo la ruta del archivo, o también indicar el nombre con el que el usuario lo recibirá:

// Descarga básica
Storage::disk('files_disk')->download($routefile);

// Descarga con nombre personalizado y extensión dinámica
Storage::disk('files_disk')->download('book/' . $file->file, $name . '.' . $file->type);

En mi caso, este enfoque me resulta mucho más seguro y controlable. Todo lo que está fuera de public es inaccesible desde el navegador, lo que garantiza que solo los usuarios autorizados puedan llegar al archivo a través de tu código.

Tip personal: Mantén una estructura clara de carpetas dentro del disco (por ejemplo: books/, images/, invoices/) para evitar colisiones de nombres y mejorar la trazabilidad cuando el proyecto escala.

4. Descargas avanzadas: streaming y archivos remotos

Si trabajas con archivos grandes o necesitas descargarlos desde proveedores externos, Laravel ofrece herramientas adicionales que mejoran tanto el rendimiento como la experiencia del usuario.

Streaming con streamDownload()

El método response()->streamDownload() permite enviar el archivo al cliente en fragmentos (chunks), en lugar de cargarlo completo en memoria antes de responder. Esto es ideal para archivos pesados como PDFs de gran tamaño, vídeos o archivos comprimidos:

return response()->streamDownload(function () use ($path) {
    echo Storage::disk('files_disk')->get($path);
}, 'archivo.pdf');

Este enfoque evita los errores de memory_limit en PHP cuando el archivo supera los límites configurados en el servidor, ya que el contenido se escribe directamente al buffer de salida sin acumularse en RAM.

Descargar desde Amazon S3 o Google Cloud Storage

Una vez configurado el disco correspondiente en config/filesystems.php, la API de descarga es exactamente la misma que con el disco local. Laravel abstrae completamente el proveedor subyacente:

return Storage::disk('s3')->download('docs/manual.pdf');

También puedes personalizar el nombre del archivo descargado y forzar el tipo MIME mediante cabeceras HTTP adicionales, lo que es útil cuando el nombre almacenado en S3 es un hash o un UUID:

return response()->download($path, 'mi-archivo.pdf', [
    'Content-Type' => 'application/pdf',
]);

En mi experiencia, este enfoque es excelente para gestionar catálogos de archivos dinámicos, copias de seguridad automatizadas o activos multimedia que se sirven desde CDN.

5. Cómo permitir descargas solo a usuarios autenticados

La seguridad debe ser el punto de partida, no un añadido posterior. Laravel ofrece dos niveles de control de acceso que puedes combinar según tus necesidades.

El primero es proteger la ruta con middleware, lo que impide el acceso a cualquier usuario no autenticado antes de llegar siquiera al controlador:

Route::get('/download/{file}', [FileController::class, 'download'])
    ->middleware('auth');

El segundo nivel —más granular— es validar dentro del propio controlador si el usuario tiene derecho a descargar ese archivo concreto. Esto es esencial cuando la autorización depende de lógica de negocio, como si el usuario compró el producto:

if (auth()->user()->hasPurchased($file->id)) {
    return Storage::disk('files_disk')->download($file->path);
}

abort(403, 'No autorizado');

Combinar ambas capas —middleware('auth') en la ruta y autorización en el controlador— es la práctica que recomiendo para cualquier sistema de descarga de productos digitales. Así garantizas que cada descarga tiene lógica de negocio detrás y que ningún archivo queda expuesto por error de configuración.

En mi caso, este patrón fue esencial cuando implementé descargas de productos digitales con control de licencias por usuario.

6. Errores comunes al descargar archivos en Laravel (y cómo evitarlos)

ErrorCausaSolución
Archivo no encontradoRuta mal construida o disco incorrectoVerifica storage_path() o el nombre del disk()
Descarga vacía o dañadaHay salida de texto previa al responseElimina cualquier echo o dd() antes del return
Permiso denegadoCarpeta sin permisos de lectura para el servidor webCorrige permisos con chmod o chown
Nombre de archivo incorrectoNo se especificó el segundo parámetro en download()Agrega $name y extensión manualmente como segundo argumento

Consejo: Usa siempre rutas absolutas con los helpers de Laravel (public_path(), storage_path()) en lugar de rutas relativas. Las rutas relativas pueden comportarse de forma distinta según el entorno (local, staging, producción) y son una fuente frecuente de bugs difíciles de rastrear.

7. Caso de uso real: venta y descarga de archivos protegidos en Laravel

La descarga de archivos es una funcionalidad central en cualquier tienda de productos digitales. Permitir que un usuario descargue un archivo solo si ha completado el pago es el ejemplo perfecto de por qué la lógica de acceso controlado es tan importante.

A continuación te muestro la implementación completa que he usado en proyectos reales.

Por qué NO debes usar la carpeta public para archivos de venta

Es crítico que los archivos de pago nunca estén dentro de la carpeta public. Cualquier persona que conozca —o adivine— la URL del archivo podría descargarlo sin haber pagado, sin que tu aplicación tenga ninguna posibilidad de interceptarlo.

En Laravel, la única carpeta accesible públicamente desde el navegador es public. Por tanto, para archivos con acceso controlado, la solución es almacenarlos fuera de ella, por ejemplo en la carpeta storage, y servirlos exclusivamente a través de tu controlador.

Configuración del disco de almacenamiento protegido

Define un disco personalizado en config/filesystems.php apuntando a una carpeta fuera de public:

// config/filesystems.php
'files_sell_uploads' => [
    'driver' => 'local',
    'root'   => app()->storagePath(),
],

Subida de archivos al disco protegido

Al subir el archivo, usa storeAs() para guardarlo en el disco protegido con un nombre controlado (en este caso, basado en el timestamp para evitar colisiones):

function uploadBook()
{
    $this->rules = [
        'fileBook' => 'nullable|mimes:epub,pdf|max:20024'
    ];
    $this->validate();

    if ($this->fileBook) {
        $name = time() . '.' . $this->fileBook->getClientOriginalExtension();
        $this->fileBook->storeAs('book', $name, 'files_sell_uploads');

        YourModel::create([
            'file' => $name,
            'type' => $this->fileBook->getClientOriginalExtension(),
            // otros campos según tu modelo
        ]);
    }
}

Control de acceso para la descarga

Para verificar el pago antes de permitir la descarga, la lógica es directa: si existe el registro de pago y el archivo, se sirve; en caso contrario, se devuelve un error 403:

if ($filePayment && $file) {
    return Storage::disk('files_sell_uploads')->download('book/' . $file->file, 'book.' . $file->type);
}

Es importante destacar que la única forma de acceder a estos archivos vía HTTP es mediante esta función. Al estar almacenados fuera de public, no existe una URL directa que los exponga.

Función completa de descarga segura

public function downloadFile(File $file)
{
    $user        = auth()->user() ?? auth('sanctum')->user();
    $filePayment = FilePayment::where(/* condición */)->first();
    $file        = File::where(/* condición */)->first();

    if ($filePayment && $file) {
        return Storage::disk('files_sell_uploads')->download('book/' . $file->file, 'book.' . $file->type);
    }

    return response()->json(['message' => 'Producto no adquirido o no existe'], 403);
}

Con esta implementación, los archivos solo se descargan si se cumplen todas las condiciones de negocio. El usuario nunca interactúa directamente con la ruta del archivo en el servidor.

Beneficios de este esquema de descarga protegida

Este patrón es la base ideal para construir una tienda de productos digitales dentro de tu aplicación Laravel. Los archivos se almacenan de forma segura, el acceso se controla completamente desde el código, y puedes añadir cualquier lógica adicional —límites de descargas por licencia, expiración, registro de auditoría— sin tocar la infraestructura de almacenamiento.

8. Conclusión y consejos finales

Descargar archivos en Laravel es tan simple o tan robusto como el caso de uso lo requiera. Estos son los criterios que uso para decidir el enfoque:

  • Para archivos públicos sin restricción: response()->download() con public_path() es suficiente.
  • Para archivos protegidos o de pago: usa discos en Storage fuera de public y protege las rutas con middleware y lógica de autorización en el controlador.
  • Para archivos grandes o servicios externos: streamDownload() y URLs temporales de S3 son tus mejores aliados.

En mi experiencia, la clave no está tanto en la función concreta que uses, sino en entender el contexto de seguridad de tu aplicación. Laravel te da todas las herramientas; la diferencia está en cómo las combinas y en dónde pones los límites de acceso.

Preguntas frecuentes

  • ¿Cuál es la diferencia entre public_path() y storage_path()?
    • public_path() apunta a la carpeta public, accesible directamente desde el navegador. storage_path() apunta a la carpeta storage, que es privada y solo accesible mediante código PHP en tu aplicación.
  • ¿Puedo descargar varios archivos a la vez?
    • Sí. Puedes usar la clase nativa de PHP ZipArchive para generar un archivo .zip en tiempo real y devolverlo con response()->download(). También existen paquetes de la comunidad Laravel que simplifican este proceso.
  • ¿Se puede forzar el nombre del archivo descargado?
    • Sí. Usa el segundo parámetro del método download(): response()->download($path, 'mi-archivo.pdf'). Esto es especialmente útil cuando el archivo está guardado con un nombre interno (hash, UUID) y quieres que el usuario reciba un nombre descriptivo.
  • ¿Qué es deleteFileAfterSend() y cuándo usarlo?
    • Es un método encadenable sobre la respuesta de descarga que elimina el archivo del servidor automáticamente una vez que se ha enviado al cliente: response()->download($path)->deleteFileAfterSend(true). Es útil para archivos generados temporalmente (PDFs dinámicos, exportaciones, zips) que no necesitas conservar en disco.

El siguiente paso es optimizar el rendimiento de tu aplicación aprendiendo a usar la Cache en Laravel.

Aprende a descargar archivos en Laravel con response()->download(), discos protegidos en Storage, streaming con streamDownload() y URLs temporales en S3. Incluye ejemplos reales, control de acceso por usuario y deleteFileAfterSend().


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.