¿Qué es el almacenamiento de archivos?
Laravel ofrece una potente capa de abstracción del sistema de archivos basada en el paquete PHP Flysystem.
Puedes manipular con la misma API distintos backends de almacenamiento (disco local, SFTP, Amazon S3…), por lo que no necesitas cambiar el código al pasar de uno a otro según el entorno.
Al cambiar de driver, el código no cambia. Puedes usar el disco local en desarrollo y S3 en producción con configuración muy simple.
Configuración
La configuración del sistema de archivos se centraliza en config/filesystems.php.
Ahí defines los “discos”. Un disco es la combinación de un driver y una ubicación de almacenamiento.
Los drivers principales son:
| Driver | Uso |
|---|
local | Sistema de archivos local del servidor |
public | Disco local para archivos accesibles desde la web |
s3 | Amazon S3 (o servicios compatibles con S3) |
sftp | Servidor SFTP |
ftp | Servidor FTP |
Driver local
Con el driver local, las operaciones se realizan tomando como base la ruta root de la configuración filesystems.
Por defecto, root es storage/app/private.
use Illuminate\Support\Facades\Storage;
Storage::disk('local')->put('example.txt', 'Contents');
// Se escribe en storage/app/private/example.txt
Cambiar el disco por defecto
Cambia el disco por defecto con la variable FILESYSTEM_DISK.
Disco public y enlaces simbólicos
El disco public sirve para almacenar archivos accesibles desde la web.
Por defecto se guardan en el directorio storage/app/public.
Para que sean accesibles desde el servidor web, crea un enlace simbólico de public/storage a storage/app/public.
Crear el enlace simbólico
Obtener la URL del archivo
Tras crear el enlace, puedes generar la URL con el helper asset.echo asset('storage/file.txt');
Si necesitas enlaces simbólicos adicionales, configúralos en el array links de config/filesystems.php.'links' => [
public_path('storage') => storage_path('app/public'),
public_path('images') => storage_path('app/images'),
],
Para eliminar el enlace simbólico usa storage:unlink.
php artisan storage:unlink
Operaciones básicas con la fachada Storage
Leer archivos
use Illuminate\Support\Facades\Storage;
// Obtener el contenido como string
$contents = Storage::get('file.jpg');
// Obtener y decodificar un JSON
$data = Storage::json('orders.json');
// Comprobar si existe
if (Storage::exists('file.jpg')) {
// El archivo existe
}
// Comprobar que no existe
if (Storage::missing('file.jpg')) {
// El archivo no existe
}
Escribir archivos
// Escribir en un archivo
Storage::put('file.jpg', $contents);
// Escribir un recurso como stream
Storage::put('file.jpg', $resource);
// Anteponer o añadir texto
Storage::prepend('file.log', 'Prepended Text');
Storage::append('file.log', 'Appended Text');
// Copiar y mover
Storage::copy('old/file.jpg', 'new/file.jpg');
Storage::move('old/file.jpg', 'new/file.jpg');
Si put falla devuelve false por defecto.
Con 'throw' => true en la configuración del disco puedes hacer que lance excepciones.
Eliminar archivos
// Un solo archivo
Storage::delete('file.jpg');
// Varios archivos
Storage::delete(['file.jpg', 'file2.jpg']);
// En un disco concreto
Storage::disk('s3')->delete('path/file.jpg');
Respuesta de descarga
// Devuelve una respuesta que provoca la descarga en el navegador
return Storage::download('file.jpg');
// Con nombre y cabeceras
return Storage::download('file.jpg', 'my-file.jpg', $headers);
Generación de URLs
URL normal
use Illuminate\Support\Facades\Storage;
$url = Storage::url('file.jpg');
Con el driver local devuelve una URL relativa como /storage/file.jpg.
Con s3 devuelve la URL remota completa.
URLs temporales
Para generar URLs con caducidad usa temporaryUrl.
Disponible en los drivers local y s3.
use Illuminate\Support\Facades\Storage;
$url = Storage::temporaryUrl(
'file.jpg',
now()->plus(minutes: 5)
);
En S3 puedes indicar parámetros adicionales.
$url = Storage::temporaryUrl(
'file.jpg',
now()->plus(minutes: 5),
[
'ResponseContentType' => 'application/octet-stream',
'ResponseContentDisposition' => 'attachment; filename=file.jpg',
]
);
Si necesitas una URL temporal para subir, usa temporaryUploadUrl.
Es útil en configuraciones serverless en las que el cliente sube directamente a S3.['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
'file.jpg', now()->plus(minutes: 5)
);
Subida de archivos
Patrón habitual para guardar un archivo enviado por un formulario.
store (genera nombre automáticamente)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserAvatarController extends Controller
{
public function update(Request $request): string
{
// El nombre se genera automáticamente a partir del MIME
$path = $request->file('avatar')->store('avatars');
return $path;
}
}
storeAs (nombre explícito)
$path = $request->file('avatar')->storeAs(
'avatars',
$request->user()->id
);
Subir a un disco concreto
// Guarda en el disco s3
$path = $request->file('avatar')->store(
'avatars/' . $request->user()->id,
's3'
);
Subir con la fachada Storage
use Illuminate\Support\Facades\Storage;
// Nombre automático
$path = Storage::putFile('avatars', $request->file('avatar'));
// Nombre explícito
$path = Storage::putFileAs(
'avatars',
$request->file('avatar'),
$request->user()->id
);
getClientOriginalName() y getClientOriginalExtension() no son seguros porque el usuario puede manipularlos.
Para el nombre usa hashName() y para la extensión extension().$file = $request->file('avatar');
$name = $file->hashName(); // Nombre aleatorio único
$extension = $file->extension(); // Extensión deducida del MIME
Visibilidad de los archivos
En Flysystem, la privacidad se gestiona con visibility.
use Illuminate\Support\Facades\Storage;
// Indica la visibilidad al escribir
Storage::put('file.jpg', $contents, 'public');
// Consultar y cambiar la visibilidad
$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public');
Para guardar un archivo subido como público, usa storePublicly.
$path = $request->file('avatar')->storePublicly('avatars', 's3');
Uso de varios discos
Cambia de disco con disk.
// En el disco por defecto
Storage::put('avatars/1', $content);
// En s3
Storage::disk('s3')->put('avatars/1', $content);
// Crear un disco on-demand
$disk = Storage::build([
'driver' => 'local',
'root' => '/path/to/root',
]);
$disk->put('image.jpg', $content);
Configuración del almacenamiento en la nube (S3)
Instalación del paquete
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
Variables de entorno
Configura las credenciales de S3 en .env.
AWS_ACCESS_KEY_ID=your-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-name
AWS_USE_PATH_STYLE_ENDPOINT=false
Servicios compatibles con S3 como DigitalOcean Spaces, Cloudflare R2 o Vultr Object Storage también funcionan con el driver s3.
Indica su endpoint en la opción endpoint.'endpoint' => env('AWS_ENDPOINT', 'https://your-endpoint.example.com'),
use Illuminate\Support\Facades\Storage;
// Tamaño (bytes)
$size = Storage::size('file.jpg');
// Última modificación (timestamp UNIX)
$time = Storage::lastModified('file.jpg');
// Tipo MIME
$mime = Storage::mimeType('file.jpg');
// Ruta absoluta (driver local)
$path = Storage::path('file.jpg');
Operaciones con directorios
use Illuminate\Support\Facades\Storage;
// Archivos del directorio
$files = Storage::files($directory);
// Archivos incluyendo subdirectorios
$files = Storage::allFiles($directory);
// Subdirectorios
$directories = Storage::directories($directory);
// Crear directorio
Storage::makeDirectory($directory);
// Eliminar el directorio y su contenido
Storage::deleteDirectory($directory);
Pruebas
Con Storage::fake() puedes probar operaciones de archivo sin tocar el disco real.
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
test('se puede subir un avatar', function () {
Storage::fake('avatars');
$file = UploadedFile::fake()->image('avatar.jpg');
$this->post('/user/avatar', ['avatar' => $file]);
// Comprueba que se ha guardado
Storage::disk('avatars')->assertExists('avatar.jpg');
// Comprueba que otro archivo no existe
Storage::disk('avatars')->assertMissing('other.jpg');
});
Para usar UploadedFile::fake()->image() necesitas la extensión GD de PHP.
Caso práctico: subida de una imagen de perfil
Ejemplo de un controlador que combina validación, guardado y registro de la ruta en la BD.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
class ProfileController extends Controller
{
public function updateAvatar(Request $request): RedirectResponse
{
$request->validate([
'avatar' => ['required', 'image', 'max:2048'],
]);
$user = $request->user();
// Elimina el avatar existente
if ($user->avatar_path) {
Storage::disk('public')->delete($user->avatar_path);
}
// Guarda el nuevo avatar
$path = $request->file('avatar')->store('avatars', 'public');
// Guarda la ruta en la BD
$user->update(['avatar_path' => $path]);
return back()->with('status', 'avatar-updated');
}
}
En la plantilla usa Storage::url() para obtener la URL.
<img src="{{ Storage::disk('public')->url($user->avatar_path) }}" alt="Avatar">