Arquitectura General
El sistema de almacenamiento de Nvito utiliza el protocolo S3 como capa de abstracción, permitiendo cambiar entre proveedores (MinIO en desarrollo, Cloudflare R2 en producción) sin modificar el código de la aplicación.
Arquitectura de Storage
Componentes Principales
| Componente | Archivo | Responsabilidad |
|---|---|---|
StorageService | modules/storage/storage.service.ts | Operaciones de almacenamiento (upload, download, delete, presign) |
storage.config.ts | config/storage.config.ts | Configuración de buckets, CDN, limites y MIME types |
file-validation.util | common/utils/file-validation.util.ts | Validación de archivos por magic bytes |
S3 Client
El servicio inicializa un S3Client del AWS SDK v3 con configuración adaptativa:
this.s3Client = new S3Client({
region: 'auto',
endpoint: endpoint || `https://${accountId}.r2.cloudflarestorage.com`,
credentials: { accessKeyId, secretAccessKey },
forcePathStyle: isMinIO, // MinIO requiere path-style, R2 usa virtual-hosted
});
Buckets de Almacenamiento
El sistema organiza los archivos en 4 buckets separados por proposito:
| Bucket | Variable | Proposito | Acceso |
|---|---|---|---|
nvito-uploads | R2_BUCKET_UPLOADS | Media subida por usuarios (fotos, videos, audio) | Publico via CDN |
nvito-assets | R2_BUCKET_ASSETS | Assets del sistema (logos, iconos, fondos) | Publico via CDN |
nvito-private | R2_BUCKET_PRIVATE | Archivos privados (contratos, documentos) | Solo con URL firmada |
nvito-templates | R2_BUCKET_TEMPLATES | HTML compilado de templates e invitaciones | Publico via CDN |
Estructura de Archivos en nvito-uploads
Estructura de nvito-uploads
Estructura de Archivos en nvito-templates
Estructura de nvito-templates
URLs Pre-firmadas (Presigned URLs)
Para subir archivos, el frontend no envia el archivo al API. En su lugar, solicita una URL pre-firmada y sube directamente al almacenamiento, reduciendo la carga en el servidor.
Presigned URLs — Upload Directo
Generación de Key
El key se construye con la siguiente estructura para evitar colisiónes:
org_{organizationId}/events/{eventId}/media/{timestamp}_{sanitizedFilename}
El filename se sanitiza eliminando caracteres especiales: filename.replace(/[^a-zA-Z0-9._-]/g, '_').
Expiracion
Las URLs pre-firmadas expiran después de 3600 segundos (1 hora) por defecto. El frontend recibe la fecha de expiración en expiresAt.
Validación Post-Upload
Después de que el frontend sube el archivo, el API descarga el archivo del bucket y valida:
- Magic bytes: Verifica que el contenido real coincide con el MIME type declarado
- Ejecutables: Rechaza archivos ejecutables detectados por analisis de bytes
- Acción en fallo: Si la validación falla, el archivo se elimina automáticamente del bucket
Tipos de Archivo Permitidos
MIME Types Aceptados
| Categoría | Formato | MIME Type |
|---|---|---|
| Imagenes | JPEG | image/jpeg |
| PNG | image/png | |
| WebP | image/webp | |
| GIF | image/gif | |
| Video | MP4 | video/mp4 |
| WebM | video/webm | |
| Audio | MP3 | audio/mpeg, audio/mp3 |
| WAV | audio/wav | |
| M4A | audio/m4a |
Archivos de Invitación (generados por el sistema)
Además de los archivos subidos por usuarios, el sistema genera y almacena:
| Tipo | Content-Type | Cache-Control |
|---|---|---|
| HTML compilado | text/html | public, max-age=3600 (1 hora) |
| CSS compilado | text/css | public, max-age=31536000 (1 ano) |
| JS compilado | application/javascript | public, max-age=31536000 (1 ano) |
Limites de Tamano por Plan
Los limites de tamano de archivo varian según el plan contratado por la organización:
| Plan | Limite | Bytes |
|---|---|---|
| Free | 5 MB | 5,242,880 |
| Essential | 25 MB | 26,214,400 |
| Plus | 50 MB | 52,428,800 |
| VIP | 100 MB | 104,857,600 |
Variables de Entorno para Limites
MAX_FILE_SIZE_FREE=5242880 # 5 MB
MAX_FILE_SIZE_ESSENTIAL=26214400 # 25 MB
MAX_FILE_SIZE_PLUS=52428800 # 50 MB
MAX_FILE_SIZE_VIP=104857600 # 100 MB
Estos valores son configurables por variable de entorno, permitiendo ajustarlos sin recompilar.
Proveedores de Almacenamiento
MinIO (Desarrollo Local)
MinIO es un servidor de almacenamiento de objetos compatible con S3, ideal para desarrollo local:
| Parametro | Valor |
|---|---|
| Endpoint | http://localhost:9000 |
| Consola Web | http://localhost:9001 |
| Region | auto |
| Path Style | true (forcePathStyle) |
| Access Key | Configurado en .env |
| Secret Key | Configurado en .env |
Detección automática: Si la variable R2_ENDPOINT está configurada (apuntando a MinIO), el servicio activa forcePathStyle: true automáticamente, ya que MinIO requiere path-style en lugar de virtual-hosted.
Cloudflare R2 (Producción)
Cloudflare R2 es el proveedor de almacenamiento para ambientes remotos:
| Parametro | Valor |
|---|---|
| Endpoint | https://{R2_ACCOUNT_ID}.r2.cloudflarestorage.com |
| Region | auto |
| Path Style | false (virtual-hosted) |
| Account ID | Variable R2_ACCOUNT_ID |
| Access Key | Variable R2_ACCESS_KEY_ID |
| Secret Key | Variable R2_SECRET_ACCESS_KEY |
Tabla Comparativa
| Caracteristica | MinIO (Local) | Cloudflare R2 (Prod) |
|---|---|---|
| Endpoint | http://localhost:9000 | https://{id}.r2.cloudflarestorage.com |
| Path Style | Si (forcePathStyle: true) | No (virtual-hosted) |
| URL pública | http://localhost:9000/{bucket}/{key} | https://cdn.nvito.mx/{key} |
| Costo | Gratis | Pay-per-use (egress gratis) |
| Disponibilidad | Solo local | Global |
| Consola | MinIO Console (:9001) | Cloudflare Dashboard |
URLs de CDN
Construcción de URLs Publicas
El servicio construye URLs públicas de forma diferente según el proveedor:
MinIO (local):
http://localhost:9000/{bucket}/{key}
Ejemplo: http://localhost:9000/nvito-uploads/org_abc123/events/evt1/media/1707123456_foto.jpg
Cloudflare R2 (producción):
https://cdn.nvito.mx/{key}
Ejemplo: https://cdn.nvito.mx/org_abc123/events/evt1/media/1707123456_foto.jpg
Diferencia Clave
En MinIO, el nombre del bucket se incluye en la URL pública (/{bucket}/{key}). En R2, el bucket no aparece en la URL porque se configura a nivel de dominio personalizado (/{key}).
Cache-Control
Los archivos subidos por usuarios se sirven con cache agresivo:
Cache-Control: public, max-age=31536000 # 1 año (media de usuario)
Cache-Control: public, max-age=3600 # 1 hora (HTML de invitación)
URLs Privadas
Para archivos en el bucket nvito-private, se generan URLs firmadas de lectura con expiración configurable:
async getPrivateUrl(key: string, expiresIn: number = 3600): Promise<string>
Estás URLs son temporales y permiten descargar el archivo sin exponer credenciales.
Operaciones del StorageService
Resumen de Metodos
| Metodo | Descripción |
|---|---|
uploadPublic() | Subir archivo con buffer a bucket público |
getUploadUrl() | Generar URL pre-firmada para upload directo |
exists() | Verificar si un archivo existe |
getObject() | Descargar un archivo como Buffer |
delete() | Eliminar un archivo |
deleteEventFiles() | Eliminar todos los archivos de un evento |
getPrivateUrl() | Generar URL firmada para contenido privado |
validateUploadedFileContent() | Validar archivo subido por magic bytes |
uploadTemplateHtml() | Subir HTML de template al bucket de templates |
uploadTemplateAsset() | Subir asset de template (preview, fondo) |
deleteTemplateFiles() | Eliminar archivos de un template |
uploadInvitationHtml() | Subir HTML compilado de invitación (versionado) |
uploadInvitationCss() | Subir CSS compilado de invitación |
uploadInvitationJs() | Subir JS compilado de invitación |
publishInvitationVersión() | Copiar versión a directorio live/ (publicar) |
Variables de Entorno
| Variable | Descripción | Default |
|---|---|---|
R2_ACCOUNT_ID | Account ID de Cloudflare R2 | - |
R2_ACCESS_KEY_ID | Access Key del proveedor S3 | (requerido) |
R2_SECRET_ACCESS_KEY | Secret Key del proveedor S3 | (requerido) |
R2_ENDPOINT | Endpoint custom (MinIO: http://localhost:9000) | - (usa R2) |
R2_BUCKET_UPLOADS | Nombre del bucket de uploads | nvito-uploads |
R2_BUCKET_ASSETS | Nombre del bucket de assets | nvito-assets |
R2_BUCKET_PRIVATE | Nombre del bucket privado | nvito-private |
R2_BUCKET_TEMPLATES | Nombre del bucket de templates | nvito-templates |
CDN_URL | URL base del CDN | https://cdn.nvito.mx |
CDN_ALLOWED_ORIGINS | Origenes permitidos (separados por coma) | - |
MAX_FILE_SIZE_FREE | Limite de archivo para plan Free | 5242880 |
MAX_FILE_SIZE_ESSENTIAL | Limite para plan Essential | 26214400 |
MAX_FILE_SIZE_PLUS | Limite para plan Plus | 52428800 |
MAX_FILE_SIZE_VIP | Limite para plan VIP | 104857600 |
Referencias
- StorageService:
src/modules/storage/storage.service.ts - Storage Config:
src/config/storage.config.ts - File Validation:
src/common/utils/file-validation.util.ts