361 lines
10 KiB
JavaScript
361 lines
10 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 { getActualLogPath, getUploadPath, saveUploadedFile, isAllowedFileType } = require('../utils/file-utils');
|
|
|
|
// 获取上传目录路径
|
|
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'];
|
|
if (isAllowedFileType(file.originalname, allowedTypes)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('不支持的文件类型'));
|
|
}
|
|
};
|
|
|
|
const upload = multer({
|
|
storage: storage,
|
|
fileFilter: fileFilter,
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB
|
|
}
|
|
});
|
|
|
|
// 读取目录内容
|
|
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
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|