refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
const cron = require('node-cron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -61,15 +60,15 @@ function calculateNextRun(cronExpression) {
|
||||
const parts = cronExpression.split(' ');
|
||||
const minute = parseInt(parts[0]) || 0;
|
||||
const hour = parseInt(parts[1]) || 0;
|
||||
|
||||
|
||||
const now = new Date();
|
||||
const next = new Date(now);
|
||||
next.setHours(hour, minute, 0, 0);
|
||||
|
||||
|
||||
if (next <= now) {
|
||||
next.setDate(next.getDate() + 1);
|
||||
}
|
||||
|
||||
|
||||
return next.toLocaleString('zh-CN');
|
||||
} catch (error) {
|
||||
return '计算失败';
|
||||
@@ -89,7 +88,8 @@ function getFileSize(filePath) {
|
||||
}
|
||||
|
||||
function createAutoBackupTask(settings) {
|
||||
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } = settings;
|
||||
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } =
|
||||
settings;
|
||||
|
||||
if (!validateCronExpression(cronExpression)) {
|
||||
throw new Error('无效的 Cron 表达式');
|
||||
@@ -106,86 +106,91 @@ function createAutoBackupTask(settings) {
|
||||
}
|
||||
|
||||
console.log('创建新调度器...');
|
||||
const task = cron.schedule(cronExpression, async function() {
|
||||
console.log('');
|
||||
console.log('============================================');
|
||||
console.log('=== 自动备份任务触发 ===');
|
||||
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
|
||||
console.log('============================================');
|
||||
|
||||
let logId = null;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
||||
|
||||
console.log('创建备份日志...');
|
||||
const log = await createLogEntry({
|
||||
logType: 'auto',
|
||||
description: `${description} - ${timestamp}`,
|
||||
backupType: backupType,
|
||||
includeFiles: includeFiles,
|
||||
compressed: compress
|
||||
});
|
||||
logId = log ? log.id : null;
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'running');
|
||||
}
|
||||
|
||||
console.log('准备执行备份...');
|
||||
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
||||
|
||||
const result = await backupFunction({
|
||||
description: `${description} - ${timestamp}`,
|
||||
includeFiles,
|
||||
compress,
|
||||
autoClean: true,
|
||||
maxCount,
|
||||
maxAgeDays,
|
||||
});
|
||||
const task = cron.schedule(
|
||||
cronExpression,
|
||||
async function () {
|
||||
console.log('');
|
||||
console.log('============================================');
|
||||
console.log('=== 自动备份任务触发 ===');
|
||||
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
|
||||
console.log('============================================');
|
||||
|
||||
if (result) {
|
||||
console.log('自动备份完成:', result.filename);
|
||||
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
|
||||
|
||||
const fileSize = getFileSize(result.path);
|
||||
|
||||
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'success', {
|
||||
filename: result.filename,
|
||||
filePath: result.path,
|
||||
fileSize: fileSize,
|
||||
remoteUploads: uploadResults
|
||||
});
|
||||
}
|
||||
|
||||
console.log('============================================\n');
|
||||
} else {
|
||||
console.log('无数据变化,跳过备份');
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'success', {
|
||||
errorMessage: '无数据变化,跳过备份'
|
||||
});
|
||||
}
|
||||
console.log('============================================\n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动备份失败:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'failed', {
|
||||
errorMessage: error.message || '未知错误'
|
||||
let logId = null;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
||||
|
||||
console.log('创建备份日志...');
|
||||
const log = await createLogEntry({
|
||||
logType: 'auto',
|
||||
description: `${description} - ${timestamp}`,
|
||||
backupType: backupType,
|
||||
includeFiles: includeFiles,
|
||||
compressed: compress,
|
||||
});
|
||||
logId = log ? log.id : null;
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'running');
|
||||
}
|
||||
|
||||
console.log('准备执行备份...');
|
||||
const backupFunction =
|
||||
backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
||||
|
||||
const result = await backupFunction({
|
||||
description: `${description} - ${timestamp}`,
|
||||
includeFiles,
|
||||
compress,
|
||||
autoClean: true,
|
||||
maxCount,
|
||||
maxAgeDays,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log('自动备份完成:', result.filename);
|
||||
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
|
||||
|
||||
const fileSize = getFileSize(result.path);
|
||||
|
||||
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'success', {
|
||||
filename: result.filename,
|
||||
filePath: result.path,
|
||||
fileSize: fileSize,
|
||||
remoteUploads: uploadResults,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('============================================\n');
|
||||
} else {
|
||||
console.log('无数据变化,跳过备份');
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'success', {
|
||||
errorMessage: '无数据变化,跳过备份',
|
||||
});
|
||||
}
|
||||
console.log('============================================\n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动备份失败:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'failed', {
|
||||
errorMessage: error.message || '未知错误',
|
||||
});
|
||||
}
|
||||
|
||||
console.error('============================================\n');
|
||||
}
|
||||
|
||||
console.error('============================================\n');
|
||||
},
|
||||
{
|
||||
timezone: 'Asia/Shanghai',
|
||||
}
|
||||
}, {
|
||||
timezone: 'Asia/Shanghai'
|
||||
});
|
||||
);
|
||||
|
||||
schedulers.set('auto-backup', task);
|
||||
console.log('自动备份任务已成功创建并启动');
|
||||
@@ -231,14 +236,14 @@ function getAutoBackupStatus() {
|
||||
if (isActive && settings.enabled) {
|
||||
const now = new Date();
|
||||
const [minute, hour] = settings.cronExpression.split(' ').slice(0, 2);
|
||||
|
||||
|
||||
const next = new Date(now);
|
||||
next.setHours(parseInt(hour), parseInt(minute), 0, 0);
|
||||
|
||||
|
||||
if (next <= now) {
|
||||
next.setDate(next.getDate() + 1);
|
||||
}
|
||||
|
||||
|
||||
nextRun = next.toISOString();
|
||||
}
|
||||
|
||||
@@ -284,29 +289,29 @@ function updateAutoBackupSettings(newSettings) {
|
||||
|
||||
async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
const globalSettings = getGlobalSettings();
|
||||
|
||||
|
||||
if (!globalSettings.enabled || !globalSettings.uploadAfterBackup) {
|
||||
console.log('远端备份已禁用');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const enabledTargets = getEnabledTargets();
|
||||
|
||||
|
||||
if (enabledTargets.length === 0) {
|
||||
console.log('没有启用的远端备份目标');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const uploadResults = [];
|
||||
|
||||
|
||||
for (const target of enabledTargets) {
|
||||
try {
|
||||
console.log('开始上传到目标:' + target.name + ' (' + target.protocol + ')');
|
||||
|
||||
|
||||
const remotePath = (target.prefix || 'backups/') + filename;
|
||||
|
||||
|
||||
const result = await uploadToRemote(target, localFilePath, remotePath);
|
||||
|
||||
|
||||
uploadResults.push({
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
@@ -314,7 +319,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
|
||||
|
||||
console.log('上传到 ' + target.name + ' 成功');
|
||||
} catch (error) {
|
||||
console.error('上传到 ' + target.name + ' 失败:', error.message);
|
||||
@@ -327,7 +332,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const settings = getGlobalSettings();
|
||||
if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) {
|
||||
try {
|
||||
@@ -337,7 +342,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
console.error('删除本地备份文件失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return uploadResults;
|
||||
}
|
||||
|
||||
@@ -346,31 +351,33 @@ async function executeBackupNow(options = {}) {
|
||||
console.log('============================================');
|
||||
console.log('=== 手动触发备份 ===');
|
||||
console.log('============================================');
|
||||
|
||||
|
||||
let logId = null;
|
||||
|
||||
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const backupType = options.backupType || settings.backupType || 'full';
|
||||
|
||||
|
||||
console.log('创建备份日志...');
|
||||
const log = await createLogEntry({
|
||||
logType: 'manual',
|
||||
description: options.description || '手动备份',
|
||||
backupType: backupType,
|
||||
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
||||
compressed: options.compress !== undefined ? options.compress : settings.compress
|
||||
includeFiles:
|
||||
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
||||
compressed: options.compress !== undefined ? options.compress : settings.compress,
|
||||
});
|
||||
logId = log ? log.id : null;
|
||||
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'running');
|
||||
}
|
||||
|
||||
|
||||
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
|
||||
const result = await backupFunction({
|
||||
description: options.description || '手动备份',
|
||||
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
||||
includeFiles:
|
||||
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
|
||||
compress: options.compress !== undefined ? options.compress : settings.compress,
|
||||
autoClean: true,
|
||||
maxCount: settings.maxCount,
|
||||
@@ -378,36 +385,36 @@ async function executeBackupNow(options = {}) {
|
||||
});
|
||||
|
||||
console.log('手动备份完成:', result.filename);
|
||||
|
||||
|
||||
const fileSize = getFileSize(result.path);
|
||||
|
||||
|
||||
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'success', {
|
||||
filename: result.filename,
|
||||
filePath: result.path,
|
||||
fileSize: fileSize,
|
||||
remoteUploads: uploadResults
|
||||
remoteUploads: uploadResults,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
console.log('============================================\n');
|
||||
return {
|
||||
success: true,
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
remoteUploads: uploadResults,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('手动备份失败:', error);
|
||||
console.error('============================================\n');
|
||||
|
||||
|
||||
if (logId) {
|
||||
await updateLogStatus(logId, 'failed', {
|
||||
errorMessage: error.message || '未知错误'
|
||||
errorMessage: error.message || '未知错误',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
@@ -417,14 +424,14 @@ function initAutoBackup() {
|
||||
console.log('============================================');
|
||||
console.log('=== 初始化自动备份调度器 ===');
|
||||
console.log('============================================');
|
||||
|
||||
|
||||
const settings = loadSettings();
|
||||
|
||||
|
||||
console.log('当前设置:');
|
||||
console.log(' 启用:', settings.enabled ? '是' : '否');
|
||||
console.log(' Cron表达式:', settings.cronExpression);
|
||||
console.log(' 备份类型:', settings.backupType);
|
||||
|
||||
|
||||
if (settings.enabled) {
|
||||
startAutoBackup(settings);
|
||||
} else {
|
||||
@@ -447,4 +454,3 @@ module.exports = {
|
||||
executeBackupNow,
|
||||
initAutoBackup,
|
||||
};
|
||||
|
||||
|
||||
+105
-84
@@ -30,31 +30,31 @@ async function enableForeignKeyChecks() {
|
||||
|
||||
// 数据表名称中英文映射
|
||||
const TABLE_NAME_MAPPING = {
|
||||
'User': '用户',
|
||||
'Role': '角色',
|
||||
'UserRole': '用户角色关联',
|
||||
'Permission': '权限',
|
||||
'Room': '机房',
|
||||
'Rack': '机柜',
|
||||
'Device': '设备',
|
||||
'DeviceField': '设备自定义字段',
|
||||
'DevicePort': '设备端口',
|
||||
'NetworkCard': '网卡',
|
||||
'Cable': '线缆',
|
||||
'PendingDevice': '待入库设备',
|
||||
'FaultCategory': '故障分类',
|
||||
'Ticket': '工单',
|
||||
'TicketField': '工单自定义字段',
|
||||
'TicketOperationRecord': '工单操作记录',
|
||||
'ConsumableCategory': '耗材分类',
|
||||
'Consumable': '耗材',
|
||||
'ConsumableRecord': '耗材记录',
|
||||
'ConsumableLog': '耗材操作日志',
|
||||
'ConsumableLogArchive': '耗材操作日志归档',
|
||||
'InventoryPlan': '盘点计划',
|
||||
'InventoryTask': '盘点任务',
|
||||
'InventoryRecord': '盘点记录',
|
||||
'SystemSetting': '系统设置',
|
||||
User: '用户',
|
||||
Role: '角色',
|
||||
UserRole: '用户角色关联',
|
||||
Permission: '权限',
|
||||
Room: '机房',
|
||||
Rack: '机柜',
|
||||
Device: '设备',
|
||||
DeviceField: '设备自定义字段',
|
||||
DevicePort: '设备端口',
|
||||
NetworkCard: '网卡',
|
||||
Cable: '线缆',
|
||||
PendingDevice: '待入库设备',
|
||||
FaultCategory: '故障分类',
|
||||
Ticket: '工单',
|
||||
TicketField: '工单自定义字段',
|
||||
TicketOperationRecord: '工单操作记录',
|
||||
ConsumableCategory: '耗材分类',
|
||||
Consumable: '耗材',
|
||||
ConsumableRecord: '耗材记录',
|
||||
ConsumableLog: '耗材操作日志',
|
||||
ConsumableLogArchive: '耗材操作日志归档',
|
||||
InventoryPlan: '盘点计划',
|
||||
InventoryTask: '盘点任务',
|
||||
InventoryRecord: '盘点记录',
|
||||
SystemSetting: '系统设置',
|
||||
};
|
||||
|
||||
// 增量备份配置
|
||||
@@ -161,8 +161,13 @@ function getBackupPath() {
|
||||
*/
|
||||
function getLastBackupTime() {
|
||||
const backupDir = getBackupPath();
|
||||
const files = fs.readdirSync(backupDir)
|
||||
.filter(f => (f.startsWith('backup_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
||||
const files = fs
|
||||
.readdirSync(backupDir)
|
||||
.filter(
|
||||
f =>
|
||||
(f.startsWith('backup_') || f.startsWith('incremental_')) &&
|
||||
(f.endsWith('.json') || f.endsWith('.json.gz'))
|
||||
)
|
||||
.map(f => {
|
||||
const filePath = path.join(backupDir, f);
|
||||
return {
|
||||
@@ -193,7 +198,7 @@ async function collectIncrementalData(lastBackupTime) {
|
||||
|
||||
try {
|
||||
const Model = require(config.modelPath);
|
||||
|
||||
|
||||
// 查询自上次备份以来新增或更新的记录
|
||||
const newRecords = await Model.findAll({
|
||||
where: {
|
||||
@@ -246,7 +251,7 @@ async function collectAllData(tableNames = null) {
|
||||
const data = {};
|
||||
let totalRecords = 0;
|
||||
|
||||
const configsToProcess = tableNames
|
||||
const configsToProcess = tableNames
|
||||
? BACKUP_MODELS_CONFIG.filter(c => tableNames.includes(c.name))
|
||||
: BACKUP_MODELS_CONFIG;
|
||||
|
||||
@@ -397,7 +402,9 @@ async function createBackup(options = {}) {
|
||||
console.log('\n检查旧备份文件...');
|
||||
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
|
||||
if (cleanResult.deletedCount > 0) {
|
||||
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
|
||||
console.log(
|
||||
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
|
||||
);
|
||||
} else {
|
||||
console.log('无需清理旧备份');
|
||||
}
|
||||
@@ -440,10 +447,11 @@ async function createIncrementalBackup(options = {}) {
|
||||
}
|
||||
|
||||
console.log(`开始增量备份(上次备份时间:${lastBackupTime.toISOString()})...`);
|
||||
|
||||
|
||||
// 收集增量数据
|
||||
const { data: incrementalData, totalChangedRecords } = await collectIncrementalData(lastBackupTime);
|
||||
|
||||
const { data: incrementalData, totalChangedRecords } =
|
||||
await collectIncrementalData(lastBackupTime);
|
||||
|
||||
if (totalChangedRecords === 0) {
|
||||
console.log('自上次备份以来没有数据变化,跳过备份');
|
||||
return null;
|
||||
@@ -520,7 +528,9 @@ async function createIncrementalBackup(options = {}) {
|
||||
console.log('\n检查旧备份文件...');
|
||||
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
|
||||
if (cleanResult.deletedCount > 0) {
|
||||
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
|
||||
console.log(
|
||||
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
|
||||
);
|
||||
} else {
|
||||
console.log('无需清理旧备份');
|
||||
}
|
||||
@@ -541,7 +551,7 @@ async function createIncrementalBackup(options = {}) {
|
||||
|
||||
async function validateBackupFile(filePath, options = {}) {
|
||||
const { isCompressed: forceCompressed } = options;
|
||||
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { valid: false, error: '备份文件不存在' };
|
||||
}
|
||||
@@ -550,7 +560,7 @@ async function validateBackupFile(filePath, options = {}) {
|
||||
try {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const isCompressed = forceCompressed !== undefined ? forceCompressed : filePath.endsWith('.gz');
|
||||
|
||||
|
||||
if (isCompressed) {
|
||||
const decompressed = zlib.gunzipSync(buffer);
|
||||
backupData = JSON.parse(decompressed.toString('utf8'));
|
||||
@@ -638,28 +648,32 @@ async function validateBackupFile(filePath, options = {}) {
|
||||
avatars: backupData.files?.avatars?.length || 0,
|
||||
others: backupData.files?.others?.length || 0,
|
||||
total: (backupData.files?.avatars?.length || 0) + (backupData.files?.others?.length || 0),
|
||||
avatarList: backupData.files?.avatars?.map(f => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
})) || [],
|
||||
otherList: backupData.files?.others?.map(f => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
})) || [],
|
||||
avatarList:
|
||||
backupData.files?.avatars?.map(f => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
})) || [],
|
||||
otherList:
|
||||
backupData.files?.others?.map(f => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
})) || [],
|
||||
};
|
||||
|
||||
const metadata = isIncremental ? {
|
||||
tableCount: Object.keys(backupData.fullData || {}).length,
|
||||
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
|
||||
totalRecords,
|
||||
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
|
||||
fileCount: fileDetails.total,
|
||||
lastBackupTime: backupData.lastBackupTime,
|
||||
} : {
|
||||
tableCount: Object.keys(backupData.data).length,
|
||||
totalRecords,
|
||||
fileCount: fileDetails.total,
|
||||
};
|
||||
const metadata = isIncremental
|
||||
? {
|
||||
tableCount: Object.keys(backupData.fullData || {}).length,
|
||||
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
|
||||
totalRecords,
|
||||
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
|
||||
fileCount: fileDetails.total,
|
||||
lastBackupTime: backupData.lastBackupTime,
|
||||
}
|
||||
: {
|
||||
tableCount: Object.keys(backupData.data).length,
|
||||
totalRecords,
|
||||
fileCount: fileDetails.total,
|
||||
};
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
@@ -679,11 +693,7 @@ async function validateBackupFile(filePath, options = {}) {
|
||||
}
|
||||
|
||||
async function restoreData(backupData, options = {}) {
|
||||
const {
|
||||
overwriteExisting = true,
|
||||
skipTables = [],
|
||||
onProgress = () => {},
|
||||
} = options;
|
||||
const { overwriteExisting = true, skipTables = [], onProgress = () => {} } = options;
|
||||
|
||||
const results = {
|
||||
tablesRestored: 0,
|
||||
@@ -725,25 +735,31 @@ async function restoreData(backupData, options = {}) {
|
||||
|
||||
try {
|
||||
const Model = require(config.modelPath);
|
||||
|
||||
|
||||
if (overwriteExisting) {
|
||||
await Model.destroy({ where: {}, truncate: true });
|
||||
}
|
||||
|
||||
const processedRecords = tableData.map(record => {
|
||||
const processed = { ...record };
|
||||
|
||||
if (tableName === 'Device' && processed.customFields !== undefined && processed.customFields !== null) {
|
||||
|
||||
if (
|
||||
tableName === 'Device' &&
|
||||
processed.customFields !== undefined &&
|
||||
processed.customFields !== null
|
||||
) {
|
||||
if (typeof processed.customFields === 'string') {
|
||||
try {
|
||||
processed.customFields = JSON.parse(processed.customFields);
|
||||
} catch (e) {
|
||||
console.warn(`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`);
|
||||
console.warn(
|
||||
`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`
|
||||
);
|
||||
processed.customFields = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return processed;
|
||||
});
|
||||
|
||||
@@ -776,13 +792,13 @@ async function restoreData(backupData, options = {}) {
|
||||
|
||||
results.tablesRestored++;
|
||||
results.recordsRestored += insertedCount;
|
||||
|
||||
|
||||
results.tableDetails[tableName] = {
|
||||
recordCount: insertedCount,
|
||||
displayName: TABLE_NAME_MAPPING[tableName] || tableName,
|
||||
success: insertedCount > 0,
|
||||
};
|
||||
|
||||
|
||||
onProgress(tableName, 'restored', insertedCount);
|
||||
} catch (error) {
|
||||
results.errors.push({ table: tableName, error: error.message });
|
||||
@@ -816,7 +832,7 @@ async function restoreIncrementalData(incrementalData, options = {}) {
|
||||
for (const tableName of Object.keys(incrementalData)) {
|
||||
const tableIncrement = incrementalData[tableName];
|
||||
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
|
||||
|
||||
|
||||
if (!config) {
|
||||
results.errors.push({ table: tableName, error: '未找到模型配置' });
|
||||
continue;
|
||||
@@ -947,7 +963,7 @@ async function restoreBackup(filePath, options = {}) {
|
||||
console.log('\n读取备份数据...');
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const isCompressed = filePath.endsWith('.gz');
|
||||
|
||||
|
||||
let backupData;
|
||||
if (isCompressed) {
|
||||
console.log('解压备份文件...');
|
||||
@@ -992,31 +1008,34 @@ async function restoreBackup(filePath, options = {}) {
|
||||
filesRestored: fileResults.filesRestored,
|
||||
errors: dataResults.errors,
|
||||
tableDetails: dataResults.tableDetails, // 每个表的详细恢复信息
|
||||
fileDetails: backupData.files ? {
|
||||
avatars: backupData.files.avatars?.length || 0,
|
||||
others: backupData.files.others?.length || 0,
|
||||
} : null,
|
||||
fileDetails: backupData.files
|
||||
? {
|
||||
avatars: backupData.files.avatars?.length || 0,
|
||||
others: backupData.files.others?.length || 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanOldBackups(options = {}) {
|
||||
const {
|
||||
maxCount = 30,
|
||||
maxAgeDays = 90,
|
||||
dryRun = false,
|
||||
} = options;
|
||||
const { maxCount = 30, maxAgeDays = 90, dryRun = false } = options;
|
||||
|
||||
const backupPath = getBackupPath();
|
||||
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
return { deleted: [], kept: [], totalSize: 0, freedSize: 0 };
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
const files = fs.readdirSync(backupPath)
|
||||
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
||||
|
||||
const files = fs
|
||||
.readdirSync(backupPath)
|
||||
.filter(
|
||||
f =>
|
||||
(f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) &&
|
||||
(f.endsWith('.json') || f.endsWith('.json.gz'))
|
||||
)
|
||||
.map(f => {
|
||||
const filePath = path.join(backupPath, f);
|
||||
const stats = fs.statSync(filePath);
|
||||
@@ -1070,7 +1089,9 @@ function cleanOldBackups(options = {}) {
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (bytes === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
+42
-29
@@ -1,10 +1,9 @@
|
||||
|
||||
const BackupLog = require('../models/BackupLog');
|
||||
const fs = require('fs');
|
||||
|
||||
async function createLogEntry(options) {
|
||||
const { logType, description, backupType, includeFiles, compressed } = options;
|
||||
|
||||
|
||||
try {
|
||||
const log = await BackupLog.create({
|
||||
logType: logType || 'manual',
|
||||
@@ -13,7 +12,7 @@ async function createLogEntry(options) {
|
||||
backupType: backupType || 'full',
|
||||
includeFiles: includeFiles || false,
|
||||
compressed: compressed || false,
|
||||
startTime: new Date()
|
||||
startTime: new Date(),
|
||||
});
|
||||
return log;
|
||||
} catch (error) {
|
||||
@@ -25,30 +24,40 @@ async function createLogEntry(options) {
|
||||
async function updateLogStatus(logId, status, options = {}) {
|
||||
try {
|
||||
const updateData = { status };
|
||||
|
||||
|
||||
if (status === 'running') {
|
||||
updateData.startTime = new Date();
|
||||
}
|
||||
|
||||
|
||||
if (status === 'success' || status === 'failed') {
|
||||
updateData.endTime = new Date();
|
||||
|
||||
|
||||
const log = await BackupLog.findByPk(logId);
|
||||
if (log && log.startTime) {
|
||||
updateData.duration = new Date() - new Date(log.startTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.filename) updateData.filename = options.filename;
|
||||
if (options.filePath) updateData.filePath = options.filePath;
|
||||
if (options.fileSize) updateData.fileSize = options.fileSize;
|
||||
if (options.errorMessage) updateData.errorMessage = options.errorMessage;
|
||||
if (options.remoteUploads) updateData.remoteUploads = options.remoteUploads;
|
||||
|
||||
|
||||
if (options.filename) {
|
||||
updateData.filename = options.filename;
|
||||
}
|
||||
if (options.filePath) {
|
||||
updateData.filePath = options.filePath;
|
||||
}
|
||||
if (options.fileSize) {
|
||||
updateData.fileSize = options.fileSize;
|
||||
}
|
||||
if (options.errorMessage) {
|
||||
updateData.errorMessage = options.errorMessage;
|
||||
}
|
||||
if (options.remoteUploads) {
|
||||
updateData.remoteUploads = options.remoteUploads;
|
||||
}
|
||||
|
||||
await BackupLog.update(updateData, {
|
||||
where: { id: logId }
|
||||
where: { id: logId },
|
||||
});
|
||||
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('更新备份日志失败:', error);
|
||||
@@ -60,25 +69,29 @@ async function getBackupLogs(options = {}) {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, logType, status } = options;
|
||||
const where = {};
|
||||
|
||||
if (logType) where.logType = logType;
|
||||
if (status) where.status = status;
|
||||
|
||||
|
||||
if (logType) {
|
||||
where.logType = logType;
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
|
||||
const { count, rows } = await BackupLog.findAndCountAll({
|
||||
where,
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: pageSize,
|
||||
offset
|
||||
offset,
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
logs: rows,
|
||||
total: count,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
totalPages: Math.ceil(count / pageSize),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取备份日志失败:', error);
|
||||
@@ -99,15 +112,15 @@ async function deleteOldLogs(days = 30) {
|
||||
try {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
|
||||
|
||||
const deletedCount = await BackupLog.destroy({
|
||||
where: {
|
||||
createdAt: {
|
||||
[require('sequelize').Op.lt]: cutoffDate
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.lt]: cutoffDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
console.log(`删除了 ${deletedCount} 条旧备份日志`);
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
@@ -121,5 +134,5 @@ module.exports = {
|
||||
updateLogStatus,
|
||||
getBackupLogs,
|
||||
getBackupLogById,
|
||||
deleteOldLogs
|
||||
deleteOldLogs,
|
||||
};
|
||||
|
||||
@@ -10,25 +10,30 @@ const ensureLogDir = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatLogEntry = (entry) => {
|
||||
const formatLogEntry = entry => {
|
||||
const timestamp = new Date().toISOString();
|
||||
return JSON.stringify({
|
||||
timestamp,
|
||||
...entry,
|
||||
}) + '\n';
|
||||
return (
|
||||
JSON.stringify({
|
||||
timestamp,
|
||||
...entry,
|
||||
}) + '\n'
|
||||
);
|
||||
};
|
||||
|
||||
const logDangerousOperation = async (req, {
|
||||
operationType,
|
||||
operationName,
|
||||
targetType,
|
||||
targetId,
|
||||
targetName,
|
||||
beforeState,
|
||||
metadata = {},
|
||||
success = true,
|
||||
errorMessage = null,
|
||||
}) => {
|
||||
const logDangerousOperation = async (
|
||||
req,
|
||||
{
|
||||
operationType,
|
||||
operationName,
|
||||
targetType,
|
||||
targetId,
|
||||
targetName,
|
||||
beforeState,
|
||||
metadata = {},
|
||||
success = true,
|
||||
errorMessage = null,
|
||||
}
|
||||
) => {
|
||||
ensureLogDir();
|
||||
|
||||
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
|
||||
@@ -57,7 +62,9 @@ const logDangerousOperation = async (req, {
|
||||
|
||||
try {
|
||||
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
|
||||
console.log(`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`);
|
||||
console.log(
|
||||
`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to write dangerous operation log:', error);
|
||||
}
|
||||
@@ -74,13 +81,15 @@ const getDangerousOperationsLogs = (filters = {}) => {
|
||||
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
|
||||
let logs = lines.map(line => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}).filter(log => log !== null);
|
||||
let logs = lines
|
||||
.map(line => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(log => log !== null);
|
||||
|
||||
if (filters.operationType) {
|
||||
logs = logs.filter(log => log.operationType === filters.operationType);
|
||||
@@ -103,7 +112,9 @@ const getDangerousOperationsLogs = (filters = {}) => {
|
||||
}
|
||||
|
||||
if (filters.username) {
|
||||
logs = logs.filter(log => log.username?.toLowerCase().includes(filters.username.toLowerCase()));
|
||||
logs = logs.filter(log =>
|
||||
log.username?.toLowerCase().includes(filters.username.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.riskLevel) {
|
||||
|
||||
@@ -4,7 +4,7 @@ const checkDatabase = async () => {
|
||||
const result = {
|
||||
status: 'ok',
|
||||
type: dbDialect,
|
||||
message: '数据库连接正常'
|
||||
message: '数据库连接正常',
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -35,19 +35,19 @@ const checkCriticalConfig = () => {
|
||||
checks.push({
|
||||
key: 'JWT_SECRET',
|
||||
status: 'error',
|
||||
message: 'JWT_SECRET 未配置'
|
||||
message: 'JWT_SECRET 未配置',
|
||||
});
|
||||
} else if (jwtSecret.length < 32) {
|
||||
checks.push({
|
||||
key: 'JWT_SECRET',
|
||||
status: 'warning',
|
||||
message: 'JWT_SECRET 长度不足,建议至少 32 字符'
|
||||
message: 'JWT_SECRET 长度不足,建议至少 32 字符',
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
key: 'JWT_SECRET',
|
||||
status: 'ok',
|
||||
message: 'JWT_SECRET 已配置'
|
||||
message: 'JWT_SECRET 已配置',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ const checkCriticalConfig = () => {
|
||||
checks.push({
|
||||
key: 'PORT',
|
||||
status: port ? 'ok' : 'warning',
|
||||
message: port ? `服务端口: ${port}` : '使用默认端口 8000'
|
||||
message: port ? `服务端口: ${port}` : '使用默认端口 8000',
|
||||
});
|
||||
|
||||
const dbType = process.env.DB_TYPE || 'sqlite';
|
||||
checks.push({
|
||||
key: 'DB_TYPE',
|
||||
status: 'ok',
|
||||
message: `数据库类型: ${dbType}`
|
||||
message: `数据库类型: ${dbType}`,
|
||||
});
|
||||
|
||||
if (dbType === 'mysql') {
|
||||
@@ -72,7 +72,7 @@ const checkCriticalConfig = () => {
|
||||
checks.push({
|
||||
key: 'MYSQL_CONFIG',
|
||||
status: 'warning',
|
||||
message: 'MySQL 配置不完整'
|
||||
message: 'MySQL 配置不完整',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ const checkCriticalConfig = () => {
|
||||
|
||||
return {
|
||||
status: overallStatus,
|
||||
checks
|
||||
checks,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -93,17 +93,25 @@ const getSystemInfo = () => {
|
||||
const memUsage = process.memoryUsage();
|
||||
const uptime = process.uptime();
|
||||
|
||||
const formatUptime = (seconds) => {
|
||||
const formatUptime = seconds => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(`${days}天`);
|
||||
if (hours > 0) parts.push(`${hours}小时`);
|
||||
if (minutes > 0) parts.push(`${minutes}分钟`);
|
||||
if (secs > 0 || parts.length === 0) parts.push(`${secs}秒`);
|
||||
if (days > 0) {
|
||||
parts.push(`${days}天`);
|
||||
}
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours}小时`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes}分钟`);
|
||||
}
|
||||
if (secs > 0 || parts.length === 0) {
|
||||
parts.push(`${secs}秒`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
};
|
||||
@@ -112,13 +120,13 @@ const getSystemInfo = () => {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
memory: {
|
||||
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
||||
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
||||
rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
|
||||
unit: 'MB'
|
||||
heapUsed: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100,
|
||||
heapTotal: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100,
|
||||
rss: Math.round((memUsage.rss / 1024 / 1024) * 100) / 100,
|
||||
unit: 'MB',
|
||||
},
|
||||
uptime: formatUptime(uptime),
|
||||
uptimeSeconds: Math.round(uptime)
|
||||
uptimeSeconds: Math.round(uptime),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -129,7 +137,7 @@ const performHealthCheck = async () => {
|
||||
|
||||
const allChecks = [
|
||||
{ name: 'database', ...dbCheck },
|
||||
{ name: 'config', ...configCheck }
|
||||
{ name: 'config', ...configCheck },
|
||||
];
|
||||
|
||||
const overallStatus = allChecks.every(c => c.status === 'ok')
|
||||
@@ -143,10 +151,10 @@ const performHealthCheck = async () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
service: {
|
||||
name: 'IDC设备管理系统',
|
||||
version: '1.0.0'
|
||||
version: '1.0.0',
|
||||
},
|
||||
checks: allChecks,
|
||||
system: systemInfo
|
||||
system: systemInfo,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -154,5 +162,5 @@ module.exports = {
|
||||
performHealthCheck,
|
||||
checkDatabase,
|
||||
checkCriticalConfig,
|
||||
getSystemInfo
|
||||
getSystemInfo,
|
||||
};
|
||||
|
||||
@@ -4,26 +4,27 @@ const generateRecordId = () => {
|
||||
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
const getOperatorInfo = (req) => {
|
||||
const getOperatorInfo = req => {
|
||||
if (!req || !req.user) {
|
||||
return {
|
||||
operatorId: 'system',
|
||||
operatorName: '系统',
|
||||
operatorRole: null
|
||||
operatorRole: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
operatorId: req.user.userId || req.user.id || 'unknown',
|
||||
operatorName: req.user.realName || req.user.username || '未知用户',
|
||||
operatorRole: req.user.roleName || req.user.role || null
|
||||
operatorRole: req.user.roleName || req.user.role || null,
|
||||
};
|
||||
};
|
||||
|
||||
const getClientInfo = (req) => {
|
||||
const getClientInfo = req => {
|
||||
if (!req) {
|
||||
return { ipAddress: null, userAgent: null };
|
||||
}
|
||||
const ipAddress = req.headers['x-forwarded-for'] ||
|
||||
const ipAddress =
|
||||
req.headers['x-forwarded-for'] ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection?.remoteAddress ||
|
||||
req.ip ||
|
||||
@@ -39,7 +40,7 @@ const DEVICE_TYPE_MAP = {
|
||||
storage: '存储设备',
|
||||
firewall: '防火墙',
|
||||
loadbalancer: '负载均衡器',
|
||||
other: '其他设备'
|
||||
other: '其他设备',
|
||||
};
|
||||
|
||||
const generateDeviceDescription = (operation, device, options = {}) => {
|
||||
@@ -48,7 +49,7 @@ const generateDeviceDescription = (operation, device, options = {}) => {
|
||||
includePosition = true,
|
||||
includeSerial = true,
|
||||
includeIp = true,
|
||||
includeModel = true
|
||||
includeModel = true,
|
||||
} = options;
|
||||
|
||||
const deviceType = DEVICE_TYPE_MAP[device.type] || device.type || '设备';
|
||||
@@ -94,7 +95,7 @@ const buildDeviceMetadata = (device, extra = {}) => {
|
||||
position: device.position !== undefined ? device.position : null,
|
||||
roomId: device.roomId || null,
|
||||
roomName: device.roomName || null,
|
||||
...extra
|
||||
...extra,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -108,7 +109,7 @@ async function logOperation({
|
||||
afterState,
|
||||
result = 'success',
|
||||
req,
|
||||
metadata = {}
|
||||
metadata = {},
|
||||
}) {
|
||||
try {
|
||||
const operatorInfo = getOperatorInfo(req);
|
||||
@@ -129,14 +130,18 @@ async function logOperation({
|
||||
result,
|
||||
ipAddress: clientInfo.ipAddress,
|
||||
userAgent: clientInfo.userAgent,
|
||||
metadata
|
||||
metadata,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录操作日志失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function logDeviceOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
|
||||
async function logDeviceOperation(
|
||||
operationType,
|
||||
operationDescription,
|
||||
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
||||
) {
|
||||
return logOperation({
|
||||
module: 'device',
|
||||
operationType,
|
||||
@@ -147,11 +152,15 @@ async function logDeviceOperation(operationType, operationDescription, { targetI
|
||||
afterState,
|
||||
result,
|
||||
req,
|
||||
metadata
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
async function logUserOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
|
||||
async function logUserOperation(
|
||||
operationType,
|
||||
operationDescription,
|
||||
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
||||
) {
|
||||
return logOperation({
|
||||
module: 'user',
|
||||
operationType,
|
||||
@@ -162,11 +171,15 @@ async function logUserOperation(operationType, operationDescription, { targetId,
|
||||
afterState,
|
||||
result,
|
||||
req,
|
||||
metadata
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
async function logRoleOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
|
||||
async function logRoleOperation(
|
||||
operationType,
|
||||
operationDescription,
|
||||
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
|
||||
) {
|
||||
return logOperation({
|
||||
module: 'role',
|
||||
operationType,
|
||||
@@ -177,7 +190,7 @@ async function logRoleOperation(operationType, operationDescription, { targetId,
|
||||
afterState,
|
||||
result,
|
||||
req,
|
||||
metadata
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,5 +200,5 @@ module.exports = {
|
||||
logUserOperation,
|
||||
logRoleOperation,
|
||||
generateDeviceDescription,
|
||||
buildDeviceMetadata
|
||||
buildDeviceMetadata,
|
||||
};
|
||||
|
||||
@@ -28,9 +28,9 @@ const PROTOCOL_LABELS = {
|
||||
*/
|
||||
async function uploadViaFTP(config, localFilePath, remotePath) {
|
||||
const { Client } = require('basic-ftp');
|
||||
|
||||
|
||||
const client = new Client();
|
||||
|
||||
|
||||
try {
|
||||
await client.access({
|
||||
host: config.host,
|
||||
@@ -42,16 +42,16 @@ async function uploadViaFTP(config, localFilePath, remotePath) {
|
||||
rejectUnauthorized: config.rejectUnauthorized !== false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
await client.cd(config.rootPath || '/');
|
||||
|
||||
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '.') {
|
||||
await ensureRemoteDir(client, dirPath, 'ftp');
|
||||
}
|
||||
|
||||
|
||||
await client.uploadFrom(localFilePath, path.basename(remotePath));
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `FTP 上传成功:${remotePath}`,
|
||||
@@ -69,7 +69,7 @@ async function uploadViaFTP(config, localFilePath, remotePath) {
|
||||
async function uploadViaSFTP(config, localFilePath, remotePath) {
|
||||
const Client = require('ssh2-sftp-client');
|
||||
const client = new Client();
|
||||
|
||||
|
||||
try {
|
||||
await client.connect({
|
||||
host: config.host,
|
||||
@@ -80,14 +80,14 @@ async function uploadViaSFTP(config, localFilePath, remotePath) {
|
||||
passphrase: config.passphrase,
|
||||
readyTimeout: config.timeout || 10000,
|
||||
});
|
||||
|
||||
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '.') {
|
||||
await ensureRemoteDir(client, dirPath, 'sftp');
|
||||
}
|
||||
|
||||
|
||||
await client.put(fs.createReadStream(localFilePath), remotePath);
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `SFTP 上传成功:${remotePath}`,
|
||||
@@ -104,7 +104,7 @@ async function uploadViaSFTP(config, localFilePath, remotePath) {
|
||||
*/
|
||||
async function uploadViaWebDAV(config, localFilePath, remotePath) {
|
||||
const { createClient } = require('webdav');
|
||||
|
||||
|
||||
const client = createClient(config.url, {
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
@@ -113,18 +113,18 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
|
||||
'User-Agent': 'IDC-Backup-Client/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '/') {
|
||||
await ensureRemoteDir(client, dirPath, 'webdav');
|
||||
}
|
||||
|
||||
|
||||
const fileContent = fs.readFileSync(localFilePath);
|
||||
await client.putFileContents(remotePath, fileContent, {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `WebDAV 上传成功:${remotePath}`,
|
||||
@@ -139,22 +139,22 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
|
||||
*/
|
||||
async function uploadViaSMB(config, localFilePath, remotePath) {
|
||||
const smb = require('smb2');
|
||||
|
||||
|
||||
const client = new smb({
|
||||
share: `\\\\${config.host}\\${config.share}`,
|
||||
domain: config.domain || '',
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '.') {
|
||||
await ensureRemoteDir(client, dirPath, 'smb');
|
||||
}
|
||||
|
||||
|
||||
await client.writeFile(remotePath, fs.readFileSync(localFilePath));
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `SMB 上传成功:${remotePath}`,
|
||||
@@ -216,11 +216,11 @@ async function ensureRemoteDir(client, dirPath, protocol) {
|
||||
*/
|
||||
async function uploadToRemote(config, localFilePath, remotePath) {
|
||||
console.log(`开始上传到远端 [${config.protocol}]: ${remotePath}`);
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
let result;
|
||||
|
||||
|
||||
switch (config.protocol) {
|
||||
case PROTOCOL_TYPES.FTP:
|
||||
result = await uploadViaFTP(config, localFilePath, remotePath);
|
||||
@@ -237,12 +237,14 @@ async function uploadToRemote(config, localFilePath, remotePath) {
|
||||
default:
|
||||
throw new Error(`不支持的协议类型:${config.protocol}`);
|
||||
}
|
||||
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const fileSize = fs.statSync(localFilePath).size;
|
||||
|
||||
console.log(`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`);
|
||||
|
||||
|
||||
console.log(
|
||||
`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
protocol: config.protocol,
|
||||
@@ -258,22 +260,22 @@ async function uploadToRemote(config, localFilePath, remotePath) {
|
||||
*/
|
||||
async function testRemoteConnection(config) {
|
||||
console.log(`测试远端连接 [${config.protocol}]...`);
|
||||
|
||||
|
||||
try {
|
||||
const testContent = `IDC Backup Connection Test - ${new Date().toISOString()}`;
|
||||
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
|
||||
fs.writeFileSync(testFile, testContent);
|
||||
|
||||
|
||||
const testRemotePath = `test/backup-connection-test-${Date.now()}.txt`;
|
||||
|
||||
|
||||
const result = await uploadToRemote(config, testFile, testRemotePath);
|
||||
|
||||
|
||||
try {
|
||||
fs.unlinkSync(testFile);
|
||||
} catch (e) {
|
||||
console.warn('删除测试文件失败:', e.message);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '连接测试成功',
|
||||
|
||||
@@ -28,7 +28,9 @@ const DEFAULT_CONFIG = {
|
||||
* 加密敏感信息
|
||||
*/
|
||||
function encrypt(text) {
|
||||
if (!text) return '';
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
||||
const iv = crypto.randomBytes(16);
|
||||
@@ -42,7 +44,9 @@ function encrypt(text) {
|
||||
* 解密敏感信息
|
||||
*/
|
||||
function decrypt(text) {
|
||||
if (!text) return '';
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
||||
@@ -113,11 +117,11 @@ function getAllTargets() {
|
||||
function getTarget(id) {
|
||||
const config = loadConfig();
|
||||
const target = config.targets.find(t => t.id === id);
|
||||
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...target,
|
||||
password: target.password ? decrypt(target.password) : undefined,
|
||||
@@ -133,7 +137,7 @@ function getTarget(id) {
|
||||
*/
|
||||
function addTarget(targetData) {
|
||||
const config = loadConfig();
|
||||
|
||||
|
||||
const newTarget = {
|
||||
id: `target_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: targetData.name,
|
||||
@@ -142,7 +146,7 @@ function addTarget(targetData) {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
switch (targetData.protocol) {
|
||||
case PROTOCOL_TYPES.FTP:
|
||||
case PROTOCOL_TYPES.SFTP:
|
||||
@@ -155,7 +159,7 @@ function addTarget(targetData) {
|
||||
secure: targetData.secure,
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case PROTOCOL_TYPES.WEBDAV:
|
||||
Object.assign(newTarget, {
|
||||
url: targetData.url,
|
||||
@@ -165,7 +169,7 @@ function addTarget(targetData) {
|
||||
rootPath: targetData.rootPath || '/',
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case PROTOCOL_TYPES.SMB:
|
||||
Object.assign(newTarget, {
|
||||
host: targetData.host,
|
||||
@@ -176,17 +180,17 @@ function addTarget(targetData) {
|
||||
rootPath: targetData.rootPath || '/',
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
throw new Error(`不支持的协议类型:${targetData.protocol}`);
|
||||
}
|
||||
|
||||
|
||||
config.targets.push(newTarget);
|
||||
|
||||
|
||||
if (saveConfig(config)) {
|
||||
return newTarget;
|
||||
}
|
||||
|
||||
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
@@ -196,14 +200,14 @@ function addTarget(targetData) {
|
||||
function updateTarget(id, updates) {
|
||||
const config = loadConfig();
|
||||
const targetIndex = config.targets.findIndex(t => t.id === id);
|
||||
|
||||
|
||||
if (targetIndex === -1) {
|
||||
throw new Error('目标不存在');
|
||||
}
|
||||
|
||||
|
||||
const existingTarget = config.targets[targetIndex];
|
||||
const updatedTarget = { ...existingTarget, ...updates, updatedAt: new Date().toISOString() };
|
||||
|
||||
|
||||
if (updates.password) {
|
||||
updatedTarget.password = encrypt(updates.password);
|
||||
}
|
||||
@@ -219,13 +223,13 @@ function updateTarget(id, updates) {
|
||||
if (updates.passphrase) {
|
||||
updatedTarget.passphrase = encrypt(updates.passphrase);
|
||||
}
|
||||
|
||||
|
||||
config.targets[targetIndex] = updatedTarget;
|
||||
|
||||
|
||||
if (saveConfig(config)) {
|
||||
return updatedTarget;
|
||||
}
|
||||
|
||||
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
@@ -235,16 +239,16 @@ function updateTarget(id, updates) {
|
||||
function deleteTarget(id) {
|
||||
const config = loadConfig();
|
||||
const initialLength = config.targets.length;
|
||||
|
||||
|
||||
config.targets = config.targets.filter(t => t.id !== id);
|
||||
|
||||
|
||||
if (config.targets.length < initialLength) {
|
||||
if (saveConfig(config)) {
|
||||
return true;
|
||||
}
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -262,11 +266,11 @@ function getGlobalSettings() {
|
||||
function updateGlobalSettings(settings) {
|
||||
const config = loadConfig();
|
||||
config.globalSettings = { ...config.globalSettings, ...settings };
|
||||
|
||||
|
||||
if (saveConfig(config)) {
|
||||
return config.globalSettings;
|
||||
}
|
||||
|
||||
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user