2026-03-13 15:13:58 +08:00
|
|
|
|
const express = require('express');
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
const zlib = require('zlib');
|
2026-03-17 11:19:15 +08:00
|
|
|
|
const { authMiddleware } = require('../middleware/auth');
|
2026-03-13 15:13:58 +08:00
|
|
|
|
const {
|
|
|
|
|
|
getBackupPath,
|
|
|
|
|
|
ensureBackupDir,
|
|
|
|
|
|
createBackup,
|
|
|
|
|
|
validateBackupFile,
|
|
|
|
|
|
restoreBackup,
|
|
|
|
|
|
cleanOldBackups,
|
|
|
|
|
|
} = require('../utils/backup');
|
|
|
|
|
|
const {
|
|
|
|
|
|
loadSettings,
|
|
|
|
|
|
saveSettings,
|
|
|
|
|
|
validateCronExpression,
|
|
|
|
|
|
timeToCron,
|
|
|
|
|
|
startAutoBackup,
|
|
|
|
|
|
stopAutoBackup,
|
|
|
|
|
|
getAutoBackupStatus,
|
|
|
|
|
|
updateAutoBackupSettings,
|
|
|
|
|
|
executeBackupNow,
|
|
|
|
|
|
} = require('../utils/autoBackupScheduler');
|
2026-03-16 17:07:20 +08:00
|
|
|
|
const {
|
|
|
|
|
|
getBackupLogs,
|
|
|
|
|
|
getBackupLogById,
|
|
|
|
|
|
deleteOldLogs,
|
|
|
|
|
|
} = require('../utils/backupLog');
|
2026-03-13 22:11:34 +08:00
|
|
|
|
const {
|
|
|
|
|
|
getAllTargets,
|
|
|
|
|
|
getTarget,
|
|
|
|
|
|
addTarget,
|
|
|
|
|
|
updateTarget,
|
|
|
|
|
|
deleteTarget,
|
|
|
|
|
|
getGlobalSettings,
|
|
|
|
|
|
updateGlobalSettings,
|
2026-03-16 17:07:20 +08:00
|
|
|
|
getEnabledTargets,
|
2026-03-13 22:11:34 +08:00
|
|
|
|
} = require('../utils/remoteBackupConfig');
|
|
|
|
|
|
const { testRemoteConnection, PROTOCOL_TYPES, PROTOCOL_LABELS } = require('../utils/remoteBackup');
|
2026-03-13 15:13:58 +08:00
|
|
|
|
|
|
|
|
|
|
const tempDir = path.join(__dirname, '..', 'temp');
|
|
|
|
|
|
if (!fs.existsSync(tempDir)) {
|
|
|
|
|
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.post('/', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { description = '', includeFiles = true } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('开始创建备份...');
|
|
|
|
|
|
const result = await createBackup({
|
|
|
|
|
|
description,
|
|
|
|
|
|
includeFiles: includeFiles !== false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '备份创建成功',
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建备份失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '创建备份失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.get('/list', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(backupPath)) {
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { backups: [], total: 0 },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const files = fs.readdirSync(backupPath)
|
|
|
|
|
|
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
|
|
|
|
|
.map(async f => {
|
|
|
|
|
|
const filePath = path.join(backupPath, f);
|
|
|
|
|
|
const stats = fs.statSync(filePath);
|
|
|
|
|
|
const isCompressed = f.endsWith('.gz');
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试从文件内容中提取元数据
|
|
|
|
|
|
let metadata = {
|
|
|
|
|
|
filename: f,
|
|
|
|
|
|
size: stats.size,
|
|
|
|
|
|
compressed: isCompressed,
|
|
|
|
|
|
createdAt: stats.birthtime,
|
|
|
|
|
|
modifiedAt: stats.mtime,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 读取文件头部的元数据信息
|
|
|
|
|
|
let content;
|
|
|
|
|
|
if (isCompressed) {
|
|
|
|
|
|
const compressed = fs.readFileSync(filePath);
|
|
|
|
|
|
content = zlib.gunzipSync(compressed).toString('utf8');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
content = fs.readFileSync(filePath, 'utf8');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const backupData = JSON.parse(content);
|
|
|
|
|
|
|
|
|
|
|
|
// 提取关键元数据
|
|
|
|
|
|
metadata.description = backupData.description || '';
|
|
|
|
|
|
metadata.backupType = backupData.backupType || 'full';
|
|
|
|
|
|
metadata.version = backupData.version || '1.0.0';
|
|
|
|
|
|
metadata.timestamp = backupData.timestamp;
|
|
|
|
|
|
metadata.checksum = backupData.checksum;
|
|
|
|
|
|
metadata.metadata = backupData.metadata;
|
|
|
|
|
|
metadata.systemInfo = backupData.systemInfo;
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否为上传的文件(通过文件名判断)
|
|
|
|
|
|
metadata.isUploaded = f.startsWith('uploaded_');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 如果读取失败,标记为无效文件
|
|
|
|
|
|
metadata.invalid = true;
|
|
|
|
|
|
metadata.error = '无法读取文件内容';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const resolvedFiles = await Promise.all(files);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { backups: resolvedFiles, total: resolvedFiles.length },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取备份列表失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取备份列表失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.get('/validate/:filename', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { filename } = req.params;
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
const filePath = path.join(backupPath, filename);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份文件不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const validation = await validateBackupFile(filePath);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: validation,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('验证备份文件失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '验证备份文件失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-17 11:19:15 +08:00
|
|
|
|
router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
|
|
|
|
|
const { filename } = req.params;
|
|
|
|
|
|
const options = req.query.options ? JSON.parse(req.query.options) : {};
|
|
|
|
|
|
|
|
|
|
|
|
if (!filename) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供备份文件名',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
const filePath = path.join(backupPath, filename);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份文件不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
|
|
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
|
|
|
|
res.setHeader('Connection', 'keep-alive');
|
|
|
|
|
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
|
|
|
|
|
|
|
|
|
|
const sendProgress = (data) => {
|
|
|
|
|
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log(`开始恢复备份: ${filename}`);
|
|
|
|
|
|
sendProgress({ stage: 'start', message: '正在验证备份文件...', progress: 5 });
|
|
|
|
|
|
|
|
|
|
|
|
const validation = await validateBackupFile(filePath);
|
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
|
sendProgress({ stage: 'error', message: `备份文件验证失败: ${validation.error}`, progress: 0 });
|
|
|
|
|
|
res.end();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sendProgress({ stage: 'validate', message: '备份文件验证通过', progress: 10, metadata: validation.metadata });
|
|
|
|
|
|
|
|
|
|
|
|
const buffer = fs.readFileSync(filePath);
|
|
|
|
|
|
const isCompressed = filePath.endsWith('.gz');
|
|
|
|
|
|
|
|
|
|
|
|
let backupData;
|
|
|
|
|
|
if (isCompressed) {
|
|
|
|
|
|
sendProgress({ stage: 'decompress', message: '正在解压备份文件...', progress: 15 });
|
|
|
|
|
|
const decompressed = zlib.gunzipSync(buffer);
|
|
|
|
|
|
backupData = JSON.parse(decompressed.toString('utf8'));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
backupData = JSON.parse(buffer.toString('utf8'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sendProgress({ stage: 'parse', message: '正在解析备份数据...', progress: 20 });
|
|
|
|
|
|
|
|
|
|
|
|
const totalTables = require('../utils/backup').RESTORE_ORDER.length;
|
|
|
|
|
|
let processedTables = 0;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await restoreBackup(filePath, {
|
|
|
|
|
|
overwriteExisting: options.overwriteExisting !== false,
|
|
|
|
|
|
skipTables: options.skipTables || [],
|
|
|
|
|
|
skipFiles: options.skipFiles === true,
|
|
|
|
|
|
onProgress: (tableName, status, count) => {
|
|
|
|
|
|
processedTables++;
|
|
|
|
|
|
const progress = 20 + Math.floor((processedTables / totalTables) * 70);
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
'restored': '已恢复',
|
|
|
|
|
|
'skipped': '已跳过',
|
|
|
|
|
|
'empty': '无数据',
|
|
|
|
|
|
'error': '错误',
|
|
|
|
|
|
};
|
|
|
|
|
|
sendProgress({
|
|
|
|
|
|
stage: 'restore',
|
|
|
|
|
|
message: `正在恢复: ${tableName} (${statusMap[status] || status}${count ? ` - ${count} 条` : ''})`,
|
|
|
|
|
|
progress,
|
|
|
|
|
|
currentTable: tableName,
|
|
|
|
|
|
status,
|
|
|
|
|
|
count,
|
|
|
|
|
|
processedTables,
|
|
|
|
|
|
totalTables,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
sendProgress({
|
|
|
|
|
|
stage: 'complete',
|
|
|
|
|
|
message: '恢复完成!',
|
|
|
|
|
|
progress: 100,
|
|
|
|
|
|
result: {
|
|
|
|
|
|
tablesRestored: result.tablesRestored,
|
|
|
|
|
|
recordsRestored: result.recordsRestored,
|
|
|
|
|
|
filesRestored: result.filesRestored,
|
|
|
|
|
|
restoredAt: result.restoredAt,
|
|
|
|
|
|
tableDetails: result.tableDetails,
|
|
|
|
|
|
fileDetails: result.fileDetails,
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
res.end();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('恢复备份失败:', error);
|
|
|
|
|
|
sendProgress({ stage: 'error', message: `恢复失败: ${error.message}`, progress: 0 });
|
|
|
|
|
|
res.end();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-13 15:13:58 +08:00
|
|
|
|
router.post('/restore', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { filename, options = {} } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!filename) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供备份文件名',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
const filePath = path.join(backupPath, filename);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份文件不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`开始恢复备份: ${filename}`);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await restoreBackup(filePath, {
|
|
|
|
|
|
overwriteExisting: options.overwriteExisting !== false,
|
|
|
|
|
|
skipTables: options.skipTables || [],
|
|
|
|
|
|
skipFiles: options.skipFiles === true,
|
|
|
|
|
|
onProgress: (tableName, status, count) => {
|
|
|
|
|
|
console.log(` ${tableName}: ${status}${count ? ` (${count})` : ''}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '数据恢复成功',
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('恢复备份失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '恢复备份失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.post('/upload', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!req.files || !req.files.backup) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请上传备份文件',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const backupFile = req.files.backup;
|
|
|
|
|
|
const originalName = backupFile.name || '';
|
|
|
|
|
|
const nameLower = originalName.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
// 验证文件类型
|
|
|
|
|
|
if (!nameLower.endsWith('.json') && !nameLower.endsWith('.gz')) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '只支持 JSON 或 GZ 格式的备份文件',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isCompressed = nameLower.endsWith('.gz');
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`上传备份文件: ${originalName}, 压缩: ${isCompressed}, 大小: ${backupFile.size}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到临时文件
|
|
|
|
|
|
const tempFilename = `upload_${Date.now()}`;
|
|
|
|
|
|
const tempPath = path.join(tempDir, tempFilename);
|
|
|
|
|
|
|
|
|
|
|
|
await backupFile.mv(tempPath);
|
|
|
|
|
|
|
|
|
|
|
|
const validation = await validateBackupFile(tempPath, { isCompressed });
|
|
|
|
|
|
|
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
|
fs.unlinkSync(tempPath);
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `备份文件验证失败: ${validation.error}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
|
|
|
|
const ext = isCompressed ? '.json.gz' : '.json';
|
|
|
|
|
|
const newFilename = `uploaded_${timestamp}${ext}`;
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
ensureBackupDir(backupPath);
|
|
|
|
|
|
const targetPath = path.join(backupPath, newFilename);
|
|
|
|
|
|
|
|
|
|
|
|
fs.renameSync(tempPath, targetPath);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '备份文件上传成功',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
filename: newFilename,
|
|
|
|
|
|
validation,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('上传备份文件失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '上传备份文件失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.get('/download/:filename', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { filename } = req.params;
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
const filePath = path.join(backupPath, filename);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份文件不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.download(filePath, filename, (err) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
console.error('下载备份文件失败:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('下载备份文件失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '下载备份文件失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.delete('/:filename', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { filename } = req.params;
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
const filePath = path.join(backupPath, filename);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份文件不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fs.unlinkSync(filePath);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '备份文件已删除',
|
|
|
|
|
|
data: { filename },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除备份文件失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '删除备份文件失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.get('/info', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
let totalSize = 0;
|
|
|
|
|
|
let backupCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (fs.existsSync(backupPath)) {
|
|
|
|
|
|
const files = fs.readdirSync(backupPath).filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
|
|
|
|
|
|
backupCount = files.length;
|
|
|
|
|
|
files.forEach(f => {
|
|
|
|
|
|
const stats = fs.statSync(path.join(backupPath, f));
|
|
|
|
|
|
totalSize += stats.size;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
backupPath,
|
|
|
|
|
|
backupCount,
|
|
|
|
|
|
totalSize,
|
|
|
|
|
|
totalSizeFormatted: formatBytes(totalSize),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取备份信息失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取备份信息失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function formatBytes(bytes) {
|
|
|
|
|
|
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));
|
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.post('/clean', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { maxCount = 30, maxAgeDays = 90, dryRun = false } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
const result = cleanOldBackups({ maxCount, maxAgeDays, dryRun });
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: dryRun ? '预览完成' : '清理完成',
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('清理备份失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '清理备份失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
router.use((error, req, res, next) => {
|
|
|
|
|
|
console.error('路由错误:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '服务器内部错误',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 自动备份接口 ====================
|
|
|
|
|
|
|
|
|
|
|
|
// 获取自动备份状态
|
|
|
|
|
|
router.get('/auto/status', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const status = getAutoBackupStatus();
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: status,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取自动备份状态失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取自动备份状态失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新自动备份设置
|
|
|
|
|
|
router.post('/auto/settings', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const {
|
|
|
|
|
|
enabled,
|
|
|
|
|
|
hour,
|
|
|
|
|
|
minute,
|
|
|
|
|
|
cronExpression,
|
|
|
|
|
|
description,
|
|
|
|
|
|
includeFiles,
|
|
|
|
|
|
compress,
|
|
|
|
|
|
maxCount,
|
|
|
|
|
|
maxAgeDays,
|
2026-03-16 17:07:20 +08:00
|
|
|
|
backupType,
|
2026-03-13 15:13:58 +08:00
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
const newSettings = {};
|
|
|
|
|
|
if (enabled !== undefined) newSettings.enabled = enabled;
|
|
|
|
|
|
if (hour !== undefined || minute !== undefined) {
|
|
|
|
|
|
newSettings.hour = hour || 2;
|
|
|
|
|
|
newSettings.minute = minute || 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cronExpression) {
|
|
|
|
|
|
if (!validateCronExpression(cronExpression)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '无效的 Cron 表达式',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
newSettings.cronExpression = cronExpression;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (description) newSettings.description = description;
|
|
|
|
|
|
if (includeFiles !== undefined) newSettings.includeFiles = includeFiles;
|
|
|
|
|
|
if (compress !== undefined) newSettings.compress = compress;
|
|
|
|
|
|
if (maxCount !== undefined) newSettings.maxCount = maxCount;
|
|
|
|
|
|
if (maxAgeDays !== undefined) newSettings.maxAgeDays = maxAgeDays;
|
2026-03-16 17:07:20 +08:00
|
|
|
|
if (backupType !== undefined) newSettings.backupType = backupType;
|
2026-03-13 15:13:58 +08:00
|
|
|
|
|
|
|
|
|
|
const success = updateAutoBackupSettings(newSettings);
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
const status = getAutoBackupStatus();
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '自动备份设置已更新',
|
|
|
|
|
|
data: status,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '保存设置失败',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('更新自动备份设置失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '更新自动备份设置失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 立即执行备份
|
|
|
|
|
|
router.post('/auto/execute', async (req, res) => {
|
|
|
|
|
|
try {
|
2026-03-16 17:07:20 +08:00
|
|
|
|
const { description, includeFiles, compress, backupType } = req.body;
|
2026-03-13 15:13:58 +08:00
|
|
|
|
|
|
|
|
|
|
const result = await executeBackupNow({
|
|
|
|
|
|
description: description || '手动触发备份',
|
|
|
|
|
|
includeFiles,
|
|
|
|
|
|
compress,
|
2026-03-16 17:07:20 +08:00
|
|
|
|
backupType,
|
2026-03-13 15:13:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '备份执行成功',
|
|
|
|
|
|
data: result.result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: result.error,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('立即执行备份失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '立即执行备份失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 测试 Cron 表达式
|
|
|
|
|
|
router.post('/auto/test-cron', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { cronExpression } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!cronExpression) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供 Cron 表达式',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isValid = validateCronExpression(cronExpression);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: isValid,
|
|
|
|
|
|
message: isValid ? 'Cron 表达式有效' : 'Cron 表达式无效',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
valid: isValid,
|
|
|
|
|
|
cronExpression,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('测试 Cron 表达式失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '测试 Cron 表达式失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-13 22:11:34 +08:00
|
|
|
|
// ==================== 远端备份配置路由 ====================
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有远端备份目标
|
|
|
|
|
|
router.get('/remote/targets', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const targets = getAllTargets();
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { targets },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取远端备份目标失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取远端备份目标失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取单个远端备份目标
|
|
|
|
|
|
router.get('/remote/targets/:id', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const target = getTarget(req.params.id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '目标不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { target },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取远端备份目标失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取远端备份目标失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 添加远端备份目标
|
|
|
|
|
|
router.post('/remote/targets', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const targetData = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!targetData.name || !targetData.protocol) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供目标名称和协议类型',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const target = addTarget(targetData);
|
|
|
|
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '远端备份目标已添加',
|
|
|
|
|
|
data: { target },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('添加远端备份目标失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '添加远端备份目标失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新远端备份目标
|
|
|
|
|
|
router.put('/remote/targets/:id', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updates = req.body;
|
|
|
|
|
|
const target = updateTarget(req.params.id, updates);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '远端备份目标已更新',
|
|
|
|
|
|
data: { target },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('更新远端备份目标失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '更新远端备份目标失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 删除远端备份目标
|
|
|
|
|
|
router.delete('/remote/targets/:id', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const deleted = deleteTarget(req.params.id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!deleted) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '目标不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '远端备份目标已删除',
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除远端备份目标失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '删除远端备份目标失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-23 10:40:26 +08:00
|
|
|
|
// 直接测试远端连接(不保存,用于添加节点前的测试)
|
|
|
|
|
|
router.post('/remote/test', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const config = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!config.host || !config.port || !config.username) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供完整的连接信息(主机、端口、用户名)',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!config.protocol) {
|
2026-03-23 11:09:36 +08:00
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供协议类型(protocol)',
|
|
|
|
|
|
});
|
2026-03-23 10:40:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await testRemoteConnection(config);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '连接测试成功',
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: result.message,
|
|
|
|
|
|
error: result.error,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[RemoteBackup] 测试远端连接失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '测试远端连接失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-13 22:11:34 +08:00
|
|
|
|
// 测试远端连接
|
|
|
|
|
|
router.post('/remote/targets/:id/test', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const target = getTarget(req.params.id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '目标不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await testRemoteConnection(target);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '连接测试成功',
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: result.message,
|
|
|
|
|
|
error: result.error,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('测试远端连接失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '测试远端连接失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取远端备份全局设置
|
|
|
|
|
|
router.get('/remote/settings', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const settings = getGlobalSettings();
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { settings },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取远端备份设置失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取远端备份设置失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新远端备份全局设置
|
|
|
|
|
|
router.put('/remote/settings', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const settings = updateGlobalSettings(req.body);
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '远端备份设置已更新',
|
|
|
|
|
|
data: { settings },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('更新远端备份设置失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '更新远端备份设置失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取支持的协议列表
|
|
|
|
|
|
router.get('/remote/protocols', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const protocols = Object.entries(PROTOCOL_TYPES).map(([key, value]) => ({
|
|
|
|
|
|
key,
|
|
|
|
|
|
value,
|
|
|
|
|
|
label: PROTOCOL_LABELS[value],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { protocols },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取协议列表失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取协议列表失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 手动上传备份到远端
|
|
|
|
|
|
router.post('/remote/upload', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { filename, targetIds } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
if (!filename) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '请提供备份文件名',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const backupPath = getBackupPath();
|
|
|
|
|
|
const filePath = path.join(backupPath, filename);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份文件不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { uploadToRemote } = require('../utils/remoteBackup');
|
|
|
|
|
|
const { getTarget } = require('../utils/remoteBackupConfig');
|
|
|
|
|
|
|
|
|
|
|
|
const targets = targetIds
|
|
|
|
|
|
? targetIds.map(id => getTarget(id)).filter(Boolean)
|
|
|
|
|
|
: getEnabledTargets();
|
|
|
|
|
|
|
|
|
|
|
|
if (targets.length === 0) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '没有可用的远端目标',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uploadResults = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const target of targets) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const remotePath = `${target.prefix || 'backups/'}${filename}`;
|
|
|
|
|
|
const result = await uploadToRemote(target, filePath, remotePath);
|
|
|
|
|
|
|
|
|
|
|
|
uploadResults.push({
|
|
|
|
|
|
targetId: target.id,
|
|
|
|
|
|
targetName: target.name,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
...result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
uploadResults.push({
|
|
|
|
|
|
targetId: target.id,
|
|
|
|
|
|
targetName: target.name,
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '上传完成',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
filename,
|
|
|
|
|
|
results: uploadResults,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('手动上传备份失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '手动上传备份失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-16 17:07:20 +08:00
|
|
|
|
// ==================== 备份日志接口 ====================
|
|
|
|
|
|
|
|
|
|
|
|
// 获取备份日志列表
|
|
|
|
|
|
router.get('/logs', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { page = 1, pageSize = 20, logType, status } = req.query;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await getBackupLogs({
|
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
|
pageSize: parseInt(pageSize),
|
|
|
|
|
|
logType,
|
|
|
|
|
|
status,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取备份日志失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取备份日志失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取备份日志详情
|
|
|
|
|
|
router.get('/logs/:id', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const log = await getBackupLogById(parseInt(id));
|
|
|
|
|
|
|
|
|
|
|
|
if (!log) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '备份日志不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: log,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取备份日志详情失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '获取备份日志详情失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理旧日志
|
|
|
|
|
|
router.delete('/logs/clean', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { days = 30 } = req.body;
|
|
|
|
|
|
const deletedCount = await deleteOldLogs(parseInt(days));
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '清理完成',
|
|
|
|
|
|
data: { deletedCount },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('清理旧日志失败:', error);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '清理旧日志失败',
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-13 15:13:58 +08:00
|
|
|
|
module.exports = router;
|