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>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user