echos-ocr/src/lib/watcher.ts
Randall Stillwell 034c73128f Add runtime safeguards: OCR concurrency limiter, upload size cap, and batched Monday sync
- Processing queue limits OCR to 2 concurrent jobs across all sources (upload, email, folder watcher)
- 50 MB file size cap enforced on uploads, email attachments, and folder watcher
- Monday.com bulk sync now processes in batches of 5 with 1s delay between batches

Made-with: Cursor
2026-04-07 13:44:04 -05:00

99 lines
2.6 KiB
TypeScript

import chokidar, { type FSWatcher } from "chokidar";
import fs from "fs/promises";
import path from "path";
import { prisma } from "./db";
import { enqueueProcessing } from "./processing-queue";
let watcher: FSWatcher | null = null;
const processedFiles = new Set<string>();
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB
export async function startWatching(watchDir: string): Promise<void> {
if (watcher) {
await stopWatching();
}
try {
await fs.access(watchDir);
} catch {
throw new Error(`Watch directory does not exist: ${watchDir}`);
}
watcher = chokidar.watch(watchDir, {
ignored: /(^|[/\\])\../,
persistent: true,
ignoreInitial: false,
awaitWriteFinish: {
stabilityThreshold: 2000,
pollInterval: 500,
},
});
watcher.on("add", async (filePath: string) => {
const ext = path.extname(filePath).toLowerCase();
const allowed = [".pdf", ".jpg", ".jpeg", ".png", ".webp"];
if (!allowed.includes(ext)) return;
if (processedFiles.has(filePath)) return;
processedFiles.add(filePath);
const fileName = path.basename(filePath);
const isPdf = ext === ".pdf";
try {
const stat = await fs.stat(filePath);
if (stat.size > MAX_FILE_SIZE) {
console.log(`[watcher] Skipping ${fileName} — exceeds 50 MB size limit (${(stat.size / 1024 / 1024).toFixed(1)} MB)`);
return;
}
const buffer = await fs.readFile(filePath);
const job = await prisma.processingJob.create({
data: {
fileName,
filePath,
status: "queued",
},
});
enqueueProcessing(job.id, fileName, buffer, isPdf).catch((err) => {
console.error(`[watcher] Processing failed for ${fileName}:`, err);
});
console.log(`[watcher] Queued: ${fileName}`);
} catch (err) {
console.error(`[watcher] Failed to read file ${filePath}:`, err);
}
});
watcher.on("error", (error: unknown) => {
console.error("[watcher] Error:", error);
});
await prisma.appSettings.upsert({
where: { id: "singleton" },
update: { watching: true, watchDir },
create: { id: "singleton", watching: true, watchDir },
});
console.log(`[watcher] Started watching: ${watchDir}`);
}
export async function stopWatching(): Promise<void> {
if (watcher) {
await watcher.close();
watcher = null;
}
await prisma.appSettings.upsert({
where: { id: "singleton" },
update: { watching: false },
create: { id: "singleton", watching: false },
});
console.log("[watcher] Stopped");
}
export function isWatching(): boolean {
return watcher !== null;
}