/** * Migrate received invoice files from DB BLOB to NAS storage. * * Usage: NAS_FINANCIALS_PATH=/mnt/nas/financials npx tsx scripts/migrate-received-invoices-to-nas.ts */ import "../src/config/env"; import fs from "fs"; import { PrismaClient } from "@prisma/client"; import { nasFinancialsManager } from "../src/services/nas-financials-manager"; const prisma = new PrismaClient(); async function main() { if (!nasFinancialsManager.isConfigured()) { console.error( "NAS_FINANCIALS_PATH is not configured or path does not exist.", ); process.exit(1); } const records = await prisma.received_invoices.findMany({ where: { file_data: { not: null }, file_path: null }, select: { id: true, file_data: true, file_name: true, month: true, year: true, }, }); console.log(`Found ${records.length} invoices to migrate.`); let migrated = 0; let failed = 0; for (const rec of records) { if (!rec.file_data) continue; const fileName = rec.file_name || `invoice-${rec.id}.pdf`; const result = nasFinancialsManager.saveReceivedInvoice( fileName, rec.year, rec.month, Buffer.from(rec.file_data), ); if ("error" in result) { console.error(` FAIL id=${rec.id}: ${result.error}`); failed++; continue; } // Read-back verification before we null out the ONLY DB copy of the bytes. // The manager uses writeFileSync (synchronous) and returns a relative path; // resolve it back to disk and confirm the file exists and its size matches // the source buffer. A 0-byte/truncated/corrupt write would otherwise lose // the invoice permanently once file_data is set to null. If verification // fails we KEEP file_data and report the row as failed. const expectedSize = rec.file_data.length; const readBack = nasFinancialsManager.readReceivedInvoice(result.filePath); let writtenSize = -1; if (readBack) { writtenSize = readBack.data.length; } else { // Fall back to a direct stat in case readReceivedInvoice's path guard // rejects an otherwise-valid path; either way we need a confirmed size. try { writtenSize = fs.statSync( (nasFinancialsManager as unknown as { basePath: string }).basePath + "/" + result.filePath, ).size; } catch { writtenSize = -1; } } if (writtenSize !== expectedSize) { console.error( ` FAIL id=${rec.id}: read-back mismatch (expected ${expectedSize} bytes, got ${writtenSize}); keeping DB copy`, ); failed++; continue; } await prisma.received_invoices.update({ where: { id: rec.id }, data: { file_path: result.filePath, file_data: null }, }); migrated++; if (migrated % 50 === 0) { console.log(` Progress: ${migrated}/${records.length}...`); } } console.log( `Done. Migrated: ${migrated}, Failed: ${failed}, Total: ${records.length}`, ); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());