Add batch upload endpoint to avoid sequential upload+OCR bottleneck

Client sends all files in a single request to /api/upload/batch,
server receives them all upfront via multer then processes OCR
sequentially. Eliminates network round-trips between each file.
This commit is contained in:
Joey Yakimowich-Payne 2026-02-19 14:00:20 -07:00
commit bce534486c
2 changed files with 76 additions and 9 deletions

View file

@ -149,6 +149,74 @@ router.post('/', upload.single('document'), async (req: AuthenticatedRequest, re
}
});
const MAX_BATCH_FILES = 20;
router.post('/batch', upload.array('documents', MAX_BATCH_FILES), async (req: AuthenticatedRequest, res) => {
if (activeUploads >= MAX_CONCURRENT_UPLOADS && uploadQueue.length >= MAX_QUEUE_SIZE) {
return res.status(503).json({ error: 'Server busy processing uploads. Please try again shortly.' });
}
try {
await waitForSlot();
} catch {
return res.status(504).json({ error: 'Upload timed out waiting in queue. Please try again.' });
}
activeUploads++;
try {
const files = req.files as Express.Multer.File[];
if (!files || files.length === 0) {
activeUploads--;
return res.status(400).json({ error: 'No files uploaded' });
}
const useOcr = req.body?.useOcr === 'true' || req.body?.useOcr === true;
if (useOcr) {
const groups = req.user?.groups || [];
const hasGroupAccess = groups.includes('kaboot-ai-access');
const status = req.user ? getSubscriptionStatus(req.user.sub) : null;
const hasSubscriptionAccess = status?.status === 'active';
if (!hasGroupAccess && !hasSubscriptionAccess) {
return res.status(403).json({ error: 'OCR is available to Pro subscribers only.' });
}
}
const results = [];
for (const file of files) {
const normalizedMime = normalizeMimeType(file.mimetype, file.originalname);
const processed = await processDocument(file.buffer, normalizedMime, { useOcr });
if (processed.type === 'native') {
results.push({
type: 'native',
content: (processed.content as Buffer).toString('base64'),
mimeType: processed.mimeType,
originalName: file.originalname
});
} else {
results.push({
type: 'text',
content: processed.content as string,
mimeType: processed.mimeType,
originalName: file.originalname
});
}
}
res.json(results);
} catch (error) {
console.error('Batch upload processing error:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Failed to process documents'
});
} finally {
activeUploads--;
drainQueue();
}
});
router.use((err: Error, _req: any, res: any, _next: any) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {