const cron = require('node-cron'); const path = require('path'); const fs = require('fs'); const { createBackup, createIncrementalBackup, getBackupPath } = require('./backup'); const { uploadToRemote } = require('./remoteBackup'); const { getEnabledTargets, getGlobalSettings } = require('./remoteBackupConfig'); const { createLogEntry, updateLogStatus } = require('./backupLog'); const schedulers = new Map(); const SETTINGS_FILE = path.join(__dirname, '..', 'config', 'auto-backup-settings.json'); const DEFAULT_SETTINGS = { enabled: false, cronExpression: '0 2 * * *', description: '自动备份', backupType: 'full', includeFiles: true, compress: true, maxCount: 30, maxAgeDays: 90, }; function loadSettings() { try { if (fs.existsSync(SETTINGS_FILE)) { const content = fs.readFileSync(SETTINGS_FILE, 'utf8'); return { ...DEFAULT_SETTINGS, ...JSON.parse(content) }; } } catch (error) { console.error('加载自动备份设置失败:', error); } return { ...DEFAULT_SETTINGS }; } function saveSettings(settings) { try { const configDir = path.dirname(SETTINGS_FILE); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8'); return true; } catch (error) { console.error('保存自动备份设置失败:', error); return false; } } function validateCronExpression(expression) { return cron.validate(expression); } function timeToCron(hour, minute) { return `${minute} ${hour} * * *`; } function calculateNextRun(cronExpression) { try { 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 '计算失败'; } } function getFileSize(filePath) { try { if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); return stats.size; } } catch (error) { console.error('获取文件大小失败:', error); } return null; } function createAutoBackupTask(settings) { const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } = settings; if (!validateCronExpression(cronExpression)) { throw new Error('无效的 Cron 表达式'); } console.log('=== 创建自动备份任务 ==='); console.log('Cron表达式:', cronExpression); console.log('备份类型:', backupType); console.log('下次执行:', calculateNextRun(cronExpression)); if (schedulers.has('auto-backup')) { console.log('停止已存在的调度器...'); stopAutoBackup(); } 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, }); 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'); } }, { timezone: 'Asia/Shanghai', } ); schedulers.set('auto-backup', task); console.log('自动备份任务已成功创建并启动'); return task; } function startAutoBackup(settings = null) { if (!settings) { settings = loadSettings(); } if (!settings.enabled) { console.log('自动备份已禁用'); return false; } try { createAutoBackupTask(settings); return true; } catch (error) { console.error('启动自动备份失败:', error.message); return false; } } function stopAutoBackup() { if (schedulers.has('auto-backup')) { const task = schedulers.get('auto-backup'); task.stop(); schedulers.delete('auto-backup'); console.log('自动备份任务已停止'); return true; } return false; } function getAutoBackupStatus() { const settings = loadSettings(); const isActive = schedulers.has('auto-backup'); let nextRun = null; 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(); } return { enabled: settings.enabled, isActive, cronExpression: settings.cronExpression, description: settings.description, backupType: settings.backupType || 'full', includeFiles: settings.includeFiles, compress: settings.compress, maxCount: settings.maxCount, maxAgeDays: settings.maxAgeDays, nextRun, }; } function updateAutoBackupSettings(newSettings) { const currentSettings = loadSettings(); const updatedSettings = { ...currentSettings, ...newSettings }; if (newSettings.hour !== undefined && newSettings.minute !== undefined) { updatedSettings.cronExpression = timeToCron(newSettings.hour, newSettings.minute); delete updatedSettings.hour; delete updatedSettings.minute; } if (!validateCronExpression(updatedSettings.cronExpression)) { throw new Error('无效的 Cron 表达式'); } if (saveSettings(updatedSettings)) { if (updatedSettings.enabled) { startAutoBackup(updatedSettings); } else { stopAutoBackup(); } return true; } return false; } 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, protocol: target.protocol, success: true, ...result, }); console.log('上传到 ' + target.name + ' 成功'); } catch (error) { console.error('上传到 ' + target.name + ' 失败:', error.message); uploadResults.push({ targetId: target.id, targetName: target.name, protocol: target.protocol, success: false, error: error.message, }); } } const settings = getGlobalSettings(); if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) { try { fs.unlinkSync(localFilePath); console.log('本地备份文件已删除'); } catch (error) { console.error('删除本地备份文件失败:', error.message); } } return uploadResults; } async function executeBackupNow(options = {}) { console.log(''); 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, }); 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, compress: options.compress !== undefined ? options.compress : settings.compress, autoClean: true, maxCount: settings.maxCount, maxAgeDays: settings.maxAgeDays, }); 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, }); } console.log('============================================\n'); return { success: true, result, remoteUploads: uploadResults, }; } catch (error) { console.error('手动备份失败:', error); console.error('============================================\n'); if (logId) { await updateLogStatus(logId, 'failed', { errorMessage: error.message || '未知错误', }); } return { success: false, error: error.message }; } } function initAutoBackup() { console.log(''); 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 { console.log('自动备份当前为禁用状态'); } console.log('============================================\n'); return getAutoBackupStatus(); } module.exports = { loadSettings, saveSettings, validateCronExpression, timeToCron, startAutoBackup, stopAutoBackup, getAutoBackupStatus, updateAutoBackupSettings, executeBackupNow, initAutoBackup, };