Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/integrations/fileScanQueue.server.ts
T

86 lines
2.8 KiB
TypeScript

// Producer-side helper for the toolctrl.file-scan BullMQ queue. Web routes
// call enqueueFileScan(...) after a successful upload to trigger the
// asynchronous post-upload ClamAV scan in the worker. Mirrors the
// TransactionalEmailQueue.server.ts shape so the queue lifecycle is the
// same shared pattern.
import {
buildFileScanJob,
enqueueJob,
queueNames,
resolveRedisConnectionOptions,
resolveRedisConnectionOptionsFromProcessEnv,
type RedisConnectionEnv,
type TypedQueue,
} from "@toolctrl/queue";
export type EnqueueFileScanInput = {
bucket: string;
key: string;
tenantId?: string;
uploadId?: string;
contentType?: string;
// AK-P4a §5 — File row id so the worker can write the verdict back onto
// File.scanStatus (clean|infected). See enqueueFileScan callers.
fileId?: string;
metadata?: Record<string, unknown>;
};
export type EnqueueFileScanResult =
| { enqueued: true; jobId?: string }
| { enqueued: false; reason: "scan-disabled" | "queue-unavailable" };
type Dependencies = {
env?: RedisConnectionEnv & { SCAN_QUEUE_ENABLED?: string };
queue?: TypedQueue<typeof queueNames.fileScan>;
};
export async function enqueueFileScan(input: EnqueueFileScanInput, dependencies: Dependencies = {}): Promise<EnqueueFileScanResult> {
const env = dependencies.env ?? (process.env as RedisConnectionEnv & { SCAN_QUEUE_ENABLED?: string });
if (env.SCAN_QUEUE_ENABLED !== "true") {
return { enqueued: false, reason: "scan-disabled" };
}
let queue = dependencies.queue;
let shouldCloseQueue = false;
const connection = queue
? undefined
: dependencies.env
? resolveRedisConnectionOptions({ env: dependencies.env })
: resolveRedisConnectionOptionsFromProcessEnv();
if (!queue && !connection) {
return { enqueued: false, reason: "queue-unavailable" };
}
if (!queue && connection) {
const queueRuntimeModule = "@toolctrl/queue/runtime";
const { createQueue } = (await import(/* @vite-ignore */ queueRuntimeModule)) as typeof import("@toolctrl/queue/runtime");
queue = createQueue(queueNames.fileScan, { connection });
shouldCloseQueue = true;
}
if (!queue) {
return { enqueued: false, reason: "queue-unavailable" };
}
try {
const descriptor = buildFileScanJob({
bucket: input.bucket,
key: input.key,
tenantId: input.tenantId,
uploadId: input.uploadId,
contentType: input.contentType,
fileId: input.fileId,
metadata: input.metadata,
});
const job = await enqueueJob(queue, descriptor, {
...(input.tenantId !== undefined ? { tenantId: input.tenantId } : {}),
});
return { enqueued: true, jobId: typeof job.id === "string" ? job.id : undefined };
} finally {
if (shouldCloseQueue && queue) {
await queue.close().catch(() => {});
}
}
}