Almacenamiento de archivos en Laravel: Storage facade explicada
Subir y gestionar archivos es una necesidad habitual en cualquier aplicación web: imágenes de perfil, documentos adjuntos, exportaciones en PDF… Laravel incluye una capa de abstracción del sistema de archivos muy bien diseñada que te permite trabajar con archivos locales, Amazon S3 o cualquier otro proveedor usando exactamente la misma API.
La abstracción del sistema de archivos
La filosofía de Laravel es que el código que escribe y lee archivos no debería saber ni importarle dónde se almacenan esos archivos. En local durante el desarrollo, en S3 en producción… el código es el mismo.
Esto es posible gracias a la librería Flysystem de PHP, sobre la cual Laravel construye su facade Storage.
Configuración: config/filesystems.php
El archivo de configuración config/filesystems.php define los “discos” disponibles:
// config/filesystems.php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
Los tres discos más importantes son:
- local: almacena en
storage/app/private. No es accesible desde la web (privado). - public: almacena en
storage/app/public. Accesible desde la web mediante un enlace simbólico. - s3: Amazon S3 o servicios compatibles (DigitalOcean Spaces, MinIO, etc.).
La facade Storage: operaciones básicas
use Illuminate\Support\Facades\Storage;
// Guardar contenido en un archivo
Storage::put('archivo.txt', 'Contenido del archivo');
// Leer el contenido de un archivo
$contenido = Storage::get('archivo.txt');
// Comprobar si un archivo existe
if (Storage::exists('archivo.txt')) {
// ...
}
// Eliminar un archivo
Storage::delete('archivo.txt');
// Eliminar múltiples archivos
Storage::delete(['uno.txt', 'dos.txt', 'tres.txt']);
// Obtener la URL pública de un archivo
$url = Storage::url('imagenes/foto.jpg');
// Obtener el tamaño en bytes
$bytes = Storage::size('archivo.txt');
// Fecha de última modificación
$timestamp = Storage::lastModified('archivo.txt');
Para usar un disco específico, encadena disk():
// Guardar en S3
Storage::disk('s3')->put('backups/dump.sql', $contenido);
// Leer del disco local
$contenido = Storage::disk('local')->get('privado/datos.json');
Subir archivos desde formularios
El formulario en Blade
<form method="POST" action="/perfil/avatar" enctype="multipart/form-data">
@csrf
@method('PUT')
<input type="file" name="avatar" accept="image/*">
@error('avatar')
<p class="text-red-500">{{ $message }}</p>
@enderror
<button type="submit">Actualizar avatar</button>
</form>
Procesar la subida en el controlador
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
public function updateAvatar(Request $request): RedirectResponse
{
$request->validate([
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
]);
$archivo = $request->file('avatar');
// store() genera un nombre único automáticamente
// Guarda en storage/app/public/avatars/
$ruta = $archivo->store('avatars', 'public');
// storeAs() te permite definir el nombre del archivo
// $ruta = $archivo->storeAs('avatars', $request->user()->id . '.webp', 'public');
// Eliminar el avatar anterior si existe
if ($request->user()->avatar) {
Storage::disk('public')->delete($request->user()->avatar);
}
$request->user()->update(['avatar' => $ruta]);
return back()->with('success', 'Avatar actualizado correctamente.');
}
Información sobre el archivo subido
$archivo = $request->file('avatar');
// Nombre original del archivo en el cliente
$nombreOriginal = $archivo->getClientOriginalName();
// foto_perfil.jpg
// Extensión original
$extension = $archivo->getClientOriginalExtension();
// jpg
// Extensión según el MIME type (más segura)
$extension = $archivo->extension();
// jpg
// Tamaño en bytes
$tamano = $archivo->getSize();
// 245632
// MIME type
$mime = $archivo->getMimeType();
// image/jpeg
// Ruta temporal en el servidor
$rutaTemporal = $archivo->getRealPath();
El disco public y storage:link
El disco public almacena archivos en storage/app/public, pero esta carpeta no es accesible desde el navegador. Para que lo sea, necesitas crear un enlace simbólico:
php artisan storage:link
Este comando crea un symlink desde public/storage hacia storage/app/public. Después de ejecutarlo, los archivos guardados en el disco public son accesibles en:
https://tudominio.com/storage/avatars/nombrearchivo.jpg
Para obtener la URL pública de un archivo:
// Forma recomendada
$url = Storage::disk('public')->url($ruta);
// O usando asset()
$url = asset('storage/' . $ruta);
// En Blade
<img src="{{ Storage::disk('public')->url($user->avatar) }}" alt="Avatar">
// O más corto:
<img src="{{ asset('storage/' . $user->avatar) }}" alt="Avatar">
Visibilidad de archivos
Los archivos pueden ser públicos o privados:
// Guardar como público
Storage::put('archivo.txt', 'contenido', 'public');
// Guardar como privado (por defecto)
Storage::put('archivo.txt', 'contenido', 'private');
// Cambiar la visibilidad
Storage::setVisibility('archivo.txt', 'public');
// Consultar la visibilidad
$visibilidad = Storage::getVisibility('archivo.txt');
// 'public' o 'private'
Mover y copiar archivos
// Copiar un archivo
Storage::copy('imagenes/original.jpg', 'imagenes/copia.jpg');
// Mover (renombrar) un archivo
Storage::move('imagenes/temporal.jpg', 'imagenes/definitivo.jpg');
Streams para archivos grandes
Para archivos grandes, usa streams en lugar de cargar todo el contenido en memoria:
// Leer como stream
$stream = Storage::readStream('videos/pelicula.mp4');
// Guardar desde un stream
Storage::writeStream('backups/dump.sql', $stream);
// Descarga en streaming desde un controlador
public function download(string $filename): StreamedResponse
{
if (! Storage::disk('local')->exists("privados/{$filename}")) {
abort(404);
}
return Storage::disk('local')->download("privados/{$filename}");
}
// O con nombre personalizado
return Storage::disk('s3')->download(
"documentos/{$filename}",
'mi-documento-descargado.pdf'
);
Listar archivos y directorios
// Listar archivos en un directorio
$archivos = Storage::files('imagenes');
// Listar archivos recursivamente
$todosLosArchivos = Storage::allFiles('imagenes');
// Listar directorios
$directorios = Storage::directories('uploads');
// Listar directorios recursivamente
$todosLosDirecctorios = Storage::allDirectories('uploads');
// Crear un directorio
Storage::makeDirectory('usuarios/123/documentos');
// Eliminar un directorio y su contenido
Storage::deleteDirectory('temporales');
Configurar Amazon S3
Primero instala el paquete necesario:
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
Añade las variables al .env:
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=tu_access_key
AWS_SECRET_ACCESS_KEY=tu_secret_key
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=nombre-de-tu-bucket
AWS_URL=https://nombre-de-tu-bucket.s3.eu-west-1.amazonaws.com
Con esto configurado, todo el código que uses con Storage::disk('s3') o con el disco por defecto funcionará con S3 sin ningún cambio adicional.
URLs temporales para archivos privados en S3
Para generar URLs de acceso temporal a archivos privados en S3:
// URL válida durante 30 minutos
$url = Storage::disk('s3')->temporaryUrl(
'documentos/privado.pdf',
now()->addMinutes(30)
);
Consejos para producción
Permisos correctos: en producción, asegúrate de que el directorio storage y sus subdirectorios tienen los permisos correctos:
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
Variables de entorno: nunca hardcodees credenciales de S3 en el código. Usa siempre las variables del .env o el sistema de secrets de tu proveedor cloud.
El symlink en producción: ejecuta php artisan storage:link como parte de tu proceso de despliegue. Si usas Deployer o Envoyer, ya está incluido.
Subidas directas a S3 desde el navegador: para evitar pasar archivos grandes por tu servidor, considera usar las URLs prefirmadas de S3 para subidas directas desde el cliente.
// Generar una URL prefirmada para subida directa
$url = Storage::disk('s3')->temporaryUploadUrl(
'uploads/' . uniqid() . '.jpg',
now()->addMinutes(5),
['ContentType' => 'image/jpeg']
);
Conclusión
La facade Storage de Laravel es una de las partes más bien diseñadas del framework. Abstraer el sistema de archivos significa que puedes desarrollar localmente con archivos en disco y desplegar en producción con S3 sin cambiar una sola línea de código. Dominar esta herramienta te va a ahorrar mucho tiempo y va a hacer tus aplicaciones más robustas y escalables.