const express = require('express'); const router = express.Router(); const fsPromises = require('fs').promises; const fs = require('fs'); const path = require('path'); const multer = require('multer'); const { exec } = require('child_process'); const { promisify } = require('util'); const { getActualLogPath, getUploadPath, saveUploadedFile, isAllowedFileType } = require('../utils/file-utils'); const execAsync = promisify(exec); // 获取上传目录路径 const uploadPath = getUploadPath(); // 配置 multer 存储 const storage = multer.diskStorage({ destination: async function (req, file, cb) { try { const uploadPath = await getUploadPath(); cb(null, uploadPath); } catch (error) { cb(error); } }, filename: function (req, file, cb) { cb(null, file.originalname); } }); // 文件过滤器 const fileFilter = (req, file, cb) => { // 允许的文件类型 const allowedTypes = ['.csv', '.dcs']; if (isAllowedFileType(file.originalname, allowedTypes)) { cb(null, true); } else { cb(new Error('不支持的文件类型')); } }; // 数据包上传文件过滤器 const packageFileFilter = (req, file, cb) => { // 允许的文件类型:.h文件和包含.so的文件 const fileName = file.originalname.toLowerCase(); if (fileName.endsWith('.h') || fileName.includes('.so')) { cb(null, true); } else { cb(new Error('数据包只能包含.h文件和动态库文件')); } }; // ZIP文件上传过滤器 const zipFileFilter = (req, file, cb) => { // 只允许.zip文件 const fileName = file.originalname.toLowerCase(); if (fileName.endsWith('.zip')) { cb(null, true); } else { cb(new Error('只能上传.zip文件')); } }; const upload = multer({ storage: storage, fileFilter: fileFilter, limits: { fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB } }); // 数据包上传专用multer配置 const packageUpload = multer({ storage: storage, fileFilter: packageFileFilter, limits: { fileSize: 50 * 1024 * 1024 // 限制文件大小为50MB } }); // ZIP文件上传专用multer配置 const zipUpload = multer({ storage: storage, fileFilter: zipFileFilter, limits: { fileSize: 100 * 1024 * 1024 // 限制文件大小为100MB } }); // 读取目录内容 router.get('/readdir', async (req, res) => { try { // 获取实际的日志目录路径 const logDirPath = await getActualLogPath(); // 安全检查 if (!logDirPath) { return res.status(403).json({ error: '无权访问该目录' }); } // 检查目录是否存在 try { const stats = await fsPromises.stat(logDirPath); if (!stats.isDirectory()) { return res.status(400).json({ error: '指定的路径不是目录' }); } } catch (statError) { // 如果目录不存在,尝试创建它 if (statError.code === 'ENOENT') { try { await fsPromises.mkdir(logDirPath, { recursive: true }); } catch (mkdirError) { console.error('创建日志目录失败:', mkdirError); return res.status(500).json({ error: '创建日志目录失败' }); } } else { throw statError; } } // 读取目录内容 const files = await fsPromises.readdir(logDirPath); // 返回文件列表 res.json({ files }); } catch (error) { console.error('读取目录失败:', error); // 处理特定错误 if (error.code === 'ENOENT') { return res.status(404).json({ error: '目录不存在' }); } if (error.code === 'EACCES') { return res.status(403).json({ error: '没有权限访问目录' }); } res.status(500).json({ error: '读取目录失败', message: error.message }); } }); // 获取文件状态信息 router.get('/stat', async (req, res) => { try { const fileName = req.query.path; if (!fileName) { return res.status(400).json({ error: '未提供文件名' }); } // 获取实际的日志目录路径 const logDirPath = await getActualLogPath(); if (!logDirPath) { return res.status(403).json({ error: '无权访问该文件' }); } // 构建完整的文件路径 const filePath = path.join(logDirPath, fileName); // 安全检查:确保文件在日志目录内 if (!filePath.startsWith(logDirPath)) { return res.status(403).json({ error: '无权访问该文件' }); } // 获取文件状态 const stats = await fsPromises.stat(filePath); // 返回文件信息 res.json({ size: stats.size, isFile: stats.isFile(), isDirectory: stats.isDirectory(), created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, mtime: stats.mtime.toISOString() }); } catch (error) { console.error('获取文件状态失败:', error); // 处理特定错误 if (error.code === 'ENOENT') { return res.status(404).json({ error: '文件不存在' }); } if (error.code === 'EACCES') { return res.status(403).json({ error: '没有权限访问文件' }); } res.status(500).json({ error: '获取文件状态失败', message: error.message }); } }); // 读取文件内容 router.get('/readFile', async (req, res) => { try { const fileName = req.query.path; if (!fileName) { return res.status(400).json({ error: '未提供文件名' }); } // 获取实际的日志目录路径 const logDirPath = await getActualLogPath(); if (!logDirPath) { return res.status(403).json({ error: '无权访问该文件' }); } // 构建完整的文件路径 const filePath = path.join(logDirPath, fileName); // 安全检查:确保文件在日志目录内 if (!filePath.startsWith(logDirPath)) { return res.status(403).json({ error: '无权访问该文件' }); } // 只允许访问.log文件 if (!fileName.endsWith('.log')) { return res.status(403).json({ error: '只能访问日志文件' }); } // 获取文件状态 const stats = await fsPromises.stat(filePath); // 检查文件大小,限制读取过大的文件 const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB if (stats.size > MAX_FILE_SIZE) { return res.status(413).json({ error: '文件过大,无法读取' }); } // 读取文件内容 const content = await fsPromises.readFile(filePath, 'utf-8'); // 设置响应头 res.setHeader('Content-Type', 'text/plain; charset=utf-8'); // 返回文件内容 res.send(content); } catch (error) { console.error('读取文件内容失败:', error); // 处理特定错误 if (error.code === 'ENOENT') { return res.status(404).json({ error: '文件不存在' }); } if (error.code === 'EACCES') { return res.status(403).json({ error: '没有权限访问文件' }); } res.status(500).json({ error: '读取文件内容失败', message: error.message }); } }); // 获取上传文件列表 router.get('/upload-files', async (req, res) => { try { const uploadPath = await getUploadPath(); // 读取目录内容 const files = await fsPromises.readdir(uploadPath); // 获取每个文件的详细信息 const fileDetails = await Promise.all(files.map(async (fileName) => { const filePath = path.join(uploadPath, fileName); const stats = await fsPromises.stat(filePath); return { name: fileName, size: stats.size, created: stats.birthtime, modified: stats.mtime, path: filePath }; })); res.json({ files: fileDetails }); } catch (error) { console.error('获取上传文件列表失败:', error); res.status(500).json({ error: '获取上传文件列表失败', message: error.message }); } }); // 上传文件 router.post('/upload', upload.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: '未提供文件' }); } // 获取上传目录路径 const uploadPath = await getUploadPath(); // 确保上传目录存在 try { await fsPromises.access(uploadPath); } catch (error) { if (error.code === 'ENOENT') { // 如果目录不存在,创建它 await fsPromises.mkdir(uploadPath, { recursive: true }); } else { throw error; } } // 保存文件并获取最终路径 const finalPath = await saveUploadedFile(req.file); // 获取文件状态 const stats = await fsPromises.stat(finalPath); res.json({ success: true, file: { name: path.basename(finalPath), size: stats.size, path: finalPath, created: stats.birthtime, modified: stats.mtime } }); } catch (error) { console.error('文件上传失败:', error); res.status(500).json({ error: '文件上传失败', message: error.message }); } }); // 删除上传的文件 router.delete('/upload/:filename', async (req, res) => { try { const { filename } = req.params; const uploadPath = await getUploadPath(); const filePath = path.join(uploadPath, filename); // 安全检查:确保文件在上传目录内 if (!filePath.startsWith(uploadPath)) { return res.status(403).json({ error: '无权删除该文件' }); } // 删除文件 await fsPromises.unlink(filePath); res.json({ success: true, message: '文件删除成功' }); } catch (error) { console.error('删除文件失败:', error); if (error.code === 'ENOENT') { return res.status(404).json({ error: '文件不存在' }); } res.status(500).json({ error: '删除文件失败', message: error.message }); } }); // 验证CSV文件头部 router.get('/validate-csv-headers', async (req, res) => { try { const { filename } = req.query; if (!filename) { return res.status(400).json({ success: false, message: '未提供文件名' }); } // 获取上传目录路径 const uploadPath = await getUploadPath(); const filePath = path.join(uploadPath, filename); // 检查文件是否存在 try { await fsPromises.access(filePath); } catch (error) { return res.status(404).json({ success: false, message: '文件不存在' }); } // 只读取文件的第一行 const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' }); let firstLine = ''; try { for await (const chunk of fileStream) { const lines = chunk.split('\n'); firstLine = lines[0]; break; } } finally { // 确保关闭文件流 fileStream.destroy(); } // 解析CSV头部 const headers = firstLine.split(',').map(header => header.trim()); res.json({ success: true, headers: headers }); } catch (error) { console.error('验证CSV文件头部失败:', error); res.status(500).json({ success: false, message: '验证CSV文件头部失败: ' + error.message }); } }); // 读取CSV文件内容 router.post('/read', async (req, res) => { try { const { filename } = req.body; if (!filename) { return res.status(400).json({ success: false, message: '未提供文件名' }); } // 获取上传目录路径 const uploadPath = await getUploadPath(); const filePath = path.join(uploadPath, filename); // 检查文件是否存在 try { await fsPromises.access(filePath); } catch (error) { return res.status(404).json({ success: false, message: '文件不存在' }); } // 读取文件内容 const content = await fsPromises.readFile(filePath, 'utf8'); res.json({ success: true, data: content }); } catch (error) { console.error('读取CSV文件失败:', error); res.status(500).json({ success: false, message: '读取CSV文件失败: ' + error.message }); } }); // 文件下载 router.get('/download', async (req, res) => { try { const { filename } = req.query; if (!filename) { return res.status(400).json({ success: false, message: '未提供文件名' }); } // 获取上传目录路径 const uploadPath = await getUploadPath(); const filePath = path.join(uploadPath, filename); // 安全检查:确保文件在上传目录内 if (!filePath.startsWith(uploadPath)) { return res.status(403).json({ success: false, message: '无权访问该文件' }); } // 检查文件是否存在 try { await fsPromises.access(filePath); } catch (error) { return res.status(404).json({ success: false, message: '文件不存在' }); } // 设置响应头 res.setHeader('Content-Disposition', `attachment; filename=${encodeURIComponent(filename)}`); res.setHeader('Content-Type', 'application/octet-stream'); // 创建文件流并发送 const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); // 处理流错误 fileStream.on('error', (error) => { console.error('文件下载失败:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: '文件下载失败: ' + error.message }); } }); } catch (error) { console.error('文件下载失败:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: '文件下载失败: ' + error.message }); } } }); // 上传数据包文件夹 router.post('/upload-package', packageUpload.array('files'), async (req, res) => { try { const { confName } = req.body; const { folderName } = req.body; // 从前端获取文件夹名称 if (!confName) { return res.status(400).json({ success: false, message: '未提供构型名称' }); } if (!req.files || req.files.length === 0) { return res.status(400).json({ success: false, message: '未提供文件' }); } // 验证是否为文件夹上传 const hasFolderStructure = req.files.some(file => file.originalname.includes('/') || file.originalname.includes('\\') ); // 优先使用前端传递的文件夹名称,如果没有则从文件路径中提取 let packageName = null; if (folderName) { // 使用前端传递的文件夹名称 packageName = folderName; } else if (hasFolderStructure) { // 如果文件路径包含分隔符,从路径中提取 for (const file of req.files) { const pathParts = file.originalname.replace(/\\/g, '/').split('/'); if (pathParts.length > 1) { packageName = pathParts[0]; // 取第一级目录名作为数据包名称 break; } } } else { // 如果文件路径不包含分隔符,尝试从webkitRelativePath获取 if (req.files.length > 0 && req.files[0].webkitRelativePath) { const pathParts = req.files[0].webkitRelativePath.split('/'); if (pathParts.length > 1) { packageName = pathParts[0]; } } // 如果还是无法获取,使用默认名称 if (!packageName) { packageName = 'uploaded_package_' + Date.now(); } } if (!packageName) { return res.status(400).json({ success: false, message: '无法从上传的文件中提取文件夹名称' }); } // 验证文件夹内容 const fileTypes = { headerFiles: [], // .h文件 libraryFiles: [] // 动态库文件 }; req.files.forEach(file => { const fileName = path.basename(file.originalname); const ext = path.extname(fileName).toLowerCase(); if (ext === '.h') { fileTypes.headerFiles.push(file); } else if (fileName.toLowerCase().includes('.so')) { fileTypes.libraryFiles.push(file); } }); // 验证文件数量 if (fileTypes.headerFiles.length !== 1) { return res.status(400).json({ success: false, message: `文件夹必须包含且仅包含一个.h文件,当前包含 ${fileTypes.headerFiles.length} 个.h文件` }); } if (fileTypes.libraryFiles.length !== 1) { return res.status(400).json({ success: false, message: `文件夹必须包含且仅包含一个动态库文件,当前包含 ${fileTypes.libraryFiles.length} 个动态库文件` }); } // 验证总文件数量 const totalFiles = fileTypes.headerFiles.length + fileTypes.libraryFiles.length; if (totalFiles !== req.files.length) { return res.status(400).json({ success: false, message: `文件夹只能包含一个.h文件和一个动态库文件,当前包含 ${req.files.length} 个文件` }); } // 获取数据包路径 const { getPackagesPath } = require('../utils/file-utils'); const packagesPath = getPackagesPath(confName); if (!packagesPath) { return res.status(400).json({ success: false, message: '无法获取数据包路径' }); } // 创建目标数据包目录 const targetPackagePath = path.join(packagesPath, packageName); try { await fsPromises.mkdir(targetPackagePath, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { console.error('创建数据包目录失败:', error); return res.status(500).json({ success: false, message: '创建数据包目录失败: ' + error.message }); } } const uploadedFiles = []; const errors = []; // 处理每个上传的文件 for (const file of req.files) { try { // 从文件的相对路径中提取目标路径 let targetPath; if (file.originalname.includes('/') || file.originalname.includes('\\')) { // 如果文件名包含路径分隔符,说明是文件夹上传 const relativePath = file.originalname.replace(/\\/g, '/'); targetPath = path.join(targetPackagePath, relativePath); } else { // 单个文件,直接放在数据包根目录 targetPath = path.join(targetPackagePath, file.originalname); } // 确保目标目录存在 const targetDir = path.dirname(targetPath); try { await fsPromises.mkdir(targetDir, { recursive: true }); } catch (mkdirError) { if (mkdirError.code !== 'EEXIST') { throw mkdirError; } } // 移动文件到目标位置 await fsPromises.copyFile(file.path, targetPath); // 删除临时文件 try { await fsPromises.unlink(file.path); } catch (unlinkError) { console.warn('删除临时文件失败:', unlinkError); } // 获取文件信息 const stats = await fsPromises.stat(targetPath); const fileName = path.basename(targetPath); const ext = path.extname(fileName).toLowerCase(); uploadedFiles.push({ name: fileName, path: path.relative(targetPackagePath, targetPath), size: stats.size, created: stats.birthtime, modified: stats.mtime, type: ext === '.h' ? 'header' : 'library' }); } catch (fileError) { console.error(`处理文件 ${file.originalname} 失败:`, fileError); errors.push({ file: file.originalname, error: fileError.message }); } } // 对动态库执行nm -D命令获取入口点函数信息 let entryPointInfo = null; try { // 找到动态库文件路径 const libraryFile = uploadedFiles.find(file => file.type === 'library'); if (libraryFile) { const libraryPath = path.join(targetPackagePath, libraryFile.path); entryPointInfo = await getEntryPointInfo(libraryPath); } } catch (error) { console.warn('获取动态库入口点信息失败:', error.message); // 不阻止上传流程,只记录警告 } let memberNames = []; // 如果获取到了入口点函数信息,尝试从头文件中查找对应的结构体定义 if (entryPointInfo && entryPointInfo.paramType) { try { const headerFile = uploadedFiles.find(file => file.type === 'header'); if (headerFile) { const headerFilePath = path.join(targetPackagePath, headerFile.path); const structMemberNames = await findStructDefinition(headerFilePath, entryPointInfo.paramType); if (structMemberNames) { memberNames = structMemberNames; } } } catch (error) { console.warn('查找结构体定义失败:', error.message); } } // 返回上传结果 res.json({ success: true, message: `成功上传数据包 ${packageName},包含 ${fileTypes.headerFiles.length} 个头文件和 ${fileTypes.libraryFiles.length} 个动态库文件`, packagePath: packageName, // 相对于Packages目录的路径 headerFile: path.basename(fileTypes.headerFiles[0].originalname), libraryFile: path.basename(fileTypes.libraryFiles[0].originalname), entryPoint: entryPointInfo ? entryPointInfo.symbolName : null, paramType: entryPointInfo ? entryPointInfo.paramType : null, memberNames: memberNames }); } catch (error) { console.error('数据包上传失败:', error); res.status(500).json({ success: false, message: '数据包上传失败: ' + error.message }); } }); /** * 获取动态库入口点函数信息 * @param {string} libraryPath - 动态库文件路径 * @returns {Promise} 入口点信息对象或null */ async function getEntryPointInfo(libraryPath) { try { // 执行nm -D命令获取动态符号表 const { stdout } = await execAsync(`nm -D "${libraryPath}" | grep EntryPoint`); if (!stdout.trim()) { console.log('未找到EntryPoint相关的符号'); return null; } // 解析nm输出,只取第一个匹配的入口点 const lines = stdout.trim().split('\n'); if (lines.length > 0) { const line = lines[0]; // 只处理第一行 // nm输出格式: 地址 类型 符号名 const parts = line.trim().split(/\s+/); if (parts.length >= 3) { const symbolName = parts[2]; // 尝试从函数名中提取参数类型信息 // 函数名格式示例: _Z28SACSCWeightBalanceEntryPointP20ComacDataStructure_S let paramType = null; if (symbolName.startsWith('_Z')) { // 解析C++符号名格式 const match = symbolName.match(/_Z\d+([A-Za-z0-9_]+)P(\d+)([A-Za-z0-9_]+)/); if (match) { paramType = match[3]; } else { // 如果无法解析,使用原始符号名 paramType = symbolName; } } else { // C函数名,使用原始符号名 paramType = symbolName; } const entryPoint = { symbolName: symbolName, paramType: paramType }; return entryPoint; } } return null; } catch (error) { console.warn('获取动态库入口点信息失败:', error.message); return null; } } /** * 查找指定结构体的定义并返回其成员信息 * @param {string} headerFilePath - 头文件路径 * @param {string} structName - 要查找的结构体名称 * @returns {Promise>} 结构体成员信息数组,格式为"type memberName" */ async function findStructDefinition(headerFilePath, structName) { try { // 检查文件是否存在 await fsPromises.access(headerFilePath); // 读取头文件内容 const content = await fsPromises.readFile(headerFilePath, 'utf8'); // 移除注释,避免干扰解析 const contentWithoutComments = content // 移除单行注释 .replace(/\/\/.*$/gm, '') // 移除多行注释 .replace(/\/\*[\s\S]*?\*\//g, ''); // 构建查找结构体定义的正则表达式 // 匹配以下格式: // struct StructName { // struct StructName{ // typedef struct StructName { // typedef struct StructName{ const structPattern = new RegExp( `(?:typedef\\s+)?struct\\s+${structName}\\s*\\{([\\s\\S]*?)\\}\\s*;?`, 'g' ); const memberNames = []; let match; while ((match = structPattern.exec(contentWithoutComments)) !== null) { const structBody = match[1]; // 按行分割结构体内容 const lines = structBody.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); // 跳过空行和只包含分号的行 if (!trimmedLine || trimmedLine === ';') { continue; } // 匹配成员声明 // 基础类型: int, double, float, char, short, long, unsigned, etc. // 结构体指针: struct xxx_S* // 结构体: struct xxx_S const memberRegex = /^(?:const\s+)?(?:struct\s+([A-Za-z_][A-Za-z0-9_]*)\s*)?(?:unsigned\s+)?(?:long\s+)?(?:int\s+)?(?:char\s+)?(?:short\s+)?(?:float\s+)?(?:double\s+)?(?:void\s*)?(?:[A-Za-z_][A-Za-z0-9_]*\s*)?(\*?)\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^\]]*\])?\s*;?\s*$/; const memberMatch = trimmedLine.match(memberRegex); if (memberMatch) { const structType = memberMatch[1]; // 结构体类型名 const isPointer = memberMatch[2]; // 是否为指针 const memberName = memberMatch[3]; // 成员名 // 构建类型信息 let type = ''; if (structType) { // 结构体类型 type = `struct ${structType}${isPointer ? '*' : ''}`; } else { // 基础类型,需要从原始行中提取 const typeMatch = trimmedLine.match(/^(?:const\s+)?(?:unsigned\s+)?(?:long\s+)?(?:int\s+)?(?:char\s+)?(?:short\s+)?(?:float\s+)?(?:double\s+)?(?:void\s*)?(?:[A-Za-z_][A-Za-z0-9_]*\s*)?/); if (typeMatch) { type = typeMatch[0].trim() + (isPointer ? '*' : ''); } else { type = 'unknown'; } } // 拼接类型和成员名 const memberString = `${type} ${memberName}`; memberNames.push(memberString); } } } return memberNames; } catch (error) { console.warn(`查找结构体 ${structName} 定义失败:`, error.message); return []; } } /** * 根据构型名、头文件路径和结构体名获取结构体成员信息 */ router.post('/get-struct-members', async (req, res) => { try { const { confName, headerFilePath, structName } = req.body; if (!confName) { return res.status(400).json({ success: false, message: '未提供构型名称' }); } if (!headerFilePath) { return res.status(400).json({ success: false, message: '未提供头文件路径' }); } if (!structName) { return res.status(400).json({ success: false, message: '未提供结构体名称' }); } // 获取数据包路径 const { getPackagesPath } = require('../utils/file-utils'); const packagesPath = getPackagesPath(confName); if (!packagesPath) { return res.status(400).json({ success: false, message: '无法获取数据包路径' }); } // 组装完整的头文件路径 const fullHeaderFilePath = path.join(packagesPath, headerFilePath); // 调用findStructDefinition函数获取结构体成员信息 const memberNames = await findStructDefinition(fullHeaderFilePath, structName); res.json({ success: true, confName: confName, structName: structName, headerFilePath: headerFilePath, fullHeaderFilePath: fullHeaderFilePath, memberNames: memberNames, count: memberNames.length }); } catch (error) { console.error('获取结构体成员信息失败:', error); res.status(500).json({ success: false, message: '获取结构体成员信息失败: ' + error.message }); } }); // 上传ZIP文件 router.post('/upload-zip', zipUpload.single('file'), async (req, res) => { try { const { confName } = req.body; if (!confName) { return res.status(400).json({ success: false, message: '未提供构型名称' }); } if (!req.file) { return res.status(400).json({ success: false, message: '未提供ZIP文件' }); } // 获取XNCore环境变量 const xnCorePath = process.env.XNCore || ''; if (!xnCorePath) { return res.status(500).json({ success: false, message: 'XNCore环境变量未设置' }); } // 构建ModelProjects目录路径 const modelProjectsPath = path.join(xnCorePath, 'Configuration', confName, 'ModelProjects'); // 确保ModelProjects目录存在 try { await fsPromises.mkdir(modelProjectsPath, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { console.error('创建ModelProjects目录失败:', error); return res.status(500).json({ success: false, message: '创建ModelProjects目录失败: ' + error.message }); } } // 移动文件到ModelProjects目录 const finalPath = path.join(modelProjectsPath, req.file.originalname); try { await fsPromises.copyFile(req.file.path, finalPath); // 删除临时文件 try { await fsPromises.unlink(req.file.path); } catch (unlinkError) { console.warn('删除临时文件失败:', unlinkError); } } catch (copyError) { console.error('移动ZIP文件失败:', copyError); return res.status(500).json({ success: false, message: '移动ZIP文件失败: ' + copyError.message }); } // 获取文件状态 const stats = await fsPromises.stat(finalPath); res.json({ success: true, message: 'ZIP文件上传成功', confName: confName, file: { name: req.file.originalname, size: stats.size, path: finalPath, created: stats.birthtime, modified: stats.mtime } }); } catch (error) { console.error('ZIP文件上传失败:', error); res.status(500).json({ success: false, message: 'ZIP文件上传失败: ' + error.message }); } }); // 下载ZIP文件 router.get('/download-zip', async (req, res) => { try { const { filePath } = req.query; if (!filePath) { return res.status(400).json({ success: false, message: '未提供文件路径' }); } // 验证文件扩展名 if (!filePath.toLowerCase().endsWith('.zip')) { return res.status(400).json({ success: false, message: '只能下载ZIP文件' }); } // 检查文件是否存在 try { await fsPromises.access(filePath); } catch (error) { return res.status(404).json({ success: false, message: 'ZIP文件不存在' }); } // 获取文件名 const filename = path.basename(filePath); // 设置响应头 res.setHeader('Content-Disposition', `attachment; filename=${encodeURIComponent(filename)}`); res.setHeader('Content-Type', 'application/zip'); // 创建文件流并发送 const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); // 处理流错误 fileStream.on('error', (error) => { console.error('ZIP文件下载失败:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: 'ZIP文件下载失败: ' + error.message }); } }); } catch (error) { console.error('ZIP文件下载失败:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: 'ZIP文件下载失败: ' + error.message }); } } }); module.exports = router;