1111 lines
36 KiB
JavaScript
1111 lines
36 KiB
JavaScript
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<Object|null>} 入口点信息对象或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<Array<string>>} 结构体成员信息数组,格式为"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 a;
|
||
* double b;
|
||
* struct input_hydraulic_S input;
|
||
* struct input_hydraulic_S *input;
|
||
* float arr[10];
|
||
* const char *name;
|
||
*/
|
||
// 新正则:分组更清晰,支持struct和基础类型指针
|
||
const memberRegex = /^(?:const\s+)?(?:(struct)\s+([A-Za-z_][A-Za-z0-9_]*)\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 isStruct = memberMatch[1]; // struct关键字
|
||
const structType = memberMatch[2]; // 结构体类型名
|
||
const baseType = memberMatch[3] ? memberMatch[3].trim() : '';
|
||
const isPointer = memberMatch[4]; // *
|
||
const memberName = memberMatch[5]; // 变量名
|
||
|
||
let type = '';
|
||
if (isStruct && structType) {
|
||
type = `struct ${structType}${isPointer ? '*' : ''}`;
|
||
} else {
|
||
type = `${baseType}${isPointer ? '*' : ''}`.replace(/\s+/g, ' ').trim();
|
||
}
|
||
const memberString = `${type} ${memberName}`.trim();
|
||
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;
|