const { spawn } = require('child_process'); const fsPromises = require('fs').promises; const fs = require('fs'); const path = require('path'); const os = require('os'); const { convertPdfToImages } = require('../utils/pdfToImages'); const soffice = process.env.SOFFICE_PATH || 'soffice'; module.exports.docToDocx = async (ctx, next) => { let tempDir = null; let outputPath = null; try { if (!ctx.file) { ctx.status = 400; ctx.body = { message: '请上传一个 .doc 文件' }; return; } const file = ctx.file; // { fieldname, originalname, encoding, mimetype, destination, filename, path, size } // 校验扩展名 if (!file.originalname.toLowerCase().endsWith('.doc')) { ctx.status = 400; ctx.body = { message: '仅支持 .doc 格式的文件' }; return; } // 创建专属临时目录(避免并发冲突) tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'doc2docx-')); const inputPath = path.join(tempDir, file.originalname); const outputDir = tempDir; // 移动 multer 生成的临时文件到我们的目录(可选,也可直接用 file.path) await fsPromises.rename(file.path, inputPath); // 调用 LibreOffice 转换 const args = ['--headless', '--convert-to', 'docx', '--outdir', outputDir, inputPath]; await new Promise((resolve, reject) => { const child = spawn(soffice, args, { stdio: 'ignore', windowsHide: true }); child.on('error', reject); child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Exit code ${code}`)))); }); // 构造输出路径 const baseName = path.basename(file.originalname, '.doc'); outputPath = path.join(outputDir, `${baseName}.docx`); await fsPromises.access(outputPath); // 确保文件存在 // 返回文件流 ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(baseName)}.docx"`); ctx.body = fs.createReadStream(outputPath); // 清理 ctx.res.on('finish', () => fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => { })); } catch (error) { ctx.logger?.error('docToDocx error:', error); ctx.status = 400; ctx.body = { message: error.message || '文件转换失败' }; // 出错时立即清理 if (tempDir) fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => { }); } }; // ========== 新增:检测 pdftoppm 是否可用 ========== function checkPopplerAvailable() { return new Promise((resolve) => { const { spawn } = require('child_process'); let found = false; const child = spawn('pdftoppm', ['-h'], { timeout: 2000 }); child.on('error', (err) => { console.warn('[PDF2IMG] pdftoppm not found or failed to start:', err.message); resolve(false); }); child.on('close', (code) => { if (code === 0) { console.log('[PDF2IMG] ✅ Poppler (pdftoppm) is available'); resolve(true); } else { console.warn(`[PDF2IMG] pdftoppm exited with code ${code}`); resolve(false); } }); }); } module.exports.pdfToImage = async (ctx, next) => { let tempDir = null; // 记录请求开始 const requestId = Math.random().toString(36).substring(2, 10); const logPrefix = `[REQ-${requestId}]`; try { console.log(`${logPrefix} 📥 Received PDF upload request`); if (!ctx.file) { ctx.status = 400; ctx.body = { message: '请上传一个 .pdf 文件' }; return; } const file = ctx.file; console.log(`${logPrefix} Original filename: ${file.originalname}`); console.log(`${logPrefix} Temp file path: ${file.path}`); console.log(`${logPrefix} File size: ${file.size} bytes`); if (!file.originalname.toLowerCase().endsWith('.pdf')) { ctx.status = 400; ctx.body = { message: '仅支持 .pdf 格式的文件' }; return; } // 创建临时目录 tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'pdf2image-')); console.log(`${logPrefix} Using temp dir: ${tempDir}`); const safeFilename = `input_${Date.now()}.pdf`; const inputPath = path.join(tempDir, safeFilename); const outputDir = tempDir; // 复制文件(关键修复) console.log(`${logPrefix} Copying file to: ${inputPath}`); await fsPromises.copyFile(file.path, inputPath); await fsPromises.unlink(file.path); // 清理 multer 临时文件 console.log(`${logPrefix} Multer temp file deleted`); // 可选:调试时保留临时文件(通过环境变量控制) const keepTemp = process.env.KEEP_TEMP_FILES === '1'; if (keepTemp) { console.warn(`${logPrefix} ⚠️ KEEP_TEMP_FILES=1: Will NOT clean up ${tempDir}`); } // 检查 Poppler(仅一次,可选) // await checkPopplerAvailable(); // 取消注释可验证 // 转换 const images = await convertPdfToImages(inputPath, outputDir, 150); // 先用 150 DPI 调试 ctx.status = 200; ctx.body = { message: '文件转换成功', data: { images } }; } catch (error) { // 统一日志 const errMsg = error.message || 'Unknown error'; ctx.logger?.error(`${logPrefix} pdfToImage failed: ${errMsg}`, { error }); console.error(`${logPrefix} 🚨 HANDLED ERROR:`, errMsg); ctx.status = 400; ctx.body = { message: errMsg }; } finally { // 清理临时目录(除非调试) // if (tempDir && process.env.KEEP_TEMP_FILES !== '1') { // fsPromises.rm(tempDir, { recursive: true, force: true }) // .then(() => console.log(`${logPrefix} 🧹 Cleaned up temp dir: ${tempDir}`)) // .catch(err => console.warn(`${logPrefix} Failed to clean temp dir:`, err.message)); // } } };