Spaces:
Paused
Paused
| import cron from 'node-cron'; | |
| import githubService from './githubService.js'; | |
| import huggingfaceStorageService from './huggingfaceStorageService.js'; | |
| import fs from 'fs/promises'; | |
| import path from 'path'; | |
| /** | |
| * 自动备份服务 | |
| * 负责定时将修改的PPT数据同步到GitHub仓库 | |
| */ | |
| class AutoBackupService { | |
| constructor() { | |
| this.isInitialized = false; | |
| this.cronJob = null; | |
| this.backupInterval = '0 */8 * * *'; // 每8小时执行一次 | |
| this.lastBackupTime = null; | |
| this.backupStats = { | |
| totalBackups: 0, | |
| successfulBackups: 0, | |
| failedBackups: 0, | |
| lastBackupResult: null | |
| }; | |
| // 跟踪修改的PPT数据 | |
| this.modifiedPPTs = new Map(); // userId -> Set of pptIds | |
| this.lastModificationCheck = new Date(); | |
| console.log('🔄 AutoBackupService initialized'); | |
| } | |
| /** | |
| * 初始化自动备份服务 | |
| */ | |
| async initialize() { | |
| try { | |
| console.log('🚀 Initializing AutoBackupService...'); | |
| // 加载上次备份时间 | |
| await this.loadBackupState(); | |
| // 启动定时任务 | |
| this.startCronJob(); | |
| this.isInitialized = true; | |
| console.log('✅ AutoBackupService initialized successfully'); | |
| console.log(`⏰ Next backup scheduled: ${this.getNextBackupTime()}`); | |
| } catch (error) { | |
| console.error('❌ Failed to initialize AutoBackupService:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 启动定时任务 | |
| */ | |
| startCronJob() { | |
| if (this.cronJob) { | |
| this.cronJob.stop(); | |
| } | |
| console.log(`⏰ Starting auto backup cron job: ${this.backupInterval}`); | |
| this.cronJob = cron.schedule(this.backupInterval, async () => { | |
| console.log('🔄 Auto backup triggered by cron job'); | |
| await this.performAutoBackup(); | |
| }, { | |
| scheduled: true, | |
| timezone: 'UTC' | |
| }); | |
| console.log('✅ Auto backup cron job started'); | |
| } | |
| /** | |
| * 停止定时任务 | |
| */ | |
| stopCronJob() { | |
| if (this.cronJob) { | |
| this.cronJob.stop(); | |
| this.cronJob = null; | |
| console.log('🛑 Auto backup cron job stopped'); | |
| } | |
| } | |
| /** | |
| * 记录PPT修改 | |
| */ | |
| recordPPTModification(userId, pptId) { | |
| if (!this.modifiedPPTs.has(userId)) { | |
| this.modifiedPPTs.set(userId, new Set()); | |
| } | |
| this.modifiedPPTs.get(userId).add(pptId); | |
| console.log(`📝 Recorded modification: User ${userId}, PPT ${pptId}`); | |
| } | |
| /** | |
| * 获取修改的PPT列表 | |
| */ | |
| getModifiedPPTs() { | |
| const result = {}; | |
| for (const [userId, pptIds] of this.modifiedPPTs.entries()) { | |
| result[userId] = Array.from(pptIds); | |
| } | |
| return result; | |
| } | |
| /** | |
| * 清除修改记录 | |
| */ | |
| clearModificationRecords() { | |
| this.modifiedPPTs.clear(); | |
| console.log('🧹 Cleared modification records'); | |
| } | |
| /** | |
| * 执行自动备份 | |
| */ | |
| async performAutoBackup() { | |
| const startTime = new Date(); | |
| console.log(`🔄 Starting auto backup at ${startTime.toISOString()}`); | |
| const backupResult = { | |
| timestamp: startTime, | |
| success: false, | |
| totalUsers: 0, | |
| totalPPTs: 0, | |
| successfulBackups: 0, | |
| failedBackups: 0, | |
| errors: [], | |
| duration: 0 | |
| }; | |
| try { | |
| this.backupStats.totalBackups++; | |
| // 获取修改的PPT列表 | |
| const modifiedPPTs = this.getModifiedPPTs(); | |
| const userIds = Object.keys(modifiedPPTs); | |
| backupResult.totalUsers = userIds.length; | |
| if (userIds.length === 0) { | |
| console.log('ℹ️ No modified PPTs found, skipping backup'); | |
| backupResult.success = true; | |
| return backupResult; | |
| } | |
| console.log(`📊 Found ${userIds.length} users with modified PPTs`); | |
| // 为每个用户备份修改的PPT | |
| for (const userId of userIds) { | |
| const pptIds = modifiedPPTs[userId]; | |
| backupResult.totalPPTs += pptIds.length; | |
| console.log(`👤 Backing up ${pptIds.length} PPTs for user ${userId}`); | |
| for (const pptId of pptIds) { | |
| try { | |
| // 获取PPT数据 | |
| const pptData = await githubService.getPPT(userId, pptId); | |
| if (!pptData) { | |
| console.warn(`⚠️ PPT ${pptId} not found for user ${userId}`); | |
| backupResult.failedBackups++; | |
| continue; | |
| } | |
| // 执行备份到GitHub | |
| await githubService.savePPT(userId, pptId, pptData); | |
| console.log(`✅ Successfully backed up PPT ${pptId} for user ${userId}`); | |
| backupResult.successfulBackups++; | |
| } catch (error) { | |
| console.error(`❌ Failed to backup PPT ${pptId} for user ${userId}:`, error); | |
| backupResult.failedBackups++; | |
| backupResult.errors.push({ | |
| userId, | |
| pptId, | |
| error: error.message | |
| }); | |
| } | |
| } | |
| } | |
| // 清除修改记录 | |
| this.clearModificationRecords(); | |
| backupResult.success = backupResult.failedBackups === 0; | |
| if (backupResult.success) { | |
| this.backupStats.successfulBackups++; | |
| console.log(`✅ Auto backup completed successfully: ${backupResult.successfulBackups} PPTs backed up`); | |
| } else { | |
| this.backupStats.failedBackups++; | |
| console.error(`❌ Auto backup completed with errors: ${backupResult.failedBackups} failures`); | |
| } | |
| } catch (error) { | |
| console.error('❌ Auto backup failed:', error); | |
| backupResult.success = false; | |
| backupResult.errors.push({ | |
| type: 'system', | |
| error: error.message | |
| }); | |
| this.backupStats.failedBackups++; | |
| } finally { | |
| const endTime = new Date(); | |
| backupResult.duration = endTime.getTime() - startTime.getTime(); | |
| this.lastBackupTime = startTime; | |
| this.backupStats.lastBackupResult = backupResult; | |
| // 保存备份状态 | |
| await this.saveBackupState(); | |
| console.log(`🏁 Auto backup finished in ${backupResult.duration}ms`); | |
| } | |
| return backupResult; | |
| } | |
| /** | |
| * 手动触发备份 | |
| */ | |
| async triggerManualBackup() { | |
| console.log('🔄 Manual backup triggered'); | |
| return await this.performAutoBackup(); | |
| } | |
| /** | |
| * 获取下次备份时间 | |
| */ | |
| getNextBackupTime() { | |
| if (!this.cronJob) { | |
| return 'Not scheduled'; | |
| } | |
| // 计算下次执行时间(简化版本) | |
| const now = new Date(); | |
| const nextHour = Math.ceil(now.getHours() / 8) * 8; | |
| const nextBackup = new Date(now); | |
| if (nextHour >= 24) { | |
| nextBackup.setDate(nextBackup.getDate() + 1); | |
| nextBackup.setHours(0, 0, 0, 0); | |
| } else { | |
| nextBackup.setHours(nextHour, 0, 0, 0); | |
| } | |
| return nextBackup.toISOString(); | |
| } | |
| /** | |
| * 获取备份统计信息 | |
| */ | |
| getBackupStats() { | |
| return { | |
| ...this.backupStats, | |
| lastBackupTime: this.lastBackupTime, | |
| nextBackupTime: this.getNextBackupTime(), | |
| modifiedPPTsCount: Array.from(this.modifiedPPTs.values()) | |
| .reduce((total, pptSet) => total + pptSet.size, 0), | |
| isRunning: !!this.cronJob, | |
| backupInterval: this.backupInterval | |
| }; | |
| } | |
| /** | |
| * 加载备份状态 | |
| */ | |
| async loadBackupState() { | |
| try { | |
| const stateFile = path.join(process.cwd(), 'data', 'backup-state.json'); | |
| try { | |
| const stateData = await fs.readFile(stateFile, 'utf8'); | |
| const state = JSON.parse(stateData); | |
| this.lastBackupTime = state.lastBackupTime ? new Date(state.lastBackupTime) : null; | |
| this.backupStats = { ...this.backupStats, ...state.backupStats }; | |
| console.log('📂 Loaded backup state from file'); | |
| } catch (error) { | |
| if (error.code !== 'ENOENT') { | |
| console.warn('⚠️ Failed to load backup state:', error.message); | |
| } | |
| } | |
| } catch (error) { | |
| console.warn('⚠️ Failed to load backup state:', error.message); | |
| } | |
| } | |
| /** | |
| * 保存备份状态 | |
| */ | |
| async saveBackupState() { | |
| try { | |
| const stateFile = path.join(process.cwd(), 'data', 'backup-state.json'); | |
| const stateDir = path.dirname(stateFile); | |
| // 确保目录存在 | |
| await fs.mkdir(stateDir, { recursive: true }); | |
| const state = { | |
| lastBackupTime: this.lastBackupTime, | |
| backupStats: this.backupStats, | |
| savedAt: new Date().toISOString() | |
| }; | |
| await fs.writeFile(stateFile, JSON.stringify(state, null, 2)); | |
| console.log('💾 Saved backup state to file'); | |
| } catch (error) { | |
| console.warn('⚠️ Failed to save backup state:', error.message); | |
| } | |
| } | |
| /** | |
| * 更新备份间隔 | |
| */ | |
| updateBackupInterval(cronExpression) { | |
| this.backupInterval = cronExpression; | |
| console.log(`⏰ Updated backup interval to: ${cronExpression}`); | |
| // 重启定时任务 | |
| this.startCronJob(); | |
| } | |
| /** | |
| * 清理服务 | |
| */ | |
| async cleanup() { | |
| console.log('🧹 Cleaning up AutoBackupService...'); | |
| this.stopCronJob(); | |
| await this.saveBackupState(); | |
| console.log('✅ AutoBackupService cleanup completed'); | |
| } | |
| } | |
| // 创建单例实例 | |
| const autoBackupService = new AutoBackupService(); | |
| export default autoBackupService; |