feat(archive): wire archive converters, validation, and extension parsing into app.js/worker.js

Also fixes a third double-extension bug the plan missed: app.js derived
each job's uuid via path.basename(file.filename, path.extname(...)),
which only strips the last dot segment. For a compound extension like
tar.gz this left "<uuid>.tar" as the "uuid", overflowing the uuid
column (Char(36)) and crashing the request. Replaced with
stripExtension(). Also bumped rateLimitMaxJobs in jobs.test.js's config
override — the file's cumulative POST /api/jobs calls across all tests
now exceeds the production default (20/window) once the archive tests
are included, causing spurious connection resets independent of any
real bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 13:00:01 +02:00
co-authored by Claude Sonnet 5
parent a8cdb06d5c
commit ef6de6b583
3 changed files with 114 additions and 6 deletions
+9 -5
View File
@@ -13,6 +13,8 @@ import { registerHeicConverter } from './converters/heic.js';
import { registerFontConverter } from './converters/font.js';
import { registerDfontConverter } from './converters/dfont.js';
import { registerEbookConverter } from './converters/ebook.js';
import { registerArchiveConverters } from './converters/archive.js';
import { extractExtension, stripExtension } from './archiveExtensions.js';
import { resolveInputFormat } from './mime.js';
import { deleteIfExists } from './storage.js';
import { createJob, getJobByUuid } from './jobs/jobRepository.js';
@@ -23,8 +25,9 @@ const VALID_ICON_SIZES = [16, 32, 48, 256, 512];
function isValidQuality(targetFormat, quality) {
if (quality === null || quality === undefined) return true;
if (!Number.isInteger(quality)) return false;
if (targetFormat === 'gif' || targetFormat === 'ico') return false;
if (targetFormat === 'gif' || targetFormat === 'ico' || targetFormat === 'tar') return false;
if (targetFormat === 'png') return quality >= 0 && quality <= 9;
if (['zip', 'tar.gz', 'tar.bz2', '7z', 'tar.7z'].includes(targetFormat)) return quality >= 0 && quality <= 9;
return quality >= 1 && quality <= 100;
}
@@ -46,6 +49,7 @@ function registerAllConverters() {
registerFontConverter();
registerDfontConverter();
registerEbookConverter();
registerArchiveConverters();
convertersRegistered = true;
}
@@ -58,7 +62,7 @@ export function createApp(config, prisma) {
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, path.join(config.storageDir, 'uploads')),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).slice(1).toLowerCase();
const ext = extractExtension(file.originalname);
cb(null, `${uuidv4()}.${ext}`);
},
});
@@ -137,8 +141,8 @@ export function createApp(config, prisma) {
for (let i = 0; i < req.files.length; i += 1) {
const file = req.files[i];
const targetFormat = targetFormats[i];
const uuid = path.basename(file.filename, path.extname(file.filename));
const sourceFormat = path.extname(file.filename).slice(1).toLowerCase();
const uuid = stripExtension(file.filename);
const sourceFormat = extractExtension(file.filename);
const { mime, valid } = await resolveInputFormat(file.path, sourceFormat);
if (!valid) {
@@ -231,7 +235,7 @@ export function createApp(config, prisma) {
}
const filePath = outputPath(config, job.uuid, job.targetFormat);
const downloadFilename = `${path.parse(job.originalFilename).name}.${job.targetFormat}`;
const downloadFilename = `${stripExtension(job.originalFilename)}.${job.targetFormat}`;
res.set('Content-Type', job.outputMimeType);
res.set('Content-Disposition', contentDispositionHeader(downloadFilename));
fs.createReadStream(filePath).pipe(res);
+2
View File
@@ -13,6 +13,7 @@ import { registerHeicConverter } from './converters/heic.js';
import { registerFontConverter } from './converters/font.js';
import { registerDfontConverter } from './converters/dfont.js';
import { registerEbookConverter } from './converters/ebook.js';
import { registerArchiveConverters } from './converters/archive.js';
import { findPendingJobs, markProcessing, markDone, markFailed } from './jobs/jobRepository.js';
const JOB_TIMEOUT_MS = 60000;
@@ -89,6 +90,7 @@ async function main() {
registerFontConverter();
registerDfontConverter();
registerEbookConverter();
registerArchiveConverters();
startWorker(prisma, config);
console.log(`Worker started, polling every ${config.workerPollIntervalMs}ms`);