- 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>
6.1 KiB
6.1 KiB
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
- Upload seguro con presigned URLs
- Organizacion por tenant
- Limites de almacenamiento por plan
- Soporte multi-provider
- 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
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
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
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
// 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
{
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