mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-17 21:25:54 +02:00
223 lines
7.6 KiB
JavaScript
223 lines
7.6 KiB
JavaScript
import axios from 'axios';
|
|
import multer from 'multer';
|
|
import libre from 'libreoffice-convert';
|
|
import { exec } from 'child_process';
|
|
import { promisify } from 'util';
|
|
import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js';
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
// -------------------- Process Management --------------------
|
|
/**
|
|
* Kill stuck LibreOffice processes
|
|
*/
|
|
async function killStuckProcesses() {
|
|
try {
|
|
// Kill soffice processes older than 2 minutes
|
|
if (process.platform === 'linux' || process.platform === 'darwin') {
|
|
await execAsync("pkill -9 -f 'soffice.*--headless' || true");
|
|
console.log('[DOCX2PDF] Cleaned up stuck processes');
|
|
} else if (process.platform === 'win32') {
|
|
await execAsync('taskkill /F /IM soffice.bin /T || exit 0');
|
|
console.log('[DOCX2PDF] Cleaned up stuck processes (Windows)');
|
|
}
|
|
} catch (err) {
|
|
console.error('[DOCX2PDF] Error killing processes:', err.message);
|
|
}
|
|
}
|
|
|
|
/** @returns {Promise<Buffer>} */
|
|
async function convertLibre(input, ext, opts) {
|
|
return await new Promise((resolve, reject) => {
|
|
try {
|
|
libre.convert(input, ext, opts, (err, out) => (err ? reject(err) : resolve(out)));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// -------------------- Concurrency limiter with queue limits --------------------
|
|
// CRITICAL FIX: Reduced to 1 for CPU-intensive LibreOffice conversions
|
|
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 1);
|
|
let active = 0;
|
|
const queue = [];
|
|
|
|
function runWithLimit(task) {
|
|
return new Promise((resolve, reject) => {
|
|
const run = async () => {
|
|
active++;
|
|
try {
|
|
const result = await task();
|
|
resolve(result);
|
|
} catch (error) {
|
|
reject(error);
|
|
} finally {
|
|
active--;
|
|
if (queue.length) {
|
|
const next = queue.shift();
|
|
next();
|
|
}
|
|
}
|
|
};
|
|
if (active < MAX_CONCURRENCY) run();
|
|
else queue.push(run);
|
|
});
|
|
}
|
|
|
|
// -------------------- Timeout helper with cleanup --------------------
|
|
/**
|
|
* @template T
|
|
* @param {Promise<T>} promise
|
|
* @param {number} ms
|
|
* @param {string} [label='operation']
|
|
* @returns {Promise<T>}
|
|
*/
|
|
export async function withTimeout(promise, ms, label = 'operation') {
|
|
let timer;
|
|
try {
|
|
const timeout = new Promise((_, reject) => {
|
|
timer = setTimeout(async () => {
|
|
// Kill stuck processes on timeout
|
|
await killStuckProcesses();
|
|
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
}, ms);
|
|
});
|
|
return await Promise.race([promise, timeout]);
|
|
} finally {
|
|
if (timer) clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
// -------------------- Multer: use memory storage --------------------
|
|
const storage = multer.memoryStorage();
|
|
export const upload = multer({
|
|
storage,
|
|
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB hard limit at multer level
|
|
fileFilter: (req, file, cb) => {
|
|
const okExt = /\.docx$/i.test(file.originalname || '');
|
|
const okMime =
|
|
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
file.mimetype === 'application/octet-stream';
|
|
if (okExt && okMime) return cb(null, true);
|
|
cb(new Error('Only .docx files are supported'));
|
|
},
|
|
});
|
|
|
|
function generatePdfName(length) {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
return result;
|
|
}
|
|
|
|
export default async function docxtopdf(req, res) {
|
|
const serverUrl = cloudServerUrl;
|
|
const parseAppKey = { 'X-Parse-Application-Id': serverAppId };
|
|
const masterKey = process.env.MASTER_KEY;
|
|
|
|
const masterHeader = { ...parseAppKey, 'X-Parse-Master-Key': masterKey };
|
|
const sessionHeader = { ...parseAppKey, 'X-Parse-Session-Token': req.headers['sessiontoken'] };
|
|
|
|
if (!req.file || !req.file.buffer) {
|
|
return res.status(400).json({ error: 'No file uploaded.' });
|
|
}
|
|
|
|
try {
|
|
// ---- Auth: current user ----
|
|
const userRes = await axios.get(`${serverUrl}/users/me`, { headers: sessionHeader });
|
|
const uploadedSizeBytes = req.file.size ?? req.file.buffer.length;
|
|
|
|
// ---- contracts_Users ----
|
|
const whereUser = JSON.stringify({
|
|
UserId: { __type: 'Pointer', className: '_User', objectId: userRes.data.objectId },
|
|
});
|
|
const resUser = await axios.get(
|
|
`${serverUrl}/classes/contracts_Users?where=${whereUser}&limit=1&include=TenantId`,
|
|
{ headers: masterHeader }
|
|
);
|
|
|
|
if (!resUser?.data?.results?.length) {
|
|
return res.status(403).json({ error: 'User not linked to tenant.' });
|
|
}
|
|
|
|
const tenantId = resUser.data.results[0]?.TenantId?.objectId;
|
|
if (!tenantId) {
|
|
return res.status(403).json({ error: 'Tenant not found for user.' });
|
|
}
|
|
|
|
// ---- DOCX -> PDF conversion with concurrency control and timeout ----
|
|
const fileName = `${generatePdfName(16)}.pdf`;
|
|
|
|
// Adjust timeout based on file size
|
|
const timeoutMs = uploadedSizeBytes > 10 * 1024 * 1024 ? 120_000 : 90_000;
|
|
|
|
// FIX: Increased timeout for large files, added nice priority
|
|
const pdfBuffer = await runWithLimit(async () => {
|
|
// Log for monitoring
|
|
console.log(
|
|
`[DOCX2PDF] Starting conversion, size: ${(uploadedSizeBytes / 1024 / 1024).toFixed(2)}MB, active: ${active}, queued: ${queue.length}`
|
|
);
|
|
|
|
const startTime = Date.now();
|
|
try {
|
|
const result = await withTimeout(
|
|
convertLibre(req.file.buffer, '.pdf', undefined),
|
|
timeoutMs,
|
|
'DOCX->PDF'
|
|
);
|
|
console.log(`[DOCX2PDF] Completed in ${Date.now() - startTime}ms`);
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`[DOCX2PDF] Failed after ${Date.now() - startTime}ms:`, error.message);
|
|
// Clean up on error
|
|
await killStuckProcesses();
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// ---- Upload PDF ----
|
|
const activeFileAdapter = resUser.data.results[0]?.TenantId?.ActiveFileAdapter;
|
|
let fileUrl;
|
|
if (activeFileAdapter) {
|
|
const params = {
|
|
fileBase64: Buffer.from(pdfBuffer).toString('base64'),
|
|
fileName,
|
|
id: activeFileAdapter,
|
|
};
|
|
const url = serverUrl + '/functions/savetofileadapter';
|
|
const headers = { 'Content-Type': 'application/json', ...sessionHeader };
|
|
const savetos3 = await axios.post(url, params, { headers });
|
|
fileUrl = savetos3?.data?.result?.url;
|
|
if (!fileUrl) throw new Error('No URL returned from file adapter');
|
|
} else {
|
|
const parsefile = await axios.post(`${serverUrl}/files/${fileName}`, pdfBuffer, {
|
|
headers: { ...masterHeader, 'Content-Type': 'application/pdf' },
|
|
});
|
|
const fileRes = getSecureUrl(parsefile.data.url);
|
|
fileUrl = fileRes.url;
|
|
}
|
|
|
|
return res.status(200).json({ message: 'success.', url: fileUrl });
|
|
} catch (err) {
|
|
// More specific error messages
|
|
let msg =
|
|
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
|
|
// Friendly message to the client
|
|
const message =
|
|
'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com';
|
|
|
|
if (msg.includes('timed out')) {
|
|
msg =
|
|
'Document conversion is taking too long. Please try a smaller file or contact support@opensignlabs.com';
|
|
} else if (msg.includes('too large') || msg.includes('size')) {
|
|
msg =
|
|
'File is too large to process. Please reduce the file size or contact support@opensignlabs.com';
|
|
} else {
|
|
msg = message;
|
|
}
|
|
console.error(`[DOCX2PDF] Error: ${msg}`);
|
|
return res.status(400).json({ error: message });
|
|
}
|
|
}
|