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