334 lines
16 KiB
Markdown
334 lines
16 KiB
Markdown
# 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<Database>()`
|
|
- 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<UploadIntent> 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<UploadIntent>`.
|
|
- `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.
|