fix(storage): route every file op through getStorageBackend()
Removes 12 direct minioClient.{put,get,remove}Object call sites that
bypassed the pluggable storage abstraction. Filesystem-mode deploys
(MULTI_NODE_DEPLOYMENT=false, storage_backend=filesystem) silently
broke at every site: GDPR export, invoice PDF, EOI generation, portal
download, file upload, folder create/rename/delete, signed PDF land,
maintenance cleanup, etc. Each site now resolves the active backend
and uses its put/get/delete + the new presignDownloadUrl() helper.
Folder marker objects in /files/folders/* keep the same on-the-wire
shape but route through the backend. A future refactor should move
folder bookkeeping to a DB-backed virtual-folder table (see audit
HIGH §3 follow-up note in the route file).
Sites left untouched: src/lib/services/system-monitoring.service.ts
and src/app/api/ready/route.ts use minioClient.bucketExists as an S3-
specific health probe — those are correctly mode-aware and stay.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §3 (auditor-D Issue 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,8 @@ import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { minioClient, buildStoragePath } from '@/lib/minio';
|
||||
import { buildStoragePath } from '@/lib/minio';
|
||||
import { getStorageBackend } from '@/lib/storage';
|
||||
import { env } from '@/lib/env';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { generatePdf } from '@/lib/pdf/generate';
|
||||
@@ -525,13 +526,14 @@ export async function generateFromTemplate(
|
||||
'pdf',
|
||||
);
|
||||
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET,
|
||||
storagePath,
|
||||
Buffer.from(pdfBytes),
|
||||
pdfBytes.byteLength,
|
||||
{ 'Content-Type': 'application/pdf' },
|
||||
);
|
||||
{
|
||||
const buffer = Buffer.from(pdfBytes);
|
||||
const backend = await getStorageBackend();
|
||||
await backend.put(storagePath, buffer, {
|
||||
contentType: 'application/pdf',
|
||||
sizeBytes: buffer.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Create file record
|
||||
const [fileRecord] = await db
|
||||
@@ -661,13 +663,14 @@ async function generateEoiFromSourcePdf(
|
||||
'pdf',
|
||||
);
|
||||
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET,
|
||||
storagePath,
|
||||
Buffer.from(pdfBytes),
|
||||
pdfBytes.byteLength,
|
||||
{ 'Content-Type': 'application/pdf' },
|
||||
);
|
||||
{
|
||||
const buffer = Buffer.from(pdfBytes);
|
||||
const backend = await getStorageBackend();
|
||||
await backend.put(storagePath, buffer, {
|
||||
contentType: 'application/pdf',
|
||||
sizeBytes: buffer.length,
|
||||
});
|
||||
}
|
||||
|
||||
const [fileRecord] = await db
|
||||
.insert(files)
|
||||
@@ -809,11 +812,13 @@ async function generateAndSignViaInApp(
|
||||
file: DbFile;
|
||||
});
|
||||
|
||||
// Fetch PDF bytes from MinIO to send to Documenso
|
||||
const pdfStream = await minioClient.getObject(env.MINIO_BUCKET, file.storagePath);
|
||||
// Fetch PDF bytes from the active storage backend to send to Documenso.
|
||||
const pdfStream = await (await getStorageBackend()).get(file.storagePath);
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of pdfStream) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as ArrayBuffer));
|
||||
if (Buffer.isBuffer(chunk)) chunks.push(chunk);
|
||||
else if (typeof chunk === 'string') chunks.push(Buffer.from(chunk));
|
||||
else chunks.push(Buffer.from(chunk as Uint8Array));
|
||||
}
|
||||
const pdfBase64 = Buffer.concat(chunks).toString('base64');
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { NotFoundError, ValidationError, ConflictError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { minioClient, buildStoragePath } from '@/lib/minio';
|
||||
import { buildStoragePath } from '@/lib/minio';
|
||||
import { getStorageBackend } from '@/lib/storage';
|
||||
import { env } from '@/lib/env';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { evaluateRule } from '@/lib/services/berth-rules-engine';
|
||||
@@ -549,7 +550,7 @@ export async function sendForSigning(documentId: string, portId: string, meta: A
|
||||
const fileRecord = await db.query.files.findFirst({ where: eq(files.id, doc.fileId) });
|
||||
if (!fileRecord) throw new NotFoundError('File');
|
||||
|
||||
const fileStream = await minioClient.getObject(env.MINIO_BUCKET, fileRecord.storagePath);
|
||||
const fileStream = await (await getStorageBackend()).get(fileRecord.storagePath);
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of fileStream) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
@@ -659,8 +660,11 @@ export async function uploadSignedManually(
|
||||
const fileId = crypto.randomUUID();
|
||||
const storagePath = buildStoragePath(port.slug, 'eoi-signed', documentId, fileId, 'pdf');
|
||||
|
||||
await minioClient.putObject(env.MINIO_BUCKET, storagePath, fileData.buffer, fileData.size, {
|
||||
'Content-Type': fileData.mimeType,
|
||||
await (
|
||||
await getStorageBackend()
|
||||
).put(storagePath, fileData.buffer, {
|
||||
contentType: fileData.mimeType,
|
||||
sizeBytes: fileData.size,
|
||||
});
|
||||
|
||||
const [fileRecord] = await db
|
||||
@@ -860,13 +864,12 @@ export async function handleDocumentCompleted(eventData: { documentId: string })
|
||||
const fileId = crypto.randomUUID();
|
||||
const storagePath = buildStoragePath(port.slug, 'eoi-signed', doc.id, fileId, 'pdf');
|
||||
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET,
|
||||
storagePath,
|
||||
signedPdfBuffer,
|
||||
signedPdfBuffer.length,
|
||||
{ 'Content-Type': 'application/pdf' },
|
||||
);
|
||||
await (
|
||||
await getStorageBackend()
|
||||
).put(storagePath, signedPdfBuffer, {
|
||||
contentType: 'application/pdf',
|
||||
sizeBytes: signedPdfBuffer.length,
|
||||
});
|
||||
|
||||
const [fileRecord] = await db
|
||||
.insert(files)
|
||||
|
||||
@@ -114,8 +114,8 @@ export async function sendEmail(
|
||||
where: eq(files.id, ref.fileId),
|
||||
});
|
||||
if (!file) throw new NotFoundError('File');
|
||||
const { minioClient } = await import('@/lib/minio');
|
||||
const stream = await minioClient.getObject(file.storageBucket, file.storagePath);
|
||||
const { getStorageBackend } = await import('@/lib/storage');
|
||||
const stream = await (await getStorageBackend()).get(file.storagePath);
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { berthMaintenanceLog } from '@/lib/db/schema/berths';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { minioClient, getPresignedUrl } from '@/lib/minio';
|
||||
import { getStorageBackend, presignDownloadUrl } from '@/lib/storage';
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
import { env } from '@/lib/env';
|
||||
import {
|
||||
@@ -50,8 +50,10 @@ export async function uploadFile(
|
||||
const sanitizedOriginal = sanitizeFilename(file.originalName);
|
||||
const sanitizedFilename = sanitizeFilename(data.filename);
|
||||
|
||||
await minioClient.putObject(env.MINIO_BUCKET, storagePath, file.buffer, file.size, {
|
||||
'Content-Type': file.mimeType,
|
||||
const backend = await getStorageBackend();
|
||||
await backend.put(storagePath, file.buffer, {
|
||||
contentType: file.mimeType,
|
||||
sizeBytes: file.size,
|
||||
});
|
||||
|
||||
const [record] = await db
|
||||
@@ -93,7 +95,7 @@ export async function uploadFile(
|
||||
|
||||
export async function getDownloadUrl(id: string, portId: string) {
|
||||
const file = await getFileById(id, portId);
|
||||
const url = await getPresignedUrl(file.storagePath);
|
||||
const url = await presignDownloadUrl(file.storagePath);
|
||||
return { url, filename: file.filename };
|
||||
}
|
||||
|
||||
@@ -104,7 +106,7 @@ export async function getPreviewUrl(id: string, portId: string) {
|
||||
throw new ValidationError('This file type cannot be previewed');
|
||||
}
|
||||
|
||||
const url = await getPresignedUrl(file.storagePath);
|
||||
const url = await presignDownloadUrl(file.storagePath);
|
||||
return { url, mimeType: file.mimeType };
|
||||
}
|
||||
|
||||
@@ -183,8 +185,10 @@ export async function deleteFile(id: string, portId: string, meta: AuditMeta) {
|
||||
throw new ConflictError('File cannot be deleted because it is referenced by other records');
|
||||
}
|
||||
|
||||
// Delete from MinIO first, then DB
|
||||
await minioClient.removeObject(env.MINIO_BUCKET, existing.storagePath);
|
||||
// Delete the blob first, then DB. The storage backend's delete is
|
||||
// idempotent, so a partial replay (worker crashed mid-delete) does not
|
||||
// throw on the missing-object retry.
|
||||
await (await getStorageBackend()).delete(existing.storagePath);
|
||||
|
||||
await db.delete(files).where(and(eq(files.id, id), eq(files.portId, portId)));
|
||||
|
||||
|
||||
@@ -18,10 +18,9 @@ import { db } from '@/lib/db';
|
||||
import { gdprExports, type GdprExport } from '@/lib/db/schema/gdpr';
|
||||
import { clients, clientContacts } from '@/lib/db/schema/clients';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { env } from '@/lib/env';
|
||||
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { minioClient, getPresignedUrl } from '@/lib/minio';
|
||||
import { getStorageBackend, presignDownloadUrl } from '@/lib/storage';
|
||||
import { getQueue } from '@/lib/queue';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { buildClientBundle, renderBundleHtml } from '@/lib/services/gdpr-bundle-builder';
|
||||
@@ -163,9 +162,11 @@ export async function processGdprExportJob(input: ProcessJobInput): Promise<void
|
||||
const portSlug = port?.slug ?? 'unknown';
|
||||
const storageKey = `${portSlug}/gdpr-exports/${input.clientId}/${input.exportId}.zip`;
|
||||
|
||||
await minioClient.putObject(env.MINIO_BUCKET, storageKey, buffer, buffer.length, {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Disposition': `attachment; filename="gdpr-export-${input.clientId}.zip"`,
|
||||
const backend = await getStorageBackend();
|
||||
await backend.put(storageKey, buffer, {
|
||||
contentType: 'application/zip',
|
||||
sizeBytes: buffer.length,
|
||||
contentDisposition: `attachment; filename="gdpr-export-${input.clientId}.zip"`,
|
||||
});
|
||||
|
||||
const expiresAt = new Date(Date.now() + EXPIRY_DAYS * 24 * 60 * 60 * 1000);
|
||||
@@ -217,7 +218,7 @@ async function emailExport(input: ProcessJobInput, storageKey: string): Promise<
|
||||
return;
|
||||
}
|
||||
|
||||
const url = await getPresignedUrl(storageKey, PRESIGN_EXPIRY_SECONDS);
|
||||
const url = await presignDownloadUrl(storageKey, PRESIGN_EXPIRY_SECONDS);
|
||||
const client = await db.query.clients.findFirst({ where: eq(clients.id, input.clientId) });
|
||||
const name = client?.fullName ?? 'there';
|
||||
const expiry = new Date(Date.now() + PRESIGN_EXPIRY_SECONDS * 1000).toUTCString();
|
||||
@@ -275,5 +276,5 @@ export async function getExportDownloadUrl(exportId: string, portId: string): Pr
|
||||
if (!row.storageKey || (row.status !== 'ready' && row.status !== 'sent')) {
|
||||
throw new ValidationError('Export is not ready to download');
|
||||
}
|
||||
return getPresignedUrl(row.storageKey, PRESIGN_EXPIRY_SECONDS);
|
||||
return presignDownloadUrl(row.storageKey, PRESIGN_EXPIRY_SECONDS);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ import { emitToRoom } from '@/lib/socket/server';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { generatePdf } from '@/lib/pdf/generate';
|
||||
import { invoiceTemplate, buildInvoiceInputs } from '@/lib/pdf/templates/invoice-template';
|
||||
import { minioClient, buildStoragePath } from '@/lib/minio';
|
||||
import { buildStoragePath } from '@/lib/minio';
|
||||
import { getStorageBackend } from '@/lib/storage';
|
||||
import { getQueue } from '@/lib/queue';
|
||||
import { env } from '@/lib/env';
|
||||
import type {
|
||||
@@ -595,13 +596,12 @@ export async function generateInvoicePdf(id: string, portId: string, meta: Audit
|
||||
const fileId = crypto.randomUUID();
|
||||
const storagePath = buildStoragePath(port?.slug ?? portId, 'invoices', id, fileId, 'pdf');
|
||||
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET,
|
||||
storagePath,
|
||||
Buffer.from(pdfBytes),
|
||||
pdfBytes.length,
|
||||
{ 'Content-Type': 'application/pdf' },
|
||||
);
|
||||
const buffer = Buffer.from(pdfBytes);
|
||||
const backend = await getStorageBackend();
|
||||
await backend.put(storagePath, buffer, {
|
||||
contentType: 'application/pdf',
|
||||
sizeBytes: buffer.length,
|
||||
});
|
||||
|
||||
const [fileRecord] = await db
|
||||
.insert(files)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ports } from '@/lib/db/schema/ports';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
||||
import { berthReservations } from '@/lib/db/schema/reservations';
|
||||
import { getPresignedUrl } from '@/lib/minio';
|
||||
import { presignDownloadUrl } from '@/lib/storage';
|
||||
import { getCountryName } from '@/lib/i18n/countries';
|
||||
|
||||
// ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||
@@ -301,7 +301,7 @@ export async function getDocumentDownloadUrl(
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
return getPresignedUrl(file.storagePath);
|
||||
return presignDownloadUrl(file.storagePath);
|
||||
}
|
||||
|
||||
// ─── Yachts (direct + via company) ────────────────────────────────────────────
|
||||
|
||||
@@ -6,7 +6,8 @@ import { notifications } from '@/lib/db/schema/operations';
|
||||
import { files } from '@/lib/db/schema/documents';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { generatePdf } from '@/lib/pdf/generate';
|
||||
import { minioClient, getPresignedUrl, buildStoragePath } from '@/lib/minio/index';
|
||||
import { buildStoragePath } from '@/lib/minio/index';
|
||||
import { getStorageBackend, presignDownloadUrl } from '@/lib/storage';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { getQueue } from '@/lib/queue';
|
||||
import { env } from '@/lib/env';
|
||||
@@ -67,11 +68,7 @@ type ReportType = keyof typeof REPORT_TYPE_MAP;
|
||||
|
||||
// ─── requestReport ────────────────────────────────────────────────────────────
|
||||
|
||||
export async function requestReport(
|
||||
portId: string,
|
||||
userId: string,
|
||||
data: RequestReportInput,
|
||||
) {
|
||||
export async function requestReport(portId: string, userId: string, data: RequestReportInput) {
|
||||
const [report] = await db
|
||||
.insert(generatedReports)
|
||||
.values({
|
||||
@@ -163,7 +160,7 @@ export async function getDownloadUrl(reportId: string, portId: string) {
|
||||
throw new NotFoundError('File');
|
||||
}
|
||||
|
||||
const url = await getPresignedUrl(file.storagePath);
|
||||
const url = await presignDownloadUrl(file.storagePath);
|
||||
return { url };
|
||||
}
|
||||
|
||||
@@ -208,7 +205,9 @@ export async function generateReport(reportJobId: string): Promise<void> {
|
||||
const portSlug = port?.slug ?? 'port';
|
||||
|
||||
// 6. Build inputs (pass portName)
|
||||
const inputs = (config.buildInputs as (data: unknown, portName: string) => Record<string, string>[])(data, portName);
|
||||
const inputs = (
|
||||
config.buildInputs as (data: unknown, portName: string) => Record<string, string>[]
|
||||
)(data, portName);
|
||||
|
||||
// 7. Generate PDF
|
||||
const pdfBytes = await generatePdf(config.template, inputs);
|
||||
@@ -217,15 +216,13 @@ export async function generateReport(reportJobId: string): Promise<void> {
|
||||
const fileId = crypto.randomUUID();
|
||||
const storagePath = buildStoragePath(portSlug, 'reports', reportJobId, fileId, 'pdf');
|
||||
|
||||
// 9. Upload PDF to MinIO
|
||||
// 9. Upload PDF via the active storage backend (filesystem or s3)
|
||||
const buffer = Buffer.from(pdfBytes);
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET,
|
||||
storagePath,
|
||||
buffer,
|
||||
buffer.length,
|
||||
{ 'Content-Type': 'application/pdf', 'report-type': reportType },
|
||||
);
|
||||
const backend = await getStorageBackend();
|
||||
await backend.put(storagePath, buffer, {
|
||||
contentType: 'application/pdf',
|
||||
sizeBytes: buffer.length,
|
||||
});
|
||||
|
||||
// 10. Insert into files table
|
||||
const [fileRecord] = await db
|
||||
|
||||
Reference in New Issue
Block a user