You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
6.0 KiB
167 lines
6.0 KiB
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));
|
|
// }
|
|
}
|
|
};
|