449 lines
11 KiB
JavaScript
449 lines
11 KiB
JavaScript
|
|
/**
|
|||
|
|
* 备份管理控制器
|
|||
|
|
* @file backupController.js
|
|||
|
|
* @description 处理数据备份和恢复相关业务逻辑
|
|||
|
|
*/
|
|||
|
|
const backupService = require('../services/backupService');
|
|||
|
|
const logger = require('../utils/logger');
|
|||
|
|
const { validationResult } = require('express-validator');
|
|||
|
|
const cron = require('node-cron');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建备份
|
|||
|
|
* @route POST /api/backup/create
|
|||
|
|
*/
|
|||
|
|
const createBackup = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const { type = 'full', description } = req.body;
|
|||
|
|
|
|||
|
|
logger.info(`用户 ${req.user.username} 开始创建备份,类型: ${type}`);
|
|||
|
|
|
|||
|
|
const backupResult = await backupService.createFullBackup({
|
|||
|
|
type,
|
|||
|
|
description,
|
|||
|
|
createdBy: req.user.id
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: '备份创建成功',
|
|||
|
|
data: backupResult
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('创建备份失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: error.message || '创建备份失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取备份列表
|
|||
|
|
* @route GET /api/backup/list
|
|||
|
|
*/
|
|||
|
|
const getBackupList = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const { page = 1, limit = 10, type } = req.query;
|
|||
|
|
|
|||
|
|
let backups = await backupService.getBackupList();
|
|||
|
|
|
|||
|
|
// 按类型过滤
|
|||
|
|
if (type) {
|
|||
|
|
backups = backups.filter(backup => backup.type === type);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分页
|
|||
|
|
const startIndex = (page - 1) * limit;
|
|||
|
|
const endIndex = startIndex + parseInt(limit);
|
|||
|
|
const paginatedBackups = backups.slice(startIndex, endIndex);
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
data: paginatedBackups,
|
|||
|
|
total: backups.length,
|
|||
|
|
page: parseInt(page),
|
|||
|
|
limit: parseInt(limit)
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('获取备份列表失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '获取备份列表失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取备份统计
|
|||
|
|
* @route GET /api/backup/stats
|
|||
|
|
*/
|
|||
|
|
const getBackupStats = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const stats = await backupService.getBackupStats();
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
data: stats
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('获取备份统计失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '获取备份统计失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除备份
|
|||
|
|
* @route DELETE /api/backup/:id
|
|||
|
|
*/
|
|||
|
|
const deleteBackup = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const { id } = req.params;
|
|||
|
|
|
|||
|
|
const result = await backupService.deleteBackup(id);
|
|||
|
|
|
|||
|
|
if (result) {
|
|||
|
|
logger.info(`用户 ${req.user.username} 删除备份: ${id}`);
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: '备份删除成功'
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
res.status(404).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '备份不存在或删除失败'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('删除备份失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '删除备份失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 恢复数据库
|
|||
|
|
* @route POST /api/backup/:id/restore
|
|||
|
|
*/
|
|||
|
|
const restoreBackup = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const { id } = req.params;
|
|||
|
|
const { confirm } = req.body;
|
|||
|
|
|
|||
|
|
if (!confirm) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '请确认恢复操作'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.info(`用户 ${req.user.username} 开始恢复备份: ${id}`);
|
|||
|
|
|
|||
|
|
const result = await backupService.restoreDatabase(id);
|
|||
|
|
|
|||
|
|
if (result) {
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: '数据恢复成功'
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '数据恢复失败'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('恢复备份失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '恢复备份失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 下载备份文件
|
|||
|
|
* @route GET /api/backup/:id/download
|
|||
|
|
*/
|
|||
|
|
const downloadBackup = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const { id } = req.params;
|
|||
|
|
const path = require('path');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
|
|||
|
|
const archivePath = path.join(__dirname, '../../backups', `${id}.zip`);
|
|||
|
|
|
|||
|
|
// 检查文件是否存在
|
|||
|
|
if (!fs.existsSync(archivePath)) {
|
|||
|
|
return res.status(404).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '备份文件不存在'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const stats = fs.statSync(archivePath);
|
|||
|
|
const filename = `backup_${id}.zip`;
|
|||
|
|
|
|||
|
|
res.setHeader('Content-Type', 'application/zip');
|
|||
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|||
|
|
res.setHeader('Content-Length', stats.size);
|
|||
|
|
|
|||
|
|
const readStream = fs.createReadStream(archivePath);
|
|||
|
|
readStream.pipe(res);
|
|||
|
|
|
|||
|
|
logger.info(`用户 ${req.user.username} 下载备份: ${id}`);
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('下载备份失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '下载备份失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清理过期备份
|
|||
|
|
* @route POST /api/backup/cleanup
|
|||
|
|
*/
|
|||
|
|
const cleanupBackups = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const deletedCount = await backupService.cleanupExpiredBackups();
|
|||
|
|
|
|||
|
|
logger.info(`用户 ${req.user.username} 执行备份清理,删除了 ${deletedCount} 个过期备份`);
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: `清理完成,删除了 ${deletedCount} 个过期备份`,
|
|||
|
|
data: { deletedCount }
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('清理备份失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '清理备份失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取备份健康状态
|
|||
|
|
* @route GET /api/backup/health
|
|||
|
|
*/
|
|||
|
|
const getBackupHealth = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const health = await backupService.checkBackupHealth();
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
data: health
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('获取备份健康状态失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '获取备份健康状态失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 自动备份任务调度器
|
|||
|
|
*/
|
|||
|
|
class BackupScheduler {
|
|||
|
|
constructor() {
|
|||
|
|
this.tasks = new Map();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 启动自动备份调度
|
|||
|
|
*/
|
|||
|
|
start() {
|
|||
|
|
// 每日备份(凌晨2点)
|
|||
|
|
const dailyTask = cron.schedule('0 2 * * *', async () => {
|
|||
|
|
try {
|
|||
|
|
logger.info('开始执行自动日备份');
|
|||
|
|
await backupService.createFullBackup({ type: 'daily', description: '自动日备份' });
|
|||
|
|
logger.info('自动日备份完成');
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('自动日备份失败:', error);
|
|||
|
|
}
|
|||
|
|
}, {
|
|||
|
|
scheduled: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 每周备份(周日凌晨3点)
|
|||
|
|
const weeklyTask = cron.schedule('0 3 * * 0', async () => {
|
|||
|
|
try {
|
|||
|
|
logger.info('开始执行自动周备份');
|
|||
|
|
await backupService.createFullBackup({ type: 'weekly', description: '自动周备份' });
|
|||
|
|
logger.info('自动周备份完成');
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('自动周备份失败:', error);
|
|||
|
|
}
|
|||
|
|
}, {
|
|||
|
|
scheduled: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 每月备份(每月1号凌晨4点)
|
|||
|
|
const monthlyTask = cron.schedule('0 4 1 * *', async () => {
|
|||
|
|
try {
|
|||
|
|
logger.info('开始执行自动月备份');
|
|||
|
|
await backupService.createFullBackup({ type: 'monthly', description: '自动月备份' });
|
|||
|
|
logger.info('自动月备份完成');
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('自动月备份失败:', error);
|
|||
|
|
}
|
|||
|
|
}, {
|
|||
|
|
scheduled: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 自动清理任务(每天凌晨5点)
|
|||
|
|
const cleanupTask = cron.schedule('0 5 * * *', async () => {
|
|||
|
|
try {
|
|||
|
|
logger.info('开始执行自动备份清理');
|
|||
|
|
const deletedCount = await backupService.cleanupExpiredBackups();
|
|||
|
|
logger.info(`自动备份清理完成,删除了 ${deletedCount} 个过期备份`);
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('自动备份清理失败:', error);
|
|||
|
|
}
|
|||
|
|
}, {
|
|||
|
|
scheduled: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.tasks.set('daily', dailyTask);
|
|||
|
|
this.tasks.set('weekly', weeklyTask);
|
|||
|
|
this.tasks.set('monthly', monthlyTask);
|
|||
|
|
this.tasks.set('cleanup', cleanupTask);
|
|||
|
|
|
|||
|
|
// 启动所有任务
|
|||
|
|
this.tasks.forEach((task, name) => {
|
|||
|
|
task.start();
|
|||
|
|
logger.info(`备份调度任务已启动: ${name}`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 停止自动备份调度
|
|||
|
|
*/
|
|||
|
|
stop() {
|
|||
|
|
this.tasks.forEach((task, name) => {
|
|||
|
|
task.stop();
|
|||
|
|
logger.info(`备份调度任务已停止: ${name}`);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取调度状态
|
|||
|
|
*/
|
|||
|
|
getStatus() {
|
|||
|
|
const status = {};
|
|||
|
|
this.tasks.forEach((task, name) => {
|
|||
|
|
status[name] = {
|
|||
|
|
running: task.running,
|
|||
|
|
lastExecution: task.lastExecution,
|
|||
|
|
nextExecution: task.nextExecution
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
return status;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建调度器实例
|
|||
|
|
const scheduler = new BackupScheduler();
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 启动自动备份调度
|
|||
|
|
* @route POST /api/backup/schedule/start
|
|||
|
|
*/
|
|||
|
|
const startScheduler = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
scheduler.start();
|
|||
|
|
logger.info(`用户 ${req.user.username} 启动自动备份调度`);
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: '自动备份调度已启动'
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('启动备份调度失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '启动备份调度失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 停止自动备份调度
|
|||
|
|
* @route POST /api/backup/schedule/stop
|
|||
|
|
*/
|
|||
|
|
const stopScheduler = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
scheduler.stop();
|
|||
|
|
logger.info(`用户 ${req.user.username} 停止自动备份调度`);
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
message: '自动备份调度已停止'
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('停止备份调度失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '停止备份调度失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取调度状态
|
|||
|
|
* @route GET /api/backup/schedule/status
|
|||
|
|
*/
|
|||
|
|
const getSchedulerStatus = async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
const status = scheduler.getStatus();
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
success: true,
|
|||
|
|
data: status
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('获取调度状态失败:', error);
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
message: '获取调度状态失败',
|
|||
|
|
error: error.message
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
createBackup,
|
|||
|
|
getBackupList,
|
|||
|
|
getBackupStats,
|
|||
|
|
deleteBackup,
|
|||
|
|
restoreBackup,
|
|||
|
|
downloadBackup,
|
|||
|
|
cleanupBackups,
|
|||
|
|
getBackupHealth,
|
|||
|
|
startScheduler,
|
|||
|
|
stopScheduler,
|
|||
|
|
getSchedulerStatus,
|
|||
|
|
scheduler
|
|||
|
|
};
|