Update DocumentUploader to upload all accepted files without filtering by type, enabling consistent handling of DOCX and other supported formats. Add a default timeout for DOCX-to-PDF conversion to prevent hanging LibreOffice processes. This improves reliability and ensures uploads do not silently stall.
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { spawn } from 'child_process';
|
|
import { existsSync } from 'fs';
|
|
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'fs/promises';
|
|
import path from 'path';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { pathToFileURL } from 'url';
|
|
|
|
const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
|
|
const TEMP_DIR = path.join(DOCSTORE_DIR, 'tmp');
|
|
const DEFAULT_DOCX_CONVERSION_TIMEOUT_MS = 60_000;
|
|
|
|
async function ensureTempDir(): Promise<void> {
|
|
if (!existsSync(DOCSTORE_DIR)) {
|
|
await mkdir(DOCSTORE_DIR, { recursive: true });
|
|
}
|
|
if (!existsSync(TEMP_DIR)) {
|
|
await mkdir(TEMP_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
async function convertDocxToPdf(inputPath: string, outputDir: string, profileDir?: string): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const args: string[] = [];
|
|
if (profileDir) {
|
|
args.push(`-env:UserInstallation=${pathToFileURL(profileDir).toString()}`);
|
|
}
|
|
args.push('--headless', '--nologo', '--convert-to', 'pdf', '--outdir', outputDir, inputPath);
|
|
const proc = spawn('soffice', args);
|
|
let settled = false;
|
|
const timeout = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
proc.kill();
|
|
reject(new Error(`LibreOffice conversion timed out after ${DEFAULT_DOCX_CONVERSION_TIMEOUT_MS}ms`));
|
|
}, DEFAULT_DOCX_CONVERSION_TIMEOUT_MS);
|
|
|
|
proc.on('error', (error) => {
|
|
clearTimeout(timeout);
|
|
if (settled) return;
|
|
settled = true;
|
|
reject(error);
|
|
});
|
|
proc.on('close', (code) => {
|
|
clearTimeout(timeout);
|
|
if (settled) return;
|
|
settled = true;
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`LibreOffice conversion failed with code ${code}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function waitForPdfReady(dir: string, timeoutMs = 20_000, intervalMs = 100): Promise<string> {
|
|
const end = Date.now() + timeoutMs;
|
|
while (Date.now() < end) {
|
|
const files = await readdir(dir);
|
|
const pdf = files.find((f) => f.toLowerCase().endsWith('.pdf'));
|
|
if (pdf) {
|
|
const pdfPath = path.join(dir, pdf);
|
|
try {
|
|
const first = await stat(pdfPath);
|
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
const second = await stat(pdfPath);
|
|
if (second.size > 0 && second.size === first.size) {
|
|
return pdfPath;
|
|
}
|
|
} catch {
|
|
// Ignore transient filesystem races while LibreOffice is still writing.
|
|
}
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
}
|
|
throw new Error(`PDF not ready in ${dir} after ${timeoutMs}ms`);
|
|
}
|
|
|
|
export async function convertDocxBufferToPdfBuffer(docxBytes: Buffer): Promise<Buffer> {
|
|
await ensureTempDir();
|
|
|
|
const tempId = randomUUID();
|
|
const jobDir = path.join(TEMP_DIR, tempId);
|
|
await mkdir(jobDir, { recursive: true });
|
|
const profileDir = path.join(jobDir, 'lo-profile');
|
|
await mkdir(profileDir, { recursive: true });
|
|
const inputPath = path.join(jobDir, 'input.docx');
|
|
|
|
try {
|
|
await writeFile(inputPath, docxBytes);
|
|
await convertDocxToPdf(inputPath, jobDir, profileDir);
|
|
const pdfPath = await waitForPdfReady(jobDir);
|
|
return await readFile(pdfPath);
|
|
} finally {
|
|
await rm(jobDir, { recursive: true, force: true }).catch(() => {});
|
|
}
|
|
}
|