86 lines
2.8 KiB
TypeScript
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(() => {});
|
|
}
|
|
}
|
|
}
|