Cargando…

Esto está tardando más de lo esperado.

Volver al centro de ayuda

Archivos, imágenes y almacenamiento

A dónde van los archivos subidos, por qué S3 es el valor por omisión, y cómo se guarda una subida de Livewire a través de la biblioteca de medios.

Esta guía cubre el disco en el que escribe la aplicación, las variables que necesita, cómo trabajar con el disco local en su lugar, y el único proceso por el que pasa una subida para guardarse: la biblioteca de medios, que es también donde vive la foto de perfil. La necesitas cuando configuras un entorno nuevo y la primera vez que un modelo tuyo tiene foto.

El disco por omisión es S3

config/filesystems.php pone el disco por omisión en env('FILESYSTEM_DISK', 's3'), y .env.example escribe FILESYSTEM_DISK=s3. El kit asume un bucket en todas partes, incluida tu máquina: toda subida y toda llamada a Storage:: sin nombre de disco va a él.

Variable En .env.example Qué hace
FILESYSTEM_DISK s3 El disco por omisión: s3, public o local
AWS_ACCESS_KEY_ID relleno Credenciales, compartidas con el mailer ses y el servicio de SMS
AWS_SECRET_ACCESS_KEY relleno Igual
AWS_DEFAULT_REGION us-west-1 La región del bucket
AWS_BUCKET weblabor-test El bucket. Pon el tuyo
AWS_USE_PATH_STYLE_ENDPOINT false true para servicios que direccionan los buckets por ruta, como MinIO
AWS_URL no está URL pública base de los archivos, cuando un CDN o un dominio propio sirve el bucket
AWS_ENDPOINT no está El endpoint de API de un servicio compatible con S3; déjalo vacío para AWS

El disco s3 tiene visibility en public, así que todo archivo que se escribe en él se puede leer en su URL directa, y throw en false, así que una escritura fallida regresa false y no registra nada en vez de lanzar un error. Cuando una subida parece no hacer nada, revisa las credenciales y la política del bucket antes que el código.

Usar un disco local en su lugar

Hay dos discos locales definidos. local escribe en storage/app/private, no es público y sirve los archivos con URL temporales firmadas. public escribe en storage/app/public, es público y arma las URL como APP_URL/storage/{ruta}. Para una máquina sin bucket, usa public:

FILESYSTEM_DISK=public
php artisan storage:link

storage:link crea el enlace simbólico public/storage apuntando a storage/app/public, la única entrada de links en config/filesystems.php. Sin él la URL se arma pero el servidor web no encuentra nada ahí, así que cada imagen es un enlace roto. Córrelo una vez por máquina y en cada despliegue que arranque de un clon nuevo.

Las URL de imagen salen de getImageUrl($path, $default = null, $disk = null), el helper detrás de $user->avatar: regresa $default cuando la ruta está vacía o el archivo no está en el disco, la URL directa cuando el visibility del disco es public, y si no, una URL temporal válida por cinco minutos, guardada en caché ese mismo tiempo. El disco local también funciona, entonces, a cambio de una URL firmada por imagen.

Qué produce saveImagesWithThumbs

saveImagesWithThumbs($source, $folder, $name, $width, $height) guarda una imagen redimensionada con miniaturas bajo una ruta simple, y deleteImagesWithThumbs($path) borra un archivo junto con sus miniaturas. El helper está en el paquete weblabormx/laravel-front y redimensiona con intervention/image sobre su driver Imagick, así que PHP necesita la extensión imagick donde corran las subidas. Por cada subida escribe, en la carpeta que pasaste:

Sufijo Tamaño Ajuste
ninguno $width×$height como máximo, reducida sólo cuando es mayor Conserva proporciones
s 90×90 Cuadrado recortado
b 160×160 Cuadrado recortado
t 160×160 Conserva proporciones
m 320×320 Conserva proporciones
l 640×640 Conserva proporciones
h 1024×1024 Conserva proporciones

El sufijo va antes de la extensión: billing-addons/addon-1725360000-a1b2c3.jpg tiene al lado billing-addons/addon-1725360000-a1b2c3b.jpg. Los seis tamaños son config('front.thumbnails') del paquete; publica config/front.php para cambiarlos. Una subida heic, heif o avif se convierte a JPEG con calidad 90 y se guarda con extensión .jpg. Todo archivo se escribe con visibilidad public.

Lee una miniatura con getThumb($rutaOUrl, 'b'), que inserta el sufijo en el último segmento de la cadena que le des, sea ruta o URL completa. Primero llama a validateGetThumb() de app/Helpers/base.php, que regresa la cadena intacta cuando empieza con https://api.dicebear.com o https://www.gravatar.com: son avatares generados sin miniaturas a las que apuntar. Agrega ahí tus propios hosts cuando un modelo pueda guardar una imagen externa.

Qué hace Livewire antes de que corra tu código

Livewire guarda primero la subida en una ubicación temporal; config/livewire.php la gobierna bajo temporary_file_upload:

Llave Valor Efecto
disk LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK, vacío El disco de los archivos temporales; vacío significa el disco por omisión, o sea S3 de fábrica
rules required, file, max_file_weight: MULTIMEDIA_MAX_FILESIZE El límite de subida del proyecto, 110 MB por omisión, en lugar de los 12 MB de Livewire. Se cambia solo con MULTIMEDIA_MAX_FILESIZE; la regla propia de cada pantalla puede ser más estricta, nunca más laxa. Un archivo que lo pasa se rechaza con la misma redacción de la comprobación del navegador, en MB
directory null livewire-tmp en ese disco
middleware null throttle:60,1 en el endpoint de subida
preview_mimes lista Extensiones a las que se les permite una URL temporal de vista previa; incluye heic y heif
max_upload_time 60 Minutos antes de que una subida inconclusa se invalide; suficiente para un archivo en el límite con una conexión lenta
cleanup true Los archivos temporales con más de 24 horas se eliminan

Dos consecuencias importan. Con el disco temporal en S3, el navegador sube directo al bucket con una URL prefirmada, así que las reglas CORS del bucket deben permitir PUT desde tu dominio; en un disco local el archivo pasa por tu servidor y no hay CORS de por medio. La biblioteca de medios y los campos de archivo e imagen de tus formularios no usan esa URL única en S3: suben por partes de 10 MB, cada una con su propio permiso corto, reintentadas si fallan y retomadas tras un corte de conexión, y revisan el tamaño antes de empezar y otra vez cuando el archivo se une, así que el límite se cumple también en S3. La regla CORS del bucket y sus permisos para esto están en las notas de despliegue de la documentación para desarrolladores. Y un campo que lee su subida a través de temporaryUrl() sólo funciona con las extensiones de preview_mimes: avif no está en esa lista, así que un campo cuyas reglas acepten AVIF y que lea el archivo de esa forma pasa la validación y luego falla con una excepción de "no previsualizable". Agrega 'avif' a preview_mimes si quieres aceptarla. En un disco local esa URL es una ruta firmada sobre APP_URL, que el propio servidor descarga, así que APP_URL debe resolverse desde la máquina que corre PHP. El proceso de la biblioteca de medios que sigue abajo lee el archivo desde su ruta temporal guardada en vez de llamar a temporaryUrl(), así que nunca choca con este límite. Toma de ahí un archivo de cualquier tipo — un PDF, una pista de audio, un video o un SVG además de una imagen — y elimina la copia temporal en cuanto el archivo se guarda o se rechaza. Aun así, una pantalla puede acotarlo. La imagen de una categoría solo acepta imágenes — SVG, JPEG, PNG, GIF, WebP, HEIC o AVIF: su diálogo de archivos ofrece solo imágenes, y cualquier otro archivo se rechaza antes de guardarse, con "La imagen debe ser un archivo de imagen." debajo del campo, así que nunca ocupa espacio.

La foto de perfil pasa por la biblioteca de medios

La foto de perfil es un registro Media en la biblioteca de medios propia de la cuenta, referenciado por una llave foránea avatar_media_id en users, no por una columna de ruta. App\Livewire\Auth\MyProfile muestra el componente de subida de imágenes, el mismo que usa cualquier formulario para guardar archivos en la biblioteca de medios, y App\Livewire\Traits\HasProfilePhoto aplica lo que eligió:

<img src="{{ $user?->avatarForSize(112) }}" alt="{{ $user->name }}">
<x-image-uploader wire:model.live="photo" :model="$user" relation="avatarMedia" :max-size="5120" />
public function updatedPhoto()
{
    $changes = is_array($this->photo) ? $this->photo : [];
    if (empty($changes['attach']) && empty($changes['detach']) && empty($changes['delete'])) {
        return;
    }

    $this->user->saveAvatarChanges($changes);
    $this->user->refresh();
    $this->photo = [];

    $this->dialog()->success(__('Success'), __('Avatar updated correctly'));
}

El componente sube la imagen en cuanto se elige, con una barra de progreso, y wire:model.live="photo" pasa su elección a updatedPhoto() de inmediato; no hay botón Guardar. User::saveAvatarChanges() relaciona la imagen con la cuenta y escribe su id en avatar_media_id, y quitar la foto actual en el componente deja la columna vacía. La subida corre a través de App\Jobs\Media\FileUpload, que la guarda en la carpeta raíz de medios propia de la cuenta y genera las conversiones que define la biblioteca de medios (thumb-64, thumb-128, thumb, thumb-512), el mismo proceso que usa la pantalla de la Biblioteca de medios (/app/media) para cualquier otra subida. Leer la imagen de vuelta nunca toca getThumb() ni getImageUrl(): $user->avatarForSize($displaySize) elige la conversión generada más pequeña que aún cubra el doble del tamaño pedido, y $user->avatar siempre regresa la conversión thumb. Cualquiera de las dos cae en un avatar generado por Dicebear a partir del correo de la cuenta mientras avatar_media_id esté vacío.

Subir una foto nueva nunca borra la anterior: el registro Media viejo se queda en la biblioteca de la cuenta, desligado de ella, y sólo el puntero avatar_media_id se mueve a la subida nueva. Y como esta subida pertenece a la cuenta igual que cualquier otro archivo de su biblioteca, cuenta contra la cuota de espacio en disco de la cuenta. La imagen de una categoría sigue a quien es dueño de la categoría: la que se sube desde /app para una categoría propia del usuario cuenta contra el espacio de ese usuario, igual que la foto de perfil, mientras que la que se sube desde el catálogo de administración va a General/Categorias, en la carpeta General sin dueño de la biblioteca de medios que se abre desde Medios en el panel de administración, y no cuenta contra el espacio de nadie.

La tarjeta Foto de perfil del perfil, con el cargador de imágenes