miinventario-v2/docs/02-integraciones/INT-005-s3-storage.md
rckrdmrd 1a53b5c4d3 [MIINVENTARIO] feat: Initial commit - Sistema de inventario con análisis de video IA
- Backend NestJS con módulos de autenticación, inventario, créditos
- Frontend React con dashboard y componentes UI
- Base de datos PostgreSQL con migraciones
- Tests E2E configurados
- Configuración de Docker y deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 02:25:48 -06:00

7.1 KiB

INT-005: Integracion S3/MinIO


id: INT-005 type: Integration status: Pendiente version: "1.0.0" created_date: 2026-01-10 updated_date: 2026-01-10 simco_version: "4.0.0"

Metadata

Campo Valor
ID INT-005
Servicio AWS S3 / MinIO
Proposito Almacenamiento de videos y artefactos
Criticidad P0
Estado Pendiente

1. Descripcion

Integracion con almacenamiento compatible con S3 para guardar videos de inventario, frames extraidos, y otros artefactos del procesamiento.


2. Informacion del Servicio

Campo Valor
Desarrollo MinIO (local)
Produccion AWS S3 o DigitalOcean Spaces
SDK @aws-sdk/client-s3, @aws-sdk/s3-request-presigner

3. Configuracion

Variables de Entorno

# Desarrollo (MinIO)
S3_ENDPOINT=http://localhost:9002
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=miinventario
S3_REGION=us-east-1

# Produccion (AWS S3)
S3_ENDPOINT=https://s3.amazonaws.com
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
S3_BUCKET=miinventario-prod
S3_REGION=us-east-1

Instalacion

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

4. Estructura de Buckets

miinventario/
├── videos/
│   └── {userId}/{sessionId}/
│       └── video.mp4
├── frames/
│   └── {sessionId}/
│       ├── frame_001.jpg
│       ├── frame_002.jpg
│       └── ...
├── thumbnails/
│   └── {sessionId}/
│       └── thumb.jpg
├── evidence/
│   └── {sessionId}/
│       └── closeup_{itemId}.jpg
└── products/
    └── {productId}/
        └── image.jpg

5. Implementacion Backend

Servicio S3

import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

@Injectable()
export class S3Service {
  private client: S3Client;
  private bucket: string;

  constructor() {
    this.client = new S3Client({
      endpoint: process.env.S3_ENDPOINT,
      region: process.env.S3_REGION,
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY,
        secretAccessKey: process.env.S3_SECRET_KEY,
      },
      forcePathStyle: true, // Necesario para MinIO
    });
    this.bucket = process.env.S3_BUCKET;
  }

  async upload(key: string, body: Buffer, contentType: string) {
    await this.client.send(new PutObjectCommand({
      Bucket: this.bucket,
      Key: key,
      Body: body,
      ContentType: contentType,
    }));

    return `${process.env.S3_ENDPOINT}/${this.bucket}/${key}`;
  }

  async getSignedUploadUrl(key: string, contentType: string, expiresIn = 3600) {
    const command = new PutObjectCommand({
      Bucket: this.bucket,
      Key: key,
      ContentType: contentType,
    });

    return getSignedUrl(this.client, command, { expiresIn });
  }

  async getSignedDownloadUrl(key: string, expiresIn = 3600) {
    const command = new GetObjectCommand({
      Bucket: this.bucket,
      Key: key,
    });

    return getSignedUrl(this.client, command, { expiresIn });
  }

  async delete(key: string) {
    await this.client.send(new DeleteObjectCommand({
      Bucket: this.bucket,
      Key: key,
    }));
  }
}

Upload Multipart

import { CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand } from '@aws-sdk/client-s3';

async initMultipartUpload(key: string) {
  const { UploadId } = await this.client.send(
    new CreateMultipartUploadCommand({
      Bucket: this.bucket,
      Key: key,
    })
  );
  return UploadId;
}

async uploadPart(key: string, uploadId: string, partNumber: number, body: Buffer) {
  const { ETag } = await this.client.send(
    new UploadPartCommand({
      Bucket: this.bucket,
      Key: key,
      UploadId: uploadId,
      PartNumber: partNumber,
      Body: body,
    })
  );
  return { ETag, PartNumber: partNumber };
}

async completeMultipartUpload(key: string, uploadId: string, parts: any[]) {
  await this.client.send(
    new CompleteMultipartUploadCommand({
      Bucket: this.bucket,
      Key: key,
      UploadId: uploadId,
      MultipartUpload: { Parts: parts },
    })
  );
}

6. Flujo de Upload de Video

┌──────────┐    ┌──────────┐    ┌──────────┐
│  Mobile  │───▶│ Backend  │───▶│   S3     │
└──────────┘    └──────────┘    └──────────┘
     │               │               │
     │ 1. Init       │               │
     │   upload      │               │
     │──────────────▶│               │
     │               │ 2. Create     │
     │               │   multipart   │
     │               │──────────────▶│
     │◀──────────────│ uploadId      │
     │               │               │
     │ 3. Upload     │               │
     │   parts       │               │
     │───────────────────────────────▶
     │   (presigned) │               │
     │               │               │
     │ 4. Complete   │               │
     │──────────────▶│               │
     │               │ 5. Complete   │
     │               │   multipart   │
     │               │──────────────▶│
     │◀──────────────│               │

7. Lifecycle Rules

Configuracion de Expiracion

{
  "Rules": [
    {
      "ID": "DeleteOldVideos",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "videos/"
      },
      "Expiration": {
        "Days": 30
      }
    },
    {
      "ID": "DeleteOldFrames",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "frames/"
      },
      "Expiration": {
        "Days": 7
      }
    }
  ]
}

8. Docker Compose (MinIO)

minio:
  image: minio/minio
  container_name: mii_minio
  ports:
    - "9002:9000"
    - "9003:9001"
  environment:
    MINIO_ROOT_USER: minioadmin
    MINIO_ROOT_PASSWORD: minioadmin
  command: server /data --console-address ":9001"
  volumes:
    - minio_data:/data
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
    interval: 30s
    timeout: 20s
    retries: 3

9. Consideraciones de Seguridad

Aspecto Implementacion
URLs firmadas Expiracion corta (1h)
Bucket privado No acceso publico
CORS Solo dominios permitidos
Encriptacion SSE-S3 en reposo

10. Referencias


Ultima Actualizacion: 2026-01-10