- Add MetricsPage and useOnboarding hook - Update superadmin controller and service - Add module documentation (docs/01-modulos/) - Add CONTEXT-MAP.yml and Sprint 5 execution report - Update project status and task traces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
6.1 KiB
Markdown
264 lines
6.1 KiB
Markdown
# 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<PresignedUrl>;
|
|
confirmUpload(uploadId: string): Promise<FileRecord>;
|
|
getDownloadUrl(fileId: string): Promise<string>;
|
|
deleteFile(fileId: string): Promise<void>;
|
|
getUsage(tenantId: string): Promise<StorageUsage>;
|
|
}
|
|
|
|
interface UploadParams {
|
|
filename: string;
|
|
mimeType: string;
|
|
sizeBytes: number;
|
|
folder?: string;
|
|
}
|
|
|
|
interface PresignedUrl {
|
|
uploadId: string;
|
|
url: string;
|
|
fields?: Record<string, string>; // Para POST form
|
|
expiresAt: Date;
|
|
}
|
|
```
|
|
|
|
### Generacion de URLs
|
|
```typescript
|
|
async getUploadUrl(params: UploadParams): Promise<PresignedUrl> {
|
|
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
|