Descarga de imágenes: diseño técnico completo
Documento técnico de la implementación operativa vigente de descarga de imágenes de Rial AI. Cubre frontend, backend, base de datos, Cloud Tasks, worker, GCS, seguridad, eficiencia, resiliencia, observabilidad y pruebas.
Cuando una guía y el código difieren, esta página describe el código y registra la brecha.
1. Resumen
Rial separa la descarga en dos fases:
- Preparación asíncrona: el frontend solicita un
IMAGE_DOWNLOAD; el backend autoriza, crea unJoby encola una task.images-downloadconsulta las imágenes y genera una signed URL por archivo. - Transferencia local: el frontend hace polling hasta
COMPLETED, descarga directamente desde GCS, genera el ZIP en un Web Worker y lo guarda en disco o como Blob.
El API no transporta todos los bytes ni construye el ZIP principal. La unidad de coordinación es Job; el trabajo se divide en JobBatch.
La optimización central es separar preparar el acceso de mover los bytes: reduce memoria, CPU, conexiones largas y timeouts del API, a cambio de usar recursos del navegador.
2. Alcance, permisos y contenido
| Actor | Crear | Completar estado cliente | Scope |
|---|---|---|---|
COMPANY_ADMIN | DOWNLOADER | Sí | compañía |
BRAND_ADMIN | DOWNLOADER | Sí | marcas autorizadas |
ADMIN | Sí | No usa el cierre cliente | compañía seleccionada |
Los endpoints aplican JWT, roles, autorización de recurso y DOWNLOADER. El worker vuelve a cargar usuario y scope. Un ID fuera de scope debe rechazar todo: no se crean tasks ni se filtran URLs.
La policy valida combinación válida de IDs/filtros, filtros incompatibles (brandId + projectId, projectName + projectId), estados permitidos, existencia de contenido y uso de companyId sólo por ADMIN.
| Modo | Selección |
|---|---|
| Inputs | ADMIN + garmentImagesOnly=true; excluye GarmentType.BYPASS |
| Generadas | GeneratedImage.isFinal=true, opcionalmente imageStatuses |
| Rechazadas | REJECTED no final inmediatamente anterior a PENDING_REGENERATION final |
| Mixta | Sólo elementos válidos del scope; BYPASS no cuenta como generada |
Los rechazos pueden incluir downloads/{jobId}/reporte_rechazos.xlsx. Una GarmentImage compartida se resuelve con el proyecto solicitado.
3. Contrato HTTP
Prefijo: /api/v1.
Disponibilidad
GET /stats/project-images/download-availability
Authorization: Bearer <jwt>Devuelve inputsCount, pendingInputsCount, approvedImagesCount, generatedImagesCount y rejectedCount. El frontend usa exactamente el mismo alcance. El backend calcula los contadores en paralelo; generadas incluye estados finales PENDING_APPROVAL, APPROVED, UNPUBLISHED e INCOMPLETE.
Crear job
POST /downloads/project-images?status=PENDING_DOWNLOAD&projectId=<public-id>
Authorization: Bearer <jwt>
Content-Type: application/json
{ "ids": ["project-image-public-id"] }Filtros: status, garmentImagesOnly, imageStatuses[], projectName, projectId, companyId, brandId, projectType, startDate, endDate, isPrioritized.
Respuesta: { "jobId": "uuid" }.
El backend crea Job(status=PENDING, progress=0, type=IMAGE_DOWNLOAD), guarda userId, query, IDs y batchSize=200, y encola el orchestrator. Si falla el enqueue, marca FAILED.
Polling
GET /jobs/<jobId>
Authorization: Bearer <jwt>Resultado conceptual: { status, progress, totalBatches, currentBatch, result: { totalImages, signedUrls[] } }. Las signed URLs son credenciales temporales: nunca deben aparecer en logs, errores o documentación.
Completar estado cliente
POST /downloads/project-images/complete?status=PENDING_DOWNLOAD&projectId=<public-id>
Authorization: Bearer <jwt>
Content-Type: application/json
{ "ids": ["project-image-public-id"] }Actualiza sólo ProjectImage.clientStatus=PENDING_DOWNLOAD a COMPLETED, con el mismo scope, y recalcula estados mediante StatusService. Es repetible e ignora filas ya completadas. Sólo se llama para clientes y sin fallos de transferencia.
4. Datos y estados
Job: publicId, type, status, payload, result, progress, totalBatches,
currentBatch, startedAt, finishedAt, errorMessage
JobBatch: jobId, batchNumber, status, result, errorMessage, startedAt, finishedAtEstados de Job: PENDING, RUNNING, COMPLETED, FAILED, RETRYING, CANCELLED. Estados de JobBatch: PENDING, RUNNING, DONE, FAILED. unique(jobId, batchNumber) es parte de la idempotencia.
currentBatch se expone como el batch actual, pero la fase vigente lo actualiza con la cantidad de batches DONE.
El código tiene CANCELLED, pero no existe endpoint de cancelación en DownloadsController. El frontend cancela polling y ZIP localmente; una task remota ya entregada puede continuar.
5. Secuencia
6. Backend
DownloadsController sólo compone HTTP. DownloadsService delega en JobsService, que crea el job, persiste filtros/IDs y encola Cloud Tasks. La creación de DB y enqueue no son transacción distribuida; un fallo entre ambos puede dejar un PENDING huérfano. Para una plataforma nueva: outbox o reconciliación periódica.
buildDownloadProjectImagesQuery traduce projectId a project.publicId, projectName al folder, brandId a marca, usuarios no admin a user.companyId, BRAND_ADMIN a marcas asignadas, status a adminStatus/clientStatus, IDs a ProjectImage.publicId, y aplica fechas/prioridad.
El scope debe volver a aplicarse al consultar GarmentImage, GeneratedImage y relaciones compartidas. Validar sólo el padre no protege automáticamente los hijos.
Cloud Tasks usa POST, body base64 { jobId }, OIDC, audience del worker y nombre determinístico download-{jobId}. El cierre masivo condicionado a PENDING_DOWNLOAD evita duplicar side effects.
7. Worker images-download
handler -> DownloadsProcessor -> JobPhasesService
orchestrator -> batch -> finalize -> DownloadsServiceEl handler mapea 405 método, 400 validación, 404 inexistente y 500 transitorio/inesperado. El processor omite jobs COMPLETED/FAILED y orchestrators con totalBatches ya definido.
Orchestrator
- Carga job, usuario y payload.
- Cuenta garments y generated images según el modo.
- Rechaza cero imágenes.
- Calcula
ceil(imagesCount / batchSize). - Crea batches con
skipDuplicates. - Persiste
totalBatches,progress=0,totalImages. - Encola una task determinística por batch.
Batch
El batch calcula skip = (batchNumber - 1) * batchSize y take = batchSize. Verifica que no esté DONE, marca RUNNING, consulta con el mismo scope, genera URLs con concurrencia 3, persiste { signedUrls }, marca DONE y calcula floor(doneBatches / totalBatches * 100). Si falla la persistencia, debe responder error para que Cloud Tasks reintente.
Finalize
Lee todos los batches, espera que todos estén DONE, concatena resultados por número y actualiza el job sólo si sigue RUNNING. Dos batches que finalizan simultáneamente no producen doble finalización.
Generated usa isFinal=true y relaciones autorizadas. Inputs excluye BYPASS y construye destino con folder, marca y género. Rechazadas combina la versión rechazada y su regeneración pendiente; el reporte Excel sólo se genera en el último batch.
El código reconsulta cada batch con skip/take; si el dataset cambia durante la descarga, los límites pueden desplazarse.
8. Signed URLs y GCS
El bucket es privado; el worker usa file.getSignedUrl V4 con acción read y una expiración actual de 24 horas. Las URLs no deben guardarse en localStorage ni aparecer en logs.
El worker persiste las URLs en JobBatch.result y Job.result.
9. Frontend
useDownloadImages solicita showSaveFilePicker si está disponible, llama al API, guarda { jobId, enqueuedAt, zipName } en localStorage, inicia el toast y comienza useJobPolling.
Polling: consulta inmediata; 3 s al inicio, 5 s en polls intermedios, 8 s después; timeout global 5 minutos. Se pausa con pestaña oculta y se reanuda con consulta inmediata. Un token de sesión invalida operaciones anteriores.
Reanudación: TTL local 10 minutos. COMPLETED vuelve a ofrecer URLs; FAILED/CANCELLED muestra error y limpia; jobs activos retoman polling; jobs inexistentes/expirados se eliminan. Esto reanuda preparación, no bytes parciales.
useZipDownloadWorker usa client-zip, FileSystemWritableFileStream y fallback Blob/file-saver. createSignedUrlEntryGenerator hace fetch anticipado, transmite response.body mediante TransformStream, cuenta bytes, usa AbortController y normaliza rutas eliminando /, . y ...
La escritura directa evita retener el ZIP completo en RAM; el fallback Blob sí lo acumula. El progreso reserva 90% a fetch y 10% a ZIP, con eventos agrupados cada 100 ms. HTTP no OK, body ausente o URL inválida incrementan failedCount; si todos fallan hay error, si algunos funcionan se entrega ZIP parcial.
La cancelación actual aborta fetch/streams, writable, worker, polling y estado local. No cancela la task remota.
10. Parámetros actuales
El payload usa batchSize=200 y el worker firma con concurrencia 3. El backend legacy de base images usa concurrencia 20, independiente de la del worker.
La implementación consulta y cuenta datos con el scope del usuario, proyecta los campos necesarios y pagina cada batch mediante skip/take.
JPEG/PNG se comprimen en el ZIP del navegador. La ruta legacy de base images usa zlib nivel 1.
11. Resiliencia e idempotencia
| Fase | Protección |
|---|---|
| Orchestrator | no repetir si existe totalBatches |
| Batches | unique (jobId, batchNumber) + skipDuplicates |
| Queue | task IDs determinísticos + ALREADY_EXISTS |
| Batch | omitir si ya está DONE |
| Finalize | update condicional status=RUNNING |
| Polling | settle idempotente + token |
| Complete | actualizar sólo PENDING_DOWNLOAD |
Si se crean URLs y falla persistencia, el batch no está completo y debe reintentarse. Si una transferencia browser falla, no se llama complete ni se marcan imágenes como descargadas.
12. Seguridad y observabilidad
JWT para usuarios, OIDC para Cloud Tasks, bucket privado, object keys con scope exacto, revalidación de tenant en cada fase, TTL corta, limpieza de resultados y normalización de rutas ZIP. No loggear URLs, JWT, headers ni payloads sensibles.
Logs: jobId, batchNumber, intento, fase, transición, duración y counts. El flujo registra jobs por estado, duración de orchestrator/batch, URLs por segundo, bytes, fallos por URL y retries.
13. Estrategia de pruebas
Backend: roles/scope, filtros incompatibles, IDs inexistentes/externos, disponibilidad por tipo, payload del job, fallo de enqueue, cierre idempotente y carreras.
Worker: handler 405/400/404/500 y retry; terminales/duplicados; orchestrator sin imágenes; batches y enqueue duplicado; batch DONE; fallos de URL/persistencia; finalize concurrente; queries de scope; concurrencia; storage y redacción; integración completa con Prisma.
Frontend: query strings, persistencia, backoff, timeout, visibility pause, reanudación de todos los estados, cancelación, no complete con fallos ni para ADMIN, progreso no 100% en error, rutas ZIP, URL expirada, fallback Worker/hilo y File System Access/Blob.
Carga: 10, 100, 500, 2.000 y 10.000 imágenes; tamaños variados; fallos GCS; tasks duplicadas; múltiples instancias; reload durante polling/ZIP; memoria, latencia, egress, retries y throughput.
14. Estado actual y límites conocidos
- Cancelación:
DownloadsControllerno expone un endpoint remoto; la UI sólo cancela polling y transferencia local. - TTL: el worker configura actualmente 24 horas para las signed URLs de descarga.
- ZIP: documentación antigua describe ZIP generado por worker; el flujo vigente genera URLs y comprime en browser.
downloadModelsBaseImageses otra ruta. - Progreso:
currentBatchdocumentado como activo, pero guardado como batches terminados. - Reanudación: guarda job/nombre, no checkpoint de bytes.
- DB + queue: no son atómicos; pueden quedar jobs huérfanos.
- Paginación:
skip/takepuede cambiar si cambia el dataset.
15. Fuentes revisadas
Backend: src/data/downloads, src/data/jobs, src/lib/policies/download.policies.ts,
src/lib/query/build-download-project-images-query.ts,
src/services/cloud-tasks/cloud-tasks.service.ts, prisma/schema/job.prisma
Worker: images-download/src/handler.ts, processor/downloads.processor.ts,
services/job/phases.service.ts, services/downloads.service.ts,
services/buckets.service.ts, services/cloud-tasks.service.ts
Frontend: src/hooks/download, src/hooks/useJobPolling.ts,
src/hooks/download/useZipDownloadWorker.ts, src/workers/zip-download.worker.ts,
src/lib/download, src/services/download.service.tsSi cambian estados, contratos, TTL, cancelación o estrategia ZIP, deben actualizarse esta página y las skills de flujo correspondientes.