/** * MinIO storage helpers for ANCPI CF extracts. * * Bucket: ancpi-documente (separate from general "tools" bucket) * Naming: {index:02d}_Extras CF_{nrCadastral} - {DD-MM-YYYY}.pdf */ import { minioClient } from "@/core/storage/minio-client"; import { Readable } from "stream"; const BUCKET = process.env.MINIO_BUCKET_ANCPI || "ancpi-documente"; let bucketChecked = false; /** Ensure the ANCPI bucket exists (idempotent, cached) */ export async function ensureAncpiBucket(): Promise { if (bucketChecked) return; try { const exists = await minioClient.bucketExists(BUCKET); if (!exists) { await minioClient.makeBucket(BUCKET); console.log(`[epay-storage] Bucket '${BUCKET}' created.`); } bucketChecked = true; } catch (error) { console.error("[epay-storage] Bucket check/create failed:", error); } } /** * Get the next file index for a given cadastral number. * Scans existing objects to find the highest index. */ export async function getNextFileIndex( nrCadastral: string, ): Promise { await ensureAncpiBucket(); const pattern = new RegExp( `^(\\d+)_Extras CF_${nrCadastral.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} -`, ); let maxIndex = 0; const stream = minioClient.listObjects(BUCKET, "", true); return new Promise((resolve, reject) => { stream.on("data", (obj) => { if (!obj.name) return; const match = obj.name.match(pattern); if (match) { const idx = parseInt(match[1] ?? "0", 10); if (idx > maxIndex) maxIndex = idx; } }); stream.on("end", () => resolve(maxIndex + 1)); stream.on("error", reject); }); } /** * Build the display filename for a CF extract. * Format: 01_Extras CF_291479 - 22-03-2026.pdf */ export function buildFileName( index: number, nrCadastral: string, date: Date, ): string { const idx = String(index).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); const mm = String(date.getMonth() + 1).padStart(2, "0"); const yyyy = date.getFullYear(); return `${idx}_Extras CF_${nrCadastral} - ${dd}-${mm}-${yyyy}.pdf`; } /** * Store a CF extract PDF in MinIO. * Returns the MinIO path and file index. */ export async function storeCfExtract( pdfBuffer: Buffer, nrCadastral: string, metadata: Record, ): Promise<{ path: string; fileName: string; index: number }> { await ensureAncpiBucket(); const index = await getNextFileIndex(nrCadastral); const fileName = buildFileName(index, nrCadastral, new Date()); // Store in subfolder per cadastral number const path = `parcele/${nrCadastral}/${fileName}`; await minioClient.putObject(BUCKET, path, pdfBuffer, pdfBuffer.length, { "Content-Type": "application/pdf", ...metadata, }); console.log( `[epay-storage] Stored: ${path} (${pdfBuffer.length} bytes)`, ); return { path, fileName, index }; } /** * Get a readable stream for a stored CF extract. */ export async function getCfExtractStream( minioPath: string, ): Promise { return minioClient.getObject(BUCKET, minioPath); } /** * List all stored CF extracts for a cadastral number. */ export async function listExtractsForParcel( nrCadastral: string, ): Promise> { await ensureAncpiBucket(); const prefix = `parcele/${nrCadastral}/`; const results: Array<{ name: string; lastModified: Date; size: number }> = []; const stream = minioClient.listObjects(BUCKET, prefix, true); return new Promise((resolve, reject) => { stream.on("data", (obj) => { if (obj.name) { results.push({ name: obj.name, lastModified: obj.lastModified ?? new Date(), size: obj.size ?? 0, }); } }); stream.on("end", () => resolve(results)); stream.on("error", reject); }); } /** * Generate a presigned download URL (7 day expiry). */ export async function getPresignedUrl( minioPath: string, expirySeconds = 7 * 24 * 3600, ): Promise { return minioClient.presignedGetObject(BUCKET, minioPath, expirySeconds); }