--- id: "SAAS-011" title: "Storage" type: "Module" status: "Published" priority: "P1" module: "storage" version: "1.0.0" created_date: "2026-01-07" updated_date: "2026-01-10" --- # SAAS-011: Storage ## Metadata - **Codigo:** SAAS-011 - **Modulo:** Storage - **Prioridad:** P1 - **Estado:** Implementado - **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 ## Implementacion ### Estado de Entregables | Entregable | Estado | Archivo | |------------|--------|---------| | storage.module.ts | Implementado | `apps/backend/src/modules/storage/storage.module.ts` | | storage.controller.ts | Implementado | `apps/backend/src/modules/storage/storage.controller.ts` | | storage.service.ts | Implementado | `apps/backend/src/modules/storage/services/storage.service.ts` | | s3.provider.ts | Implementado | `apps/backend/src/modules/storage/providers/s3.provider.ts` | | DDL storage schema | Implementado | `apps/database/ddl/schemas/storage/` | | Frontend components | Implementado | `apps/frontend/src/components/storage/` | | StoragePage | Implementado | `apps/frontend/src/pages/dashboard/StoragePage.tsx` | ### Archivos Creados **Backend:** ``` apps/backend/src/modules/storage/ ├── storage.module.ts # NestJS module ├── storage.controller.ts # REST endpoints (8 endpoints) ├── index.ts # Module barrel export ├── entities/ │ ├── file.entity.ts # FileEntity with enums │ ├── pending-upload.entity.ts │ ├── storage-usage.entity.ts │ └── index.ts ├── dto/ │ ├── storage.dto.ts # Request/Response DTOs │ └── index.ts ├── providers/ │ ├── s3.provider.ts # S3/R2/MinIO provider │ └── index.ts └── services/ ├── storage.service.ts # Business logic └── index.ts ``` **Database:** ``` apps/database/ddl/schemas/storage/ └── tables/ ├── 01-files.sql # files, pending_uploads tables └── 02-storage-usage.sql # usage table, triggers, functions ``` **Frontend:** ``` apps/frontend/src/ ├── components/storage/ │ ├── FileUpload.tsx # Drag & drop upload component │ ├── FileItem.tsx # File display (grid/list view) │ ├── FileList.tsx # Paginated file list │ ├── StorageUsageCard.tsx # Usage statistics card │ └── index.ts ├── hooks/ │ └── useStorage.ts # React Query hooks ├── pages/dashboard/ │ └── StoragePage.tsx # Main storage page └── services/ └── api.ts # storageApi client (added) ``` ### Tablas de Base de Datos | Tabla | Descripcion | |-------|-------------| | storage.files | Archivos subidos con metadata | | storage.pending_uploads | Uploads pendientes de confirmacion | | storage.usage | Uso de almacenamiento por tenant | ### Funciones de Base de Datos | Funcion | Descripcion | |---------|-------------| | storage.update_usage_on_file_change() | Trigger para actualizar usage | | storage.can_upload() | Valida si tenant puede subir | | storage.get_tenant_stats() | Estadisticas de uso | | storage.cleanup_expired_uploads() | Limpieza de uploads expirados | ## Endpoints API | Metodo | Endpoint | Descripcion | Implementado | |--------|----------|-------------|--------------| | 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 | ✓ | | PATCH | /storage/files/:id | Actualizar metadata | ✓ | | DELETE | /storage/files/:id | Eliminar archivo (soft) | ✓ | | GET | /storage/usage | Uso actual | ✓ | ## Flujo de Upload ``` 1. Cliente solicita upload URL POST /storage/upload-url Body: { filename, mimeType, sizeBytes, folder?, visibility? } 2. Backend valida: - Tipo MIME permitido - Extension no bloqueada - Tamaño dentro de limite - Espacio disponible 3. Backend genera presigned URL (AWS SDK v3) - Expira en 15 minutos - Limite de tamaño 4. Cliente sube directo a S3/R2 PUT [presigned-url] Headers: Content-Type Body: [file binary] 5. Cliente confirma POST /storage/confirm Body: { uploadId, metadata? } 6. Backend: - Verifica archivo existe (HeadObject) - Crea registro en storage.files - Actualiza storage.usage via trigger ``` ## Tipos MIME Permitidos ```typescript const ALLOWED_MIME_TYPES = { images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'], 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', 'text/plain'] }; const BLOCKED_EXTENSIONS = ['.exe', '.bat', '.sh', '.php', '.js', '.cmd', '.com', '.scr']; ``` ## Proveedores Soportados | Proveedor | Uso | Config | |-----------|-----|--------| | AWS S3 | Produccion | Bucket por region | | Cloudflare R2 | Produccion | Sin egress fees | | MinIO | Desarrollo | Docker local | ## 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 | ## Frontend Hooks ```typescript // Listar archivos const { data, isLoading } = useFiles({ page, limit, folder, search }); // Obtener archivo individual const { data: file } = useFile(fileId); // Uso de almacenamiento const { data: usage } = useStorageUsage(); // Subir archivo const uploadMutation = useUploadFile(); await uploadMutation.mutateAsync({ file, folder: 'documents', visibility: 'private', onProgress: (percent) => setProgress(percent) }); // Actualizar archivo const updateMutation = useUpdateFile(); await updateMutation.mutateAsync({ id, data: { folder: 'archive' } }); // Eliminar archivo const deleteMutation = useDeleteFile(); await deleteMutation.mutateAsync(fileId); ``` ## Configuracion ### Variables de Entorno ```bash # Provider: s3 | r2 | minio STORAGE_PROVIDER=s3 STORAGE_BUCKET=my-bucket # AWS/S3 AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... # Para R2/MinIO (opcional) STORAGE_ENDPOINT=https://... ``` ## Criterios de Aceptacion - [x] Upload presigned funciona - [x] Download presigned funciona - [x] Limites se respetan - [ ] Thumbnails se generan (futuro) - [x] Uso se trackea - [x] Archivos se aislan por tenant ## 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 ## Notas de Implementacion 1. **AWS SDK v3**: Se usa `@aws-sdk/client-s3` y `@aws-sdk/s3-request-presigner` 2. **Soft Delete**: Los archivos se marcan como deleted, no se eliminan fisicamente de inmediato 3. **RLS Policies**: Aplicadas para aislamiento por tenant 4. **Triggers**: El uso se actualiza automaticamente via trigger --- **Ultima actualizacion:** 2026-01-10