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文件和动态库文件')); } }; 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 } }); // 读取目录内容 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 { console.log('接收到的请求体:', req.body); console.log('接收到的文件:', req.files ? req.files.map(f => f.originalname) : '无文件'); console.log('文件详细信息:', req.files ? req.files.map(f => ({ originalname: f.originalname, webkitRelativePath: f.webkitRelativePath, fieldname: f.fieldname })) : '无文件'); 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); // 不阻止上传流程,只记录警告 } // 返回上传结果 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 }); } 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 }; console.log('动态库入口点信息:', entryPoint); return entryPoint; } } return null; } catch (error) { console.warn('获取动态库入口点信息失败:', error.message); return null; } } module.exports = router;