Files
jorgecuadros-platform/apps/api/src/storage/upload-file.ts
T
rmancinasandClaude Opus 4.8 afe2411c86
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m37s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m8s
feat(storage): wire MinIO/S3 document upload & download into the API + web
The schema has carried `storageKey` pointers and the migration has written
blobs to MinIO since day one, but the API had no S3 client — documents could
only be deleted, never uploaded or retrieved. This adds the missing wiring.

API
- StorageModule/StorageService (@aws-sdk/client-s3, path-style for MinIO):
  put/getStream/delete, best-effort bucket ensure on boot, gracefully disabled
  when S3 env is absent (ServiceUnavailable on use).
- Reads S3_ENDPOINT/S3_BUCKET + S3_ACCESS_KEY/S3_SECRET_KEY, falling back to
  MINIO_ROOT_USER/MINIO_ROOT_PASSWORD so one credential set drives both the
  migration and the API.
- Property service documents: POST :id/documents (multipart), GET
  :id/documents/:childId/download (streamed), delete now also drops the blob.
- Policy documents: same upload/download/delete (previously had none).
- Keys stay under the service/<id>/… and policy/<id>/… prefixes the migration
  established.

Web
- api.ts: shared uploadFile() helper (uploadIngest refactored onto it),
  upload/download/remove helpers for property & policy documents.
- Servicios, polizas, clientes detail pages: real Descargar links and an
  upload control (gated by policy:update / property:update) replacing the
  "storage pending" notes.

Infra
- docker-compose: minio service (9000/9001, healthcheck, named volume) + S3
  env wired into the api service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:19:57 -07:00

33 lines
1.1 KiB
TypeScript

import { extname } from "node:path";
/** Multer file shape we rely on (subset of Express.Multer.File). */
export interface UploadedFileLike {
buffer: Buffer;
originalname?: string;
mimetype?: string;
size?: number;
}
const MIME_EXT: Record<string, string> = {
"application/pdf": ".pdf",
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/tiff": ".tif",
"image/bmp": ".bmp",
};
/** File extension for a stored blob, from the original name, else the mimetype. */
export function extForUpload(file: UploadedFileLike): string {
const fromName = file.originalname ? extname(file.originalname).toLowerCase() : "";
if (fromName) return fromName;
return (file.mimetype && MIME_EXT[file.mimetype]) || "";
}
/** Download filename for a stored document, from its key + document type. */
export function downloadName(storageKey: string, documentType: string): string {
const ext = extname(storageKey) || "";
const base = documentType.replace(/[^\w.-]+/g, "_") || "document";
return base.toLowerCase().endsWith(ext.toLowerCase()) ? base : `${base}${ext}`;
}