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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user