feat: add job status and download endpoints
Also adds vitest.config.js with fileParallelism: false. Every DB-backed test file wipes and reseeds the shared conversion_jobs table in beforeEach; running test files in parallel (Vitest's default) let one file's DELETE race another file's just-inserted row against the same live MariaDB instance, causing intermittent cross-file failures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+44
@@ -1,4 +1,5 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
@@ -10,6 +11,7 @@ import { registerDocumentConverters } from './converters/document.js';
|
||||
import { resolveInputFormat } from './mime.js';
|
||||
import { deleteIfExists } from './storage.js';
|
||||
import { createJob, getJobById } from './jobs/jobRepository.js';
|
||||
import { outputPath } from './storage.js';
|
||||
|
||||
let convertersRegistered = false;
|
||||
|
||||
@@ -112,5 +114,47 @@ export function createApp(config, pool) {
|
||||
res.status(201).json({ jobs: results });
|
||||
});
|
||||
|
||||
app.get('/api/jobs/:id', async (req, res) => {
|
||||
const job = await getJobById(pool, req.params.id);
|
||||
if (!job) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: job.id,
|
||||
status: job.status,
|
||||
originalFilename: job.originalFilename,
|
||||
sourceFormat: job.sourceFormat,
|
||||
targetFormat: job.targetFormat,
|
||||
errorMessage: job.errorMessage,
|
||||
});
|
||||
});
|
||||
|
||||
function contentDispositionHeader(filename) {
|
||||
const asciiFallback = filename.replace(/[^\x20-\x7E]/g, '_').replace(/"/g, "'");
|
||||
const encoded = encodeURIComponent(filename);
|
||||
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`;
|
||||
}
|
||||
|
||||
app.get('/api/jobs/:id/download', async (req, res) => {
|
||||
const job = await getJobById(pool, req.params.id);
|
||||
if (!job) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
if (job.status !== 'done') {
|
||||
return res.status(409).json({ error: `Job is not ready yet (status: ${job.status})` });
|
||||
}
|
||||
|
||||
const filePath = outputPath(config, job.id, job.targetFormat);
|
||||
res.set('Content-Type', job.outputMimeType);
|
||||
res.set('Content-Disposition', contentDispositionHeader(job.originalFilename));
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Unhandled API error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user