Almacenamiento y Media
Tabla de Contenidos
- Arquitectura General
- Buckets de Almacenamiento
- URLs Pre-firmadas (Presigned URLs)
- Tipos de Archivo Permitidos
- Limites de Tamano por Plan
- Proveedores de Almacenamiento
- URLs de CDN
Arquitectura General
El sistema de almacenamiento de Nvito utiliza el protocolo S3 como capa de abstraccion, permitiendo cambiar entre proveedores (MinIO en desarrollo, Cloudflare R2 en produccion) sin modificar el codigo de la aplicacion.
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 | Configuracion de buckets, CDN, limites y MIME types |
file-validation.util | common/utils/file-validation.util.ts | Validacion de archivos por magic bytes |
S3 Client
El servicio inicializa un S3Client del AWS SDK v3 con configuracion 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
nvito-uploads/
└── org_{organizationId}/
└── events/
└── {eventId}/
├── media/
│ ├── {timestamp}_{filename}.jpg # Fotos
│ ├── {timestamp}_{filename}.mp4 # Videos
│ └── {timestamp}_{filename}.mp3 # Audio
└── invitations/
└── {invitationId}/
├── versions/
│ └── v{N}/
│ ├── index.html # HTML compilado
│ ├── styles.css # CSS compilado
│ └── script.js # JS compilado (opcional)
└── live/
├── index.html # Version publicada
├── styles.css
└── script.js
Estructura de Archivos en nvito-templates
nvito-templates/
└── templates/
└── {templateId}/
├── template.html # HTML del template base
└── assets/
└── {timestamp}_{file} # Imagenes preview, fondos, etc.
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.
Generacion de Key
El key se construye con la siguiente estructura para evitar colisiones:
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 despues de 3600 segundos (1 hora) por defecto. El frontend recibe la fecha de expiracion en expiresAt.
Validacion Post-Upload
Despues 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
- Accion en fallo: Si la validacion falla, el archivo se elimina automaticamente del bucket
Tipos de Archivo Permitidos
MIME Types Aceptados
| Categoria | 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 Invitacion (generados por el sistema)
Ademas 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 segun el plan contratado por la organizacion:
| 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 |
Deteccion automatica: Si la variable R2_ENDPOINT esta configurada (apuntando a MinIO), el servicio activa forcePathStyle: true automaticamente, ya que MinIO requiere path-style en lugar de virtual-hosted.
Cloudflare R2 (Produccion)
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 publica | 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
Construccion de URLs Publicas
El servicio construye URLs publicas de forma diferente segun 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 (produccion):
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 publica (/{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 ano (media de usuario)
Cache-Control: public, max-age=3600 # 1 hora (HTML de invitacion)
URLs Privadas
Para archivos en el bucket nvito-private, se generan URLs firmadas de lectura con expiracion configurable:
async getPrivateUrl(key: string, expiresIn: number = 3600): Promise<string>
Estas URLs son temporales y permiten descargar el archivo sin exponer credenciales.
Operaciones del StorageService
Resumen de Metodos
| Metodo | Descripcion |
|---|---|
uploadPublic() | Subir archivo con buffer a bucket publico |
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 invitacion (versionado) |
uploadInvitationCss() | Subir CSS compilado de invitacion |
uploadInvitationJs() | Subir JS compilado de invitacion |
publishInvitationVersion() | Copiar version a directorio live/ (publicar) |
Variables de Entorno
| Variable | Descripcion | 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