# SAAS-011: Storage ## Metadata - **Codigo:** SAAS-011 - **Modulo:** Storage - **Prioridad:** P1 - **Estado:** Pendiente - **Fase:** 3 - Features Core ## Descripcion Sistema de almacenamiento de archivos: upload/download con presigned URLs, organizacion por tenant, limites por plan, y soporte multi-provider (S3, R2, MinIO). ## Objetivos 1. Upload seguro con presigned URLs 2. Organizacion por tenant 3. Limites de almacenamiento por plan 4. Soporte multi-provider 5. Procesamiento de imagenes ## Alcance ### Incluido - Upload via presigned URLs - Download via presigned URLs - Organizacion: tenant/tipo/archivo - Tracking de uso por tenant - Limites por plan - Thumbnails automaticos (imagenes) - Tipos MIME permitidos ### Excluido - CDN propio - usar Cloudflare - Streaming de video - Compresion de archivos ## Proveedores Soportados | Proveedor | Uso | Config | |-----------|-----|--------| | AWS S3 | Produccion | Bucket por region | | Cloudflare R2 | Produccion | Sin egress fees | | MinIO | Desarrollo | Docker local | ## Modelo de Datos ### Tablas (schema: storage) **files** - id, tenant_id, uploaded_by - filename, original_name - mime_type, size_bytes - path, bucket - metadata (JSONB) - created_at **storage_usage** - id, tenant_id - total_files, total_bytes - updated_at ## Endpoints API | Metodo | Endpoint | Descripcion | |--------|----------|-------------| | POST | /storage/upload-url | Obtener presigned upload URL | | POST | /storage/confirm | Confirmar upload exitoso | | GET | /storage/files | Listar archivos | | GET | /storage/files/:id | Metadata de archivo | | GET | /storage/files/:id/download | Presigned download URL | | DELETE | /storage/files/:id | Eliminar archivo | | GET | /storage/usage | Uso actual | ## Flujo de Upload ``` 1. Cliente solicita upload URL POST /storage/upload-url Body: { filename, mimeType, size } 2. Backend valida: - Tipo MIME permitido - Tamaño dentro de limite - Espacio disponible 3. Backend genera presigned URL - Expira en 15 minutos - Limite de tamaño 4. Cliente sube directo a S3/R2 PUT [presigned-url] Body: [file binary] 5. Cliente confirma POST /storage/confirm Body: { uploadId } 6. Backend registra archivo - Actualiza storage_usage ``` ## Implementacion ### Servicio ```typescript interface StorageService { getUploadUrl(params: UploadParams): Promise; confirmUpload(uploadId: string): Promise; getDownloadUrl(fileId: string): Promise; deleteFile(fileId: string): Promise; getUsage(tenantId: string): Promise; } interface UploadParams { filename: string; mimeType: string; sizeBytes: number; folder?: string; } interface PresignedUrl { uploadId: string; url: string; fields?: Record; // Para POST form expiresAt: Date; } ``` ### Generacion de URLs ```typescript async getUploadUrl(params: UploadParams): Promise { const uploadId = uuid(); const key = `${this.tenantId}/${params.folder || 'files'}/${uploadId}/${params.filename}`; const command = new PutObjectCommand({ Bucket: this.bucket, Key: key, ContentType: params.mimeType, ContentLength: params.sizeBytes, Metadata: { 'tenant-id': this.tenantId, 'upload-id': uploadId } }); const url = await getSignedUrl(this.s3, command, { expiresIn: 900 }); // Guardar pending upload await this.savePendingUpload(uploadId, params); return { uploadId, url, expiresAt: addMinutes(new Date(), 15) }; } ``` ## Limites por Plan | Plan | Storage | Max Archivo | |------|---------|-------------| | Free | 100 MB | 5 MB | | Starter | 1 GB | 25 MB | | Pro | 10 GB | 100 MB | | Enterprise | Ilimitado | 500 MB | ## Tipos MIME Permitidos ```typescript const ALLOWED_MIME_TYPES = { images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], documents: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], spreadsheets: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], data: ['text/csv', 'application/json'] }; const BLOCKED_EXTENSIONS = ['.exe', '.bat', '.sh', '.php', '.js']; ``` ## Procesamiento de Imagenes ```typescript // Al confirmar upload de imagen if (isImage(file.mimeType)) { await this.imageProcessor.createThumbnails(file, [ { name: 'thumb', width: 150, height: 150 }, { name: 'medium', width: 800, height: 600 }, { name: 'large', width: 1920, height: 1080 } ]); } ``` ## Estructura de Paths ``` bucket/ ├── tenant-uuid-1/ │ ├── avatars/ │ │ └── user-uuid/ │ │ └── avatar.jpg │ ├── documents/ │ │ └── upload-id/ │ │ └── contract.pdf │ └── imports/ │ └── upload-id/ │ └── data.csv └── tenant-uuid-2/ └── ... ``` ## Entregables | Entregable | Estado | Archivo | |------------|--------|---------| | storage.module.ts | Pendiente | `modules/storage/` | | storage.service.ts | Pendiente | `services/` | | s3.provider.ts | Pendiente | `providers/` | | image.processor.ts | Pendiente | `services/` | | DDL storage schema | Pendiente | `ddl/schemas/storage/` | ## Dependencias ### Depende de - SAAS-002 (Tenants) - SAAS-005 (Plans - limites) - AWS S3 / Cloudflare R2 / MinIO ### Bloquea a - Upload de avatares - Adjuntos en modulos - Importacion de datos ## Criterios de Aceptacion - [ ] Upload presigned funciona - [ ] Download presigned funciona - [ ] Limites se respetan - [ ] Thumbnails se generan - [ ] Uso se trackea - [ ] Archivos se aislan por tenant ## Configuracion ```typescript { storage: { provider: 's3', // 's3' | 'r2' | 'minio' bucket: process.env.STORAGE_BUCKET, region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY }, // Para R2 endpoint: process.env.R2_ENDPOINT, // Para MinIO (dev) endpoint: 'http://localhost:9000', forcePathStyle: true } } ``` --- **Ultima actualizacion:** 2026-01-07