Saltar al contenido principal

¿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:
DriverUso
localSistema de archivos local del servidor
publicDisco local para archivos accesibles desde la web
s3Amazon S3 (o servicios compatibles con S3)
sftpServidor SFTP
ftpServidor 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.
FILESYSTEM_DISK=s3

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.
1

Crear el enlace simbólico

php artisan storage:link
2

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'),

Metadatos del archivo

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">
Última modificación el 13 de julio de 2026