From 5751b9ea08ff774a86dad11604a3c16a82953e29 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 15:45:53 +0200 Subject: [PATCH] Remove docs and agents --- AGENTS.md | 41 ---- docs/changes-summary.md | 333 ----------------------------- docs/delete-workflows-plan.md | 205 ------------------ docs/presigned-upload-hardening.md | 263 ----------------------- docs/tech-stack.md | 186 ---------------- 5 files changed, 1028 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 docs/changes-summary.md delete mode 100644 docs/delete-workflows-plan.md delete mode 100644 docs/presigned-upload-hardening.md delete mode 100644 docs/tech-stack.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 41a2ff6..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,41 +0,0 @@ -# Contexto del proyecto para agentes de IA - -## Resumen rápido -- Proyecto backend en `.NET 10`. -- Estructura principal en capas: `Domain`, `Application`, `Infrastructure`. -- Patrón arquitectónico: `Clean Architecture` con enfoque `CQRS`. - -## Arquitectura -- **Domain (`Domain`)**: entidades de negocio puras. -- **Application (`Application`)**: - - Casos de uso (comandos/queries y handlers). - - Contratos (`Application.Contracts`). - - Configuración de aplicación (`Application.Configuration`). - - Mensajería orientada a eventos a través de `IEventBus`. -- **Infrastructure (`Infrastructure`)**: - - Persistencia y acceso a datos. - - Integraciones externas (Keycloak, almacenamiento, procesamiento de imágenes). - - Implementación de mensajería y consumidores. - -## CQRS / Mediación -- Se usa **DispatchR** (paquete `DispatchR.Mediator`) como variante de MediatR para orquestar comandos y consultas. -- Los casos de uso están organizados por feature con handlers dedicados (`Commands`, `Queries`). - -## Mensajería y workers -- Se usa **MassTransit** para publicación/consumo de eventos. -- Broker actual: **RabbitMQ** (`MassTransit.RabbitMQ`). -- Hay modo de registro para workers con consumidores en `Infrastructure.Messaging.Consumers`. -- Actualmente existen consumidores como: - - `ResizePictureConsumer` - - `RemoveFileConsumer` - -## Persistencia e integraciones -- `Entity Framework Core` + proveedor `Npgsql` (PostgreSQL). -- Integración de identidad con `Keycloak`. -- Almacenamiento con `AWS S3`. - -## Notas para agentes -- Mantener separación de responsabilidades por capa. -- Evitar lógica de infraestructura dentro de `Application` y `Domain`. -- Agregar nuevos casos de uso siguiendo el patrón CQRS existente (comando/query + handler). -- Para nuevas integraciones asíncronas, reutilizar el pipeline de `MassTransit` y la abstracción `IEventBus`. diff --git a/docs/changes-summary.md b/docs/changes-summary.md deleted file mode 100644 index c25c997..0000000 --- a/docs/changes-summary.md +++ /dev/null @@ -1,333 +0,0 @@ -# MusifyBackend — Resumen de cambios (2026-05-14) - -Este documento resume **todos los cambios aplicados hasta ahora** en la rama `master` durante esta iteración. - -> Nota: El proyecto compila en `Release` al cierre de estos cambios. - ---- - -## 1) Mensajería robusta: MassTransit + EF Outbox - -**Objetivo**: hacer la publicación de eventos confiable y transaccional (DB + mensajes) y tolerar redelivery en consumidores. - -### Cambios principales -- Se integró **MassTransit EntityFramework Outbox** con PostgreSQL (`UsePostgres` + `UseBusOutbox`). -- Se añadieron entidades/tablas de **Inbox/Outbox** en el `DbContext`. -- Se generó y añadió una **migración** para las tablas de outbox/inbox. -- Se ajustó la configuración de endpoints para aplicar: - - **Retry** (exponential backoff) - - **Idempotencia** en consumo usando `UseEntityFrameworkOutbox()` -- Se ajustó el orden de operaciones para el patrón outbox: **publicar evento y luego `SaveChanges`**. - -### Archivos involucrados (principalmente) -- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` -- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs` -- `Infrastructure/Persistence/Database.cs` -- `Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs` -- `Application/Abstractions/Infrastructure/IEventBus.cs` -- `Infrastructure/MassTransit/EventBus/MassTransitEventBus.cs` - ---- - -## 2) Workflows: Courier RoutingSlip para imágenes y audio - -**Objetivo**: mantener el pipeline asíncrono (descarga, resize/transcode, upload, update de entidades) con reintentos e idempotencia. - -### Fix importante en workflow de imágenes -- Se corrigió el nombre de actividad del paso de **Upload del tamaño Medium** (antes duplicaba el nombre de `ResizeMedium`). - -Archivos: -- `Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs` -- `Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs` - -### Fix importante en compensación de uploads -- `UploadFileToBucketActivity` ahora registra correctamente el **object key completo** (ruta + filename) para que la compensación borre el objeto correcto. - -Archivo: -- `Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs` - ---- - -## 3) Uploads (Opción B): Presigned PUT URL + Create/Update con nombres - -**Objetivo**: separar la subida de binarios (S3) de la operación de dominio (Create/Update), evitando rollbacks complejos y haciendo el flujo más robusto. - -### Endpoint/Commands añadidos (request presigned URL) -- Playlist picture: - - `Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.cs` - - `Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs` - - `Application/PlayLists/Responses/PlayListPictureUploadResponse.cs` -- Track picture + audio: - - `Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs` - - `Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs` - - `Application/Tracks/Responses/TrackUploadUrlsResponse.cs` - -### Storage: presigned PUT -- Se añadió soporte para generar **presigned PUT URLs**: - - `Application/Abstractions/Infrastructure/IStorageService.cs` - - `Infrastructure/Services/StorageService.cs` (`GetUploadUrlAsync`) - ---- - -## 4) Limpieza: eliminar V1 y normalizar nombres (V2 → “normal”) - -**Objetivo**: eliminar comandos/handlers “V1” (subida directa por stream/multipart) y dejar un único set de comandos “normales” con el contrato final. - -### Resultado -- `CreatePlayListCommand`, `UpdatePlayListCommand` y `CreateTrackCommand` quedaron con el contrato nuevo (nombres/metadata, no streams). -- Se eliminaron archivos `*V2*` ya innecesarios. - -Archivos clave: -- `Application/PlayLists/Commands/CreatePlayListCommand.cs` -- `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` -- `Application/PlayLists/Commands/UpdatePlayListCommand.cs` -- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` -- `Application/Tracks/Commands/CreateTrackCommand.cs` -- `Application/Tracks/Handler/CreateTrackCommandHandler.cs` - ---- - -## 5) Prefijo de seguridad: `uploads/{userId}/...` para originales - -**Objetivo**: reducir superficie (y colisiones) haciendo que los objetos originales queden naturalmente “namespaced” por usuario. - -### Cambios -- Las **keys de originales** (playlist original picture, track original picture, track original audio) ahora se generan bajo: - - `uploads/{userId}/...` -- Se añadieron overloads de rutas para construir keys con `userId`. -- Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`. -- Los eventos de workflows (sourceKey) se publican con la key prefijada. -- El borrado de playlist y track usan keys prefijadas dentro de sus workflows de delete. - -Archivos: -- `Application/Configuration/PlayListConfiguration.cs` -- `Application/Configuration/TrackConfiguration.cs` -- `Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs` -- `Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs` -- `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` -- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` -- `Application/Tracks/Handler/CreateTrackCommandHandler.cs` -- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` - - ---- - -## 6) Normalización de separadores en keys S3 (evitar `\\` en Windows) - -**Objetivo**: evitar que `Path.Combine` introduzca separadores `\\` en keys S3 (lo cual es incorrecto/inesperado en S3, y rompe interoperabilidad). - -### Cambios -- Se reemplazaron combinaciones de keys que usaban `Path.Combine` por joins con `/` en puntos críticos. - -Archivos: -- `Application/Configuration/PlayListConfiguration.cs` -- `Application/Configuration/TrackConfiguration.cs` -- `Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs` -- `Infrastructure/Services/StorageService.cs` - ---- - -## 7) Validaciones en Application eliminadas (delegadas a API) - -**Objetivo**: evitar duplicar validaciones de formato (extensiones/strings) si la capa API ya se encarga. - -### Cambios -- Se eliminó el helper de validación creado inicialmente y los checks en handlers. - -Archivos: -- Eliminado: `Application/Validation/UploadObjectNameValidator.cs` -- Ajustados: - - `Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs` - - `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs` - - `Application/Tracks/Handler/CreateTrackCommandHandler.cs` - ---- - -## 8) Documento de mitigaciones (presigned uploads) - -Se añadió documentación de hardening/mitigaciones contra abuso/spam en endpoints presigned. - -Archivo: -- `docs/presigned-upload-hardening.md` - ---- - -## 9) Notas de compatibilidad / impacto - -- Este cambio **rompe compatibilidad** con la API anterior si existían endpoints que aceptaban multipart/streams para Create/Update (ahora se espera el flujo presigned y luego Create/Update con nombres). -- Los objetos originales ahora viven bajo `uploads/{userId}/...`. Objetos antiguos fuera de ese prefijo pueden quedar huérfanos si ya existían de antes. - ---- - -## 10) Estado final (iteración anterior) - -- `dotnet build MusifyBackend.slnx -c Release` ✅ - ---- - -## 11) Deletes unificados con Workflows (RoutingSlip) - -**Objetivo**: homogeneizar deletes de Track/PlayList usando Courier RoutingSlip con actividades granulares e idempotentes. - -### Cambios -- Se añadió `LifeCycleStatus` (`Active`/`Removing`) a `Track` y `PlayList` y se generó migración. -- Track delete: - - Se eliminó la activity monolítica y el slip ahora encadena: marcar como removing → borrar N ficheros (condicional) → borrar en DB. -- PlayList delete: - - Se añadió `DeletePlayListEvent` + consumer + routing slip builder + activities para marcar como removing, borrar pictures (si no son preset) y borrar en DB. -- Se endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado. -- Se eliminaron `RemoveFileEvent`/`RemoveFileConsumer` por quedar sin usos. - -Archivos clave: -- `Domain/Entities/LifeCycleStatus.cs` -- `Domain/Entities/Track.cs` -- `Domain/Entities/PlayList.cs` -- `Application/Events/DeletePlayListEvent.cs` -- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs` -- `Application/Tracks/Handler/DeleteTrackCommandHandler.cs` -- `Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs` -- `Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs` -- `Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs` -- `Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs` -- `Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs` -- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs` -- `Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs` - ---- - -## 12) Hardening de pre-signed uploads (Fases 1 y 2) - -**Objetivo**: evitar abuso de coste/almacenamiento cuando usuarios hacen spam de endpoints presigned sin crear la entidad final (objetos huérfanos en S3). - -Referencia: `docs/presigned-upload-hardening.md` - ---- - -### Fase 1 — Upload Intents en DB - -#### Nuevas entidades (Domain) -- `Domain/Entities/UploadIntent.cs` — tabla `UploadIntents` con campos: `Id`, `UserId`, `Bucket`, `Key`, `ObjectName`, `ContentType`, `ExpectedSizeBytes`, `Purpose`, `Status`, `ExpiresAt`, `CreatedAt`. -- `Domain/Entities/UploadIntentStatus.cs` — enum: `Issued`, `Consumed`, `Expired`. -- `Domain/Entities/UploadIntentPurpose.cs` — enum: `PlayListPicture`, `TrackPicture`, `TrackAudio`. - -#### Nueva configuración (Application) -- `Application/Configuration/UploadIntentConfiguration.cs` — sección `UploadIntent`: - - `UploadUrlExpiresInSeconds` (default: 120) - - `MaxActiveUploadIntentsPerUser` (default: 5) - - `MaxActiveUploadBytesPerUser` (default: 200 MB) - - `MaxUploadBytes` (default: 100 MB, para validación HEAD) - - `DefaultExpectedPictureSizeBytes` / `DefaultExpectedAudioSizeBytes` (fallback cuando el caller no envía tamaño) - - `ExpirationJobIntervalSeconds` / `ExpiredIntentsRetentionDays` - - `TempRootPrefix` / `TempUploadsRetentionDays` / `TempCleanupJobIntervalSeconds` (Fase 2) - -#### Interfaces actualizadas -- `Application/Abstractions/Infrastructure/IDatabase.cs` — añadido `DbSet UploadIntents`. -- `Application/Abstractions/Infrastructure/IStorageService.cs` — añadidos: - - `HeadObjectAsync(bucket, key)` → `ObjectMetadata?` (para validación antes de consumir). - - `ListObjectsAsync(bucket, prefix)` → `IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)>` (para el cleanup job). -- `Application/Abstractions/Infrastructure/ObjectMetadata.cs` — record `(ContentType, ContentLength)`. - -#### Infrastructure actualizada -- `Infrastructure/Persistence/Database.cs` — añadido `DbSet`. -- `Infrastructure/Services/StorageService.cs` — implementaciones de `HeadObjectAsync` y `ListObjectsAsync`. - -#### Commands actualizados (cambio de contrato) -- `RequestPlayListPictureUploadCommand` — nuevo campo opcional `ExpectedSizeBytes?`. -- `RequestTrackUploadUrlsCommand` — nuevos campos opcionales `ExpectedPictureSizeBytes?`, `ExpectedAudioSizeBytes?`. -- `CreatePlayListCommand` — `OriginalPictureName?` → `PictureIntentId?` (Guid). -- `CreateTrackCommand` — `OriginalPictureName` + `OriginalAudioName` → `PictureIntentId` + `AudioIntentId` (Guid). -- `UpdatePlayListCommand` — `NewOriginalPictureName?` → `NewPictureIntentId?` (Guid). - -#### Responses actualizadas -- `PlayListPictureUploadResponse` — añadido `IntentId` (Guid). -- `TrackUploadUrlsResponse` — añadidos `PictureIntentId` + `AudioIntentId` (Guid). - -#### Handlers actualizados - -**Request upload** (`RequestPlayListPictureUploadCommandHandler`, `RequestTrackUploadUrlsCommandHandler`): -- Comprueban cuota de usuario (conteo de intents activos + suma de bytes) antes de generar la URL. -- Crean un registro `UploadIntent` (Status = `Issued`) en la misma llamada. -- TTL reducido a `UploadUrlExpiresInSeconds` (configurable, default 120 s vs. los anteriores 600 s hardcodeados). - -**Create/Update** (`CreatePlayListCommandHandler`, `CreateTrackCommandHandler`, `UpdatePlayListCommandHandler`): -- Reciben `IntentId` en lugar del nombre de fichero. -- Validan el intent: existe, pertenece al usuario, Status = `Issued`, no caducado. -- Realizan `HEAD` del objeto en S3: debe existir y tamaño ≤ `MaxUploadBytes`. -- Marcan el intent como `Consumed` y guardan todo en el mismo `SaveChangesAsync`. - -#### Job de expiración -- `Infrastructure/Jobs/UploadIntentExpirationJob.cs` — `BackgroundService` que: - - Marca como `Expired` los intents `Issued` con `ExpiresAt < now`. - - Elimina intents `Expired` cuya fecha de expiración supera la ventana de retención configurable. - -#### Migración -- `Infrastructure/Persistence/Migrations/*_AddUploadIntentTable.cs` - ---- - -### Fase 2 — Prefijo temporal + cleanup de S3 - -#### Rutas temporales -- `TrackRoutes.BuildTempPicturePath` / `BuildTempAudioPath` — construyen `{TempRootPrefix}/{userId}/{ParentFolder}/{objectName}`. -- `PlayListRoutes.BuildTempPicturePath` — ídem para playlists. - -#### Handlers de request upload -- La presigned URL ahora apunta a `temp/{userId}/...` (prefijo temporal) en lugar de la ruta final. -- El intent almacena la temp key. - -#### Handlers de create/update (consume) -- Al consumir un intent, se llama `CopyFileAsync(tempKey → finalKey)` antes de publicar el evento de procesado. -- El objeto temporal queda en S3 y es eliminado posteriormente por el cleanup job. - -#### Job de cleanup de temporales -- `Infrastructure/Jobs/TempUploadsCleanupJob.cs` — `BackgroundService` que: - - Lista objetos bajo `TempRootPrefix/` usando `ListObjectsAsync`. - - Borra los que superan `TempUploadsRetentionDays` (default 3 días). - - Tolerante a errores por objeto (continúa si un borrado falla). - -#### DI -- `Infrastructure/Services/ServicesDependencyInjection.AddUploadIntentJobs()` — registra `UploadIntentConfiguration` + ambos background jobs. - ---- - -## 13) Refactor: IntentValidationResult (eliminar tuplas) - -**Objetivo**: evitar retornar tuplas `(Result?, UploadIntent?)` desde métodos privados — patrón no idiomático en C#. - -### Cambios -- Creado `Application/UploadIntents/IntentValidationResult.cs` — `internal sealed record` con: - - `Result? Error` / `UploadIntent? Intent` - - `bool IsSuccess` - - Factory methods `Ok(intent)` / `Fail(error)` -- Los tres handlers (`CreatePlayListCommandHandler`, `CreateTrackCommandHandler`, `UpdatePlayListCommandHandler`) ahora usan `IntentValidationResult` en `ValidateAndLoadIntentAsync`. -- El tipo es `internal` (no `private`) porque es compartido entre 3 handlers de distintas carpetas. - ---- - -## 14) Fix: race condition en cuota de intents - -**Objetivo**: garantizar que el check de cuota + insert del intent sean atómicos y no admitan concurrent bypass. - -### Cambios -- Añadida `IDatabaseTransaction` en `Application/Abstractions/Infrastructure/IDatabaseTransaction.cs` — interfaz limpia (`CommitAsync`, `DisposeAsync`) que no expone tipos EF. -- Añadido `BeginTransactionAsync(IsolationLevel, CancellationToken)` a `IDatabase`. -- Implementado en `Infrastructure/Persistence/Database.cs` con un `private sealed class DatabaseTransaction` que envuelve `IDbContextTransaction` de EF. -- `RequestPlayListPictureUploadCommandHandler` y `RequestTrackUploadUrlsCommandHandler`: el bloque quota check + insert ahora se ejecuta dentro de una transacción `SERIALIZABLE`, eliminando la ventana de race condition entre el check y el commit. - ---- - -## 15) Fix: separación AddUploadIntentConfiguration / AddUploadIntentJobs - -**Objetivo**: `UploadIntentConfiguration` es necesaria tanto en el host del API (para los handlers) como en el worker (para los jobs). El método anterior mezclaba config + jobs en uno solo, haciendo imposible registrar la config sin registrar los `BackgroundService`. - -### Cambios -- `AddUploadIntentConfiguration(services, configuration)` — registra solo la config. Lo llama el host del API **y** el worker. -- `AddUploadIntentJobs(services, configuration)` — llama internamente a `AddUploadIntentConfiguration` y además registra `UploadIntentExpirationJob` + `TempUploadsCleanupJob`. Solo lo llama el worker. - -Archivo: `Infrastructure/Services/ServicesDependencyInjection.cs` - ---- - -## 16) Estado final - -- `dotnet build` ✅ — 0 errores, 0 advertencias. diff --git a/docs/delete-workflows-plan.md b/docs/delete-workflows-plan.md deleted file mode 100644 index 22205d7..0000000 --- a/docs/delete-workflows-plan.md +++ /dev/null @@ -1,205 +0,0 @@ -# Plan — Unificar Deletes con Workflows (RoutingSlip) - -## Contexto - -Actualmente hay dos problemas de diseño en los deletes: - -**PlayLists:** el borrado disparaba múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego eliminaba la entidad en DB. Sin workflow, sin trazabilidad. - -**Tracks:** existía un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hacía **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales. - -La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes. - ---- - -## Objetivos - -- **Un único camino** de borrado por aggregate (Track / PlayList), basado en **RoutingSlip con actividades granulares**. -- El handler de comando hace lo mínimo: validar ownership/reglas, cambiar estado a `Removing`, y publicar el evento de borrado (outbox). -- El workflow ejecuta pasos **ordenados, reutilizables y reiniciables** individualmente. -- **Reutilizar** `RemoveFileFromBucketActivity` (ya existente) para cada archivo a borrar, en lugar de lógica ad-hoc dentro de una activity monolítica. -- Eliminar `RemoveFileEvent`, `RemoveFileConsumer` y su registration si no tienen otros usos. - -No objetivos (por ahora): -- No introducir observabilidad extra (tracing/telemetría) fuera de lo ya existente. - ---- - -## Problemas del enfoque actual - -### PlayLists -- El handler implementa orquestación (decide qué archivos borrar y cómo) → acoplamiento alto. -- Publica múltiples `RemoveFileEvent` sueltos, sin agruparlos en un proceso único. -- No hay log de RoutingSlip que permita seguir el progreso/estado. - -### Tracks ← **nuevo problema identificado** -- `DeleteTrackActivity` es monolítica: carga entidad, borra pictures, borra audios, elimina de DB, todo en un solo `Execute`. -- No reutiliza `RemoveFileFromBucketActivity` ya existente. -- Si falla a mitad, no hay granularidad de reintento (no se sabe qué paso falló). -- El RoutingSlip tiene un único nodo → no es realmente un "slip" con pasos. - ---- - -## Diseño propuesto - -## Implementación final (nombres reales) - -Este plan se implementó con los siguientes nombres finales (para facilitar traceo/debug): - -### Colas (consumers) -- `delete-track` (`DeleteTrackConsumer`) -- `delete-playList` (`DeletePlayListConsumer`) - -### Execute endpoints (activities) -MassTransit crea las colas de execute como `_execute`. - -- `remove-file-from-bucket` → cola `remove-file-from-bucket_execute` -- `mark-track-as-removing` → cola `mark-track-as-removing_execute` -- `delete-track-from-db` → cola `delete-track-from-db_execute` -- `mark-playList-as-removing` → cola `mark-playList-as-removing_execute` -- `delete-playList-from-db` → cola `delete-playList-from-db_execute` - -### Estado de borrado (DB) -- Se introdujo `LifeCycleStatus` en `Track` y `PlayList` con valores `Active` y `Removing`. -- El handler marca `Removing` antes de publicar el evento (y persiste con `SaveChanges`). - -> Nota: se eliminó el enfoque de “estado Removing” implícito en `ProcessingStatus` para deletes. El tracking de delete queda en `LifeCycleStatus`. - -### Actividades compartidas / reutilizables - -Ya existe (y se mantiene): -- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip. - -Se añaden actividades de DB por aggregate: -- `MarkTrackAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste. -- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones). -- `MarkPlayListAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste. -- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones). - -### Estructura del RoutingSlip para Track - -``` -DeleteTrackEvent - └─► DeleteTrackConsumer (queue: `delete-track`) - └─► DeleteTrackRoutingSlipBuilder.BuildAsync(...) - ├─ MarkTrackAsRemovingActivity (endpoint: `mark-track-as-removing`) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original picture - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small (si IsPicturesProcessed) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium (si IsPicturesProcessed) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large (si IsPicturesProcessed) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original audio - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — processed audio (si IsAudioProcessed) - └─ DeleteTrackFromDbActivity (endpoint: `delete-track-from-db`) -``` - -> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue). - -### Estructura del RoutingSlip para PlayList - -``` -DeletePlayListEvent - └─► DeletePlayListConsumer (queue: `delete-playList`) - └─► DeletePlayListRoutingSlipBuilder.BuildAsync(...) - ├─ MarkPlayListAsRemovingActivity (endpoint: `mark-playList-as-removing`) - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original (uploads/{userId}/...) ← solo si no es preset - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small ← solo si no es preset - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium ← solo si no es preset - ├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large ← solo si no es preset - └─ DeletePlayListFromDbActivity (endpoint: `delete-playList-from-db`) -``` - -### Handler de comando (ambos aggregates) - -```csharp -// DeletePlayListCommandHandler — ANTES -await PublishRemoveFileEventsAsync(...); // 4 eventos sueltos -database.PlayLists.Remove(playlist); -await database.SaveChangesAsync(); - -// DESPUÉS -playlist.LifeCycleStatus = LifeCycleStatus.Removing; -await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId)); -await database.SaveChangesAsync(); -// El workflow se encarga de borrar ficheros y eliminar en DB. -``` - -```csharp -// DeleteTrackCommandHandler — ANTES -// (ya publicaba DeleteTrackEvent, se mantiene igual) - -// DESPUÉS -track.LifeCycleStatus = LifeCycleStatus.Removing; -await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId)); -await database.SaveChangesAsync(); - -// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica. -``` - ---- - -## Reglas de negocio a preservar - -- Ownership se valida en el handler del comando (igual que ahora), antes de publicar el evento. -- PlayList: solo borrar original/sizes si no son presets. -- PlayList: el original está bajo `uploads/{userId}/...` (cambio reciente). -- Track: respetar estados de procesamiento — si audio está `Pending/Processing`, el handler ya bloquea el delete antes de llegar al workflow. -- EF cascade elimina `PlayListHasTrack` al borrar la PlayList → no hace falta activity explícita para ello. - ---- - -## Idempotencia / reintentos - -- RoutingSlip + reintentos de endpoint ya configurados en MassTransit. -- Operaciones S3: idempotentes por diseño. Si el key no existe (`NoSuchKey`/`NotFound`) → log `Information` y `Completed()` (no throw). -- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora). -- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip. - ---- - -## Plan de implementación (paso a paso) - -### 1. Inventario -- Confirmar todos los usos de `RemoveFileEvent` / `RemoveFileConsumer` en la solución. -- Si no tienen otros usos fuera de playlist delete → marcarlos para eliminación en el paso 7. - -### 2. Refactorizar Track delete ← **nuevo** -- Crear `MarkTrackAsRemovingActivity` (execute-activity): carga Track, setea estado `Removing`, persiste. -- Crear `DeleteTrackFromDbActivity` (execute-activity): carga Track, llama `Remove()`, persiste. -- Actualizar `DeleteTrackRoutingSlipBuilder`: - - Carga el Track para calcular el plan (keys condicionales según `IsPicturesProcessed`, `IsAudioProcessed`). - - Encadena: `MarkTrackAsRemoving` → N × `RemoveFileFromBucket` → `DeleteTrackFromDb`. -- Eliminar `DeleteTrackActivity` monolítica (ya no se usa). -- Ajustar registration DI de MassTransit (quitar activity vieja, registrar las nuevas). - -### 3. Crear PlayList delete: evento + consumer + builder -- Añadir `DeletePlayListEvent(PlayListId, UserId)` en `Application/Events`. -- Crear `DeletePlayListConsumer` en Infrastructure: recibe el evento y ejecuta el builder. -- Crear `DeletePlayListRoutingSlipBuilder`: - - Carga la PlayList para calcular el plan (keys condicionales por presets, ruta de original). - - Encadena: `MarkPlayListAsRemoving` → N × `RemoveFileFromBucket` → `DeletePlayListFromDb`. - -### 4. Crear actividades de PlayList -- `MarkPlayListAsRemovingActivity` (execute-activity): carga PlayList, setea `Removing`, persiste. -- `DeletePlayListFromDbActivity` (execute-activity): carga PlayList, llama `Remove()`, persiste (cascade). - -### 5. Actualizar `RemoveFileFromBucketActivity` para NotFound -- Si el SDK lanza excepción por key inexistente → capturar, log `Information("Key {Key} not found, skipping")`, retornar `Completed()`. -- Si es otro error → log `Error` + throw (reintento normal). - -### 6. Cambiar `DeletePlayListCommandHandler` -- Reemplazar `PublishRemoveFileEventsAsync` por `eventBus.PublishAsync(new DeletePlayListEvent(...))`. -- No publicar más `RemoveFileEvent` para playlist delete. - -### 7. Ajustar DI / registration MassTransit -- Registrar: `DeletePlayListConsumer`, `MarkPlayListAsRemovingActivity`, `DeletePlayListFromDbActivity`. -- Registrar: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`. -- Desregistrar: `DeleteTrackActivity` (monolítica), y si procede `RemoveFileConsumer`. - -### 8. Eliminar código muerto -- `DeleteTrackActivity` monolítica. -- `RemoveFileEvent` y `RemoveFileConsumer` (si no tienen otros usos). -- Endpoints y registrations asociados. - -### 9. Build y verificación -- `dotnet build -c Release` sin warnings de referencias muertas. -- Smoke test manual: borrar un Track y una PlayList, verificar logs de cada activity en orden. \ No newline at end of file diff --git a/docs/presigned-upload-hardening.md b/docs/presigned-upload-hardening.md deleted file mode 100644 index f9159b8..0000000 --- a/docs/presigned-upload-hardening.md +++ /dev/null @@ -1,263 +0,0 @@ -# Hardening: abuso de pre-signed uploads (Opción B) - -## Contexto -Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando `pictureName/audioName`. - -Esto mejora latencia y descarga el host/servicio, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”). - -## Qué pasa hoy si el usuario spamea -- Se generan muchas URLs pre-firmadas (costo bajo pero no cero). -- El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB. -- El impacto principal es: - - coste/uso de S3 (storage + requests) - - posible saturación del componente de generación de URLs - -## Objetivo -Reducir abuso y coste, manteniendo: -- UX simple -- backend sin streams -- workflows asíncronos + MassTransit Outbox - -## Qué significa “sólido” aquí -Como el upload es directo a S3 (o compatible), el backend no puede garantizar “single-use” del upload en el lado del storage mientras una URL sea válida. Por tanto, el objetivo realista es: - -- Asegurar que **cualquier abuso solo pueda impactar un prefijo temporal** barato de limpiar. -- Poner límites duros (cuotas) en DB para que el coste tenga un techo. -- No aceptar referencias a objetos no válidos (verificación antes de consumir). -- Evitar pérdida de datos en reemplazos (no borrar la versión previa hasta éxito). - -## Principio de configuración (no hardcode) -Todas las rutas/prefijos/TTLs/límites deben ser **configurables**. - -Regla: -- En `Application`/`Infrastructure` no debe haber strings hardcodeadas de keys S3 (p.ej. `temp/`, `uploads/`, `PlayLists/`, `Tracks/`). -- Las rutas deben centralizarse en opciones/config (p.ej. `StorageRoutes`, `PlayListRoutes`, `TrackRoutes`, o similar), de forma que cambiar la convención de keys no requiera tocar lógica. - -Los ejemplos de este documento son **convenciones sugeridas** y deben leerse como plantillas parametrizadas. - -## Invariantes de seguridad (reglas que no se rompen) -- El cliente solo puede escribir en rutas temporales bajo `{TempRootPrefix}/...` (o el prefijo temporal equivalente). La ruta final **nunca** recibe credenciales pre-firmadas. -- Cada intent emite un `Key` **nuevo** (GUID). No hay keys “estables” para reusar. -- Una entidad solo apunta a un `pictureName/audioName` si su intent asociado se valida y se consume. -- Reemplazos: el borrado de la versión anterior ocurre **al final** del workflow, tras actualizar DB. - -## Prioridad (coste alto) -Si el coste es el principal riesgo, el orden recomendado es: -1) **Cuotas duras en DB** (intents activos + bytes activos) + job de expiración. -2) Prefijo temporal + cleanup por prefijo (lifecycle si existe, y job como fallback). -3) Validación de objeto (HEAD) antes de consumir. -4) (Opcional) Migrar a pre-signed POST si el backend de storage lo soporta bien. - -## Decisiones validadas (no tocaría) -- El orden de prioridad (cuotas → prefijo temporal → HEAD → POST) es el más sensato cuando el **coste** es el riesgo principal. -- La advertencia sobre S3-compatible / SeaweedFS es importante: el diseño debe ser seguro sin depender de features avanzadas. -- El principio de no hardcodear rutas y centralizar prefijos/convenciones en config es coherente con el estilo del repo. -- La semántica de errores debe permanecer separada del mapeo a códigos/respuestas (responsabilidad del host/presentación). -- En reemplazos, no borrar la versión anterior hasta éxito del workflow es clave para resiliencia con Courier. - -## Mitigaciones recomendadas (pueden coexistir) - -### 1) “Upload intents” en DB (control y conciliación) -Persistir una intención de upload cuando se genera un pre-signed URL. - -> Importante: **no usar el nombre original del fichero** como `pictureName/audioName`. La key debe usar un **identificador generado** (p.ej. GUID) para evitar colisiones, inputs “raros” y facilitar cleanup. - -Sugerencia práctica: -- `objectName = {guid}.{ext}` (o directamente `{guid}` si la extensión no aporta valor) -- Guardar `originalFileName` solo como metadata opcional (si se necesita para UI), pero **nunca** como key. - -Modelo sugerido (mínimo): -- `UploadIntent` - - `Id` - - `UserId` - - `Bucket` - - `Key` - - `ContentType` - - `ExpectedSizeBytes` (opcional; si el caller conoce el tamaño) - - `ExpiresAt` - - `Status`: `Issued | Consumed | Expired` - - `CreatedAt` - -Opcional (útil para auditoría/UX): -- `OriginalFileName` -- `Purpose` (`TrackPicture | TrackAudio | PlayListPicture`) - -Reglas: -- Al pedir URL (Request*Upload*): - - insertar intent `Issued` - - imponer límites por usuario (ver “Cuotas”) - - si supera el límite: fallar con un error semántico (p.ej. `UploadIntentLimitExceeded`) -- Al crear/actualizar entidad (Create*/Update* V2): - - validar que el `pictureName/audioName` recibido corresponde a un intent `Issued` del mismo `UserId` y no expirado - - validar que el objeto existe en S3 (HEAD) y que tamaño / `Content-Type` cumplen la policy del backend - - marcar intent como `Consumed` - -#### Cuotas (anti-cost) 🔴 Alta -Además del conteo de intents, imponer **cuotas por bytes** para que el coste tenga techo. - -Recomendación mínima: -- `MaxActiveUploadIntentsPerUser` (número) -- `MaxActiveUploadBytesPerUser` (bytes) — suma de objetos temporales “en vuelo” por usuario - -Cómo aproximarlo sin streams: -- Guardar `ExpectedSizeBytes` en el intent (si está disponible). -- Si no se dispone del tamaño, usar un valor conservador por `Purpose` (p.ej. imagen <= X, audio <= Y) y validarlo en HEAD antes de consumir. - -> Nota: si el caller puede mentir sobre tamaño, no confíes ciegamente en `ExpectedSizeBytes`; úsalo como guardrail y valida al consumir. - -#### Semántica de errores (intent inválido) 🟡 Media -Definir errores semánticos y dejar el mapeo al “caller” (host/presentación): -- `UploadIntentLimitExceeded` -- `UploadIntentNotFoundOrNotAccessible` (si se quiere no filtrar existencia) -- `UploadIntentAlreadyConsumed` -- `UploadIntentExpired` -- `UploadObjectNotFound` -- `UploadObjectPolicyViolation` (tamaño, `Content-Type`, extensión, etc.) - -#### Nota sobre race conditions en el límite 🟡 Media -El check “`count(Issued) < Max`” es vulnerable a concurrencia (dos requests paralelas pueden pasar el check y crear 2 intents extra). - -Mitigaciones (de menor a mayor robustez): -- Ejecutar **check + insert** en la **misma transacción** con aislamiento fuerte (p.ej. `SERIALIZABLE`). -- Usar un mecanismo de lock por usuario (p.ej. advisory lock en PostgreSQL) durante la emisión. -- Modelar el límite con una tabla/contador por usuario y aplicar **update condicional** (estilo “compare-and-swap”). - -#### Job de expiración de intents 🔴 Alta -Aunque `ExpiresAt` esté en DB, hace falta un mecanismo que marque y limpie intents caducados: -- Un job periódico (worker/cron) que: - - marque intents `Issued` con `ExpiresAt < now` como `Expired` - - opcionalmente encole el borrado del objeto S3 (si existe) - -Si se aplica lifecycle en S3 (ver sección 2), el job puede ser “solo DB” (marcar `Expired`) y dejar el borrado real a S3. - -Adicionalmente, este job es un buen sitio para: -- borrar intents `Expired` antiguos (retención de auditoría configurable) -- emitir métricas/contadores (intents expirados por usuario) - -Ventajas: -- Previene spam con reglas duras y auditables. -- Permite cleanup de huérfanos con certeza. - -Coste: -- entidad + migración + lógica de validación. - -### 2) Limitar el scope del pre-signed URL (reducción de daño) -Aplicar restricciones conservadoras: -- Reducir TTL: - - `ExpiresInSeconds` más bajo (p.ej. 60–120s en vez de 600s) -- Validar/limitar: - - `ContentType` permitido - - extensiones permitidas (`FileType`) -- Usar keys temporales por usuario: - - Opción recomendada (facilita lifecycle por prefijo): - - `{TempRootPrefix}/{userId}/playlists/{guid}.{ext}` - - `{TempRootPrefix}/{userId}/tracks/{guid}.{ext}` - - Alternativa (si se quiere mantener todo bajo `uploads/{userId}`): - - `{UploadsRootPrefix}/{userId}/PlayLists/TempUploads/{guid}.{ext}` - - `{UploadsRootPrefix}/{userId}/Tracks/TempUploads/{guid}.{ext}` - -Luego, en el workflow/worker: -- mover/copiar desde la ruta temporal (p.ej. `temp/...` o `.../TempUploads/...`) a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`) -- borrar el objeto temporal - -Ventajas: -- Cleanup masivo sencillo (por prefijo `{TempRootPrefix}/`) -- Reduce el impacto de huérfanos. - -#### S3 Lifecycle Rule en `TempUploads` 🔴 Alta -Configurar una **Lifecycle Rule** en el bucket para auto-expirar objetos bajo el prefijo de temporales, por ejemplo: -- Prefijo: `{TempRootPrefix}/` -- Expiración: 1–7 días (según tolerancia a reintentos) - -Nota: las reglas de lifecycle por **prefijo** no soportan comodines en medio (p.ej. `uploads/*/...`). -Si se usa la alternativa `uploads/{userId}/.../TempUploads/...`, para filtrar correctamente se recomienda: -- cambiar la convención de key para que el prefijo sea común (p.ej. `{TempRootPrefix}/...`), o -- usar **tags** en los objetos (p.ej. `temporary=true`) y filtrar la lifecycle rule por tag (si se puede firmar el tagging en el upload). - -Esto evita que el coste de huérfanos crezca indefinidamente incluso si el job de cleanup falla. - -#### Cleanup backend (fallback) 🔴 Alta -No dependas solo de lifecycle del storage (en S3 compatibles puede variar). Añadir un job de cleanup que: -- liste por prefijo temporal `{TempRootPrefix}/` -- borre objetos con edad > `TempUploadsRetentionDays` - -Este job debe ser: -- incremental (paginación) -- tolerante a errores (reintentos) -- con límites de throughput (para no auto-generar costes por borrar en masa) - -#### Restricción de `Content-Type` en S3 🟡 Media -Si el objetivo es enforcement “duro” a nivel de S3 (no solo validación posterior), **pre-signed POST** suele ser más robusto que PUT. - -**pre-signed PUT** (lo actual en este repo): -- Permite fijar `Content-Type` si se pre-firma incluyendo ese header. -- Es más limitado para imponer condiciones ricas (tamaño, tags, etc.). -- Recomendación: combinar con validación posterior (HEAD) antes de consumir el intent. - -**pre-signed POST** (recomendado si buscas más control): -- Permite policy conditions como: - - `content-length-range` (tamaño máximo) - - `eq`/`starts-with` para `Content-Type` - - key/prefijo (el cliente no elige keys fuera del scope) - - headers requeridos (p.ej. SSE) - - opcional: tagging (p.ej. `temporary=true`) para lifecycle por tag - -Trade-off: el cliente sube con `multipart/form-data` (POST), pero el backend sigue sin manejar streams. - -#### Nota S3-compatible / SeaweedFS 🟡 Media -No todos los backends S3 compatibles implementan al 100%: -- policy conditions de POST -- tagging en upload -- respuestas/errores idénticos a AWS - -Recomendación: -- Diseñar el sistema para ser seguro **sin depender** de condiciones avanzadas (keys únicas + prefijo temporal + cuotas + verificación HEAD). -- Tratar POST/condiciones como una mejora opcional, tras validar compatibilidad del backend. - -#### Reemplazos: no borrar originales antiguos hasta éxito ✅ -Al actualizar una imagen/audio existente: -- Subir el nuevo binario a la ruta temporal (p.ej. `{TempRootPrefix}/.../{newGuid}`). -- Procesar (resize/transcode) y subir a rutas finales con nombres nuevos. -- Actualizar la entidad en DB para apuntar a los nuevos `*PictureName/*AudioFolderName`. -- **Solo al final** borrar los objetos antiguos (original + sizes / audio procesado), cuando el workflow haya completado. - -Esto evita perder la versión anterior si el worker falla a mitad (punto crítico para resiliencia). - -## Validación del objeto antes de consumir (HEAD) 🔴 Alta -Antes de consumir un intent y actualizar DB: -- `HEAD Bucket/Key` -- validar `Content-Type` esperado (según `Purpose`) -- validar tamaño máximo (`MaxUploadBytes` o por propósito) -- (opcional) validar extensión inferida de `Key` si se usa - -Si falla la validación: -- no consumir el intent -- marcarlo como violación o dejarlo expirar -- el cleanup se encarga del objeto - -## Orden de implementación sugerido -1) Upload intents en DB (límite de activos + validación al crear/actualizar) -2) Reducir TTL y mover a rutas temporales + S3 lifecycle + cleanup -3) Si se requiere enforcement fuerte: migrar a pre-signed POST + policy conditions - -## Parámetros recomendados (config) -- `UploadUrlExpiresInSeconds` (default: 120) -- `MaxActiveUploadIntentsPerUser` (default: 5) -- `MaxActiveUploadBytesPerUser` (default sugerido: depende de coste; p.ej. 200MB) -- `AllowedPictureExtensions` / `AllowedAudioExtensions` -- `AllowedContentTypes` (lista blanca) -- `TempUploadsRetentionDays` (default: 3) -- `PresignedUploadMethod` (default sugerido: `POST` si se quiere enforcement fuerte; `PUT` si se prioriza simplicidad) -- `MaxUploadBytes` (límite máximo de tamaño por upload) - -Jobs (config): -- `UploadIntentsExpirationJobIntervalSeconds` -- `TempUploadsCleanupJobIntervalSeconds` -- `ExpiredIntentsRetentionDays` - -Rutas/prefijos (ejemplos): -- `TempRootPrefix` (default sugerido: `temp`) -- `UploadsRootPrefix` (default sugerido: `uploads`) - -## Notas -- Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos. diff --git a/docs/tech-stack.md b/docs/tech-stack.md deleted file mode 100644 index 9243f62..0000000 --- a/docs/tech-stack.md +++ /dev/null @@ -1,186 +0,0 @@ -# Tech stack y arquitectura (MusifyBackend) - -Este documento describe **qué tecnologías usa este repo** y **cómo encajan** (capas, workers, mensajería, storage, observabilidad). Está pensado para alguien que llega de cero o para una IA que necesita contexto. - -## Foto rápida -- Runtime: **.NET 10** (C#) -- Estilo: **Clean Architecture** (capas `Domain` / `Application` / `Infrastructure`) -- Casos de uso: **CQRS** con **DispatchR** (similar a MediatR) -- Persistencia: **Entity Framework Core** + **PostgreSQL** (provider `Npgsql`) -- Workers / mensajería: **MassTransit** + **RabbitMQ** -- Orquestación de workflows: **MassTransit Courier / RoutingSlip** (Activities) -- Storage de objetos: **SeaweedFS** expuesto como **S3-compatible** (vía AWS SDK `IAmazonS3`) -- Observabilidad: **OpenTelemetry** (traces + metrics, export OTLP) -- Procesado multimedia: - - Imágenes: **SixLabors.ImageSharp** (resize, WebP) - - Audio: **ffmpeg** (transcode a DASH) - -## Estructura del repo (capas) - -### `Domain/` (núcleo de negocio) -- Entidades puras (sin dependencias de infraestructura). -- Ejemplos: `Track`, `PlayList`, `Upload`, etc. -- Estados de negocio relevantes: - - `ProcessingStatus` (procesado en curso/finalizado, etc.). - - `LifeCycleStatus` (p.ej. `Active`, `Removing`) para deletes asíncronos. - -### `Application/` (casos de uso + contratos) -- Contiene comandos/queries/handlers por feature. -- CQRS: - - `Commands/` y `Queries/` por feature (`PlayLists`, `Tracks`, `Users`, ...). - - `Handlers/` ejecutan la lógica de aplicación (coordinar DB + eventos), pero **no implementan** detalles de infraestructura. -- Contratos con infraestructura: `Application/Abstractions/Infrastructure/*` - - `IDatabase` (abstracto de EF DbContext) - - `IEventBus` (publicación de eventos) - - `IStorageService`, `IPictureService`, `IAudioTranscoderService`, etc. -- Eventos de integración: `Application/Events/*` - - Se publican desde `Application` y se consumen en `Infrastructure`. - -### `Infrastructure/` (implementaciones + integraciones) -- Implementa las abstracciones de `Application`. -- Contiene: - - EF Core DbContext y migraciones - - MassTransit (consumers, activities, routing slips, outbox) - - Integración S3 (SeaweedFS) mediante AWS SDK - - Observabilidad OpenTelemetry - - Servicios de procesado (ImageSharp/ffmpeg) - -## CQRS con DispatchR -- Los “use cases” se modelan como: - - **Commands**: mutaciones (crear/actualizar/borrar). - - **Queries**: lecturas. -- Los handlers viven en `Application/*/Handlers/` y dependen de: - - `IDatabase` para persistencia (sin acoplarse al DbContext concreto). - - `IEventBus` para emitir eventos cuando hay trabajo asíncrono. - -## Persistencia (EF Core + PostgreSQL) -- DbContext: `Infrastructure/Persistence/Database.cs` - - Usa `UseNpgsql(connectionString)`. - - Incluye tablas de MassTransit Outbox/Inbox (ver sección de mensajería). -- DI: `Infrastructure/Persistence/DatabaseDependencyInjection.cs` -- Config: `Infrastructure/Configuration/DatabaseConfiguration.cs` (sección `Database`) - - `ConnectionString` - -## Mensajería y workers (MassTransit + RabbitMQ) - -### Qué se usa -- Broker: RabbitMQ (`MassTransit.RabbitMQ`). -- MassTransit está configurado en dos modos: - 1) **Cliente** (publica eventos): `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` → `AddMassTransitClient()` - 2) **Workers** (consumen y ejecutan activities): `AddMassTransitConsumers()` - -### Outbox / Inbox (robustez) -- Se usa **EntityFramework Outbox** (`AddEntityFrameworkOutbox()`) con: - - `UsePostgres()` - - `UseBusOutbox()` - - `QueryDelay = 1s` - - `DuplicateDetectionWindow = 30m` -- En el DbContext se registran entidades de MassTransit: - - `AddInboxStateEntity()` - - `AddOutboxMessageEntity()` - - `AddOutboxStateEntity()` - -Idea clave: -- La outbox asegura que, tras confirmar la transacción de DB, los mensajes/eventos pendientes se publican de forma fiable. - -### Courier / RoutingSlip (workflows asíncronos) -Para operaciones multi-paso (p.ej. imagen/audio y deletes), se usa **MassTransit Courier**: -- Un “RoutingSlip” es una lista de pasos. -- Cada paso es una **Activity** (con args + log opcional). -- Existen dos tipos que aparecen en este repo: - - `ExecuteActivity` (endpoint `_execute`), típicamente acciones atómicas. - - `Activity` (con compensación/log), típico para pasos de workflow. - -Dónde mirar: -- Builders: `Infrastructure/MassTransit/RoutingSlip/Builders/*` -- Activities: `Infrastructure/MassTransit/Activities/*` -- Consumers (punto de entrada del workflow): `Infrastructure/MassTransit/Consumers/*` -- Registro de endpoints: `Infrastructure/MassTransit/DependencyInjection/*` - -Ejemplos de workflows presentes: -- Procesado de imagen (resize + upload + update DB) -- Procesado de audio (transcode DASH + upload + update DB) -- Deletes asíncronos (marcar “removing”, borrar objetos, borrar filas) - -### Config de MassTransit -- `Infrastructure/Configuration/MassTransitConfiguration.cs` (sección `MassTransit`) - - `Address` - - `Username` - - `Password` - -## Storage de objetos (SeaweedFS como S3-compatible) - -### Qué se usa -- SDK: `Amazon.S3` (`IAmazonS3`). -- Implementación: `Infrastructure/Services/StorageService.cs` (usa `IAmazonS3`). -- DI: `Infrastructure/Services/ServicesDependencyInjection.cs` → `AddStorageService()` - -### Config -- `Infrastructure/Configuration/InfrastructureStorageConfiguration.cs` (sección `InfrastructureStorage`) - - `Address` (ServiceURL) - - `AccessKey` / `SecretAccessKey` - - `ForcePathStyle` (útil en S3-compatibles) - - `UseHttp` - -Notas prácticas (S3-compatible): -- Algunas features avanzadas de AWS S3 pueden no comportarse igual (depende de SeaweedFS). Diseñar workflows para que sean correctos con el subconjunto básico: PUT/GET/HEAD/LIST/DELETE y paths bien definidos. - -## Procesado multimedia - -### Imágenes -- Librería: `SixLabors.ImageSharp` -- Servicio: `Infrastructure/Services/PictureService.cs` -- Contrato: `Application/Abstractions/Infrastructure/IPictureService.cs` -- Patrón: workers descargan/transforman/suben; la app coordina por eventos. - -### Audio -- Herramienta: `ffmpeg` (binario externo) -- Config: `Infrastructure/Configuration/AudioTranscoderConfiguration.cs` (sección `AudioTranscoder`) - - Timeout, codec/bitrate/channels/sample-rate, segmentación DASH, etc. -- Servicio: `Infrastructure/Services/AudioTranscoderService.cs` -- Contrato: `Application/Abstractions/Infrastructure/IAudioTranscoderService.cs` - -### Directorio temporal (workers) -- Config: `Infrastructure/Configuration/WorkerConfiguration.cs` (sección `Workers`) - - `Routes.TemporaryFilesDirectory` - -## Observabilidad (OpenTelemetry) -- DI: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs` -- Config: `Infrastructure/Configuration/OpenTelemetryConfiguration.cs` (sección `OpenTelemetry`) - - `OtlpEndpoint` - -Qué instrumenta: -- Tracing: - - `MassTransit` - - ASP.NET Core - - HttpClient - - EF Core - - Exporter OTLP -- Métricas: - - `MassTransit` - - ASP.NET Core - - HttpClient - - Runtime + Process - - Exporter OTLP - -## Identidad (Keycloak) -- El repo asume integración con **Keycloak** (ver `AGENTS.md` y nombres como `GetUserByKeycloakQuery`). -- La capa `Application` trata el “Keycloak Id” como identificador de usuario en algunos casos de uso. -- La integración concreta (tokens, validación, etc.) puede vivir fuera de estas 3 capas o en otro proyecto/host. - -## Glosario rápido -- **Command / Query**: operación de escritura / lectura. -- **Handler**: clase que ejecuta el caso de uso (CQRS). -- **EventBus**: abstracción para publicar eventos (MassTransit por debajo). -- **Consumer**: “listener” de mensajes en una cola. -- **RoutingSlip**: workflow compuesto por Activities. -- **Activity**: paso del workflow (puede tener log/compensación). -- **Outbox**: patrón para publicar eventos de forma fiable tras commit de DB. - -## “Dónde empiezo a leer” (ruta recomendada) -1) Arquitectura general: `AGENTS.md` -2) Casos de uso: `Application/*/Commands`, `Application/*/Queries` y sus `Handlers/` -3) Persistencia: `Infrastructure/Persistence/Database.cs` + migraciones -4) Mensajería/workflows: `Infrastructure/MassTransit/*` -5) Integraciones: `Infrastructure/Services/*` (S3, imagen, audio) -6) Observabilidad: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs`