import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import githubService from './githubService.js'; import huggingfaceStorageService from './huggingfaceStorageService.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * 备份调度服务 * 管理用户数据的定时备份到GitHub和服务器重启时的数据恢复 */ class BackupSchedulerService { constructor() { this.backupQueue = new Map(); // userId -> { timer, scheduledTime } this.isRestoring = false; this.backupInProgress = new Set(); // 正在备份的用户ID this.initialized = false; // 备份配置 this.BACKUP_DELAY = 8 * 60 * 60 * 1000; // 8小时 this.MAX_BACKUP_RETRIES = 3; this.BACKUP_RETRY_DELAY = 5 * 60 * 1000; // 5分钟 } /** * 初始化备份调度服务 */ async initialize() { if (this.initialized) return; try { // 服务器启动时检查是否需要从GitHub恢复数据 await this.checkAndRestoreFromGitHub(); this.initialized = true; console.log('✅ BackupSchedulerService initialized successfully'); } catch (error) { console.error('❌ Failed to initialize BackupSchedulerService:', error); throw error; } } /** * 调度用户数据备份 * @param {string} userId - 用户ID * @param {number} delay - 延迟时间(毫秒),默认8小时 */ scheduleUserBackup(userId, delay = this.BACKUP_DELAY) { if (!this.initialized) { console.warn('BackupSchedulerService not initialized, skipping backup schedule'); return; } // 如果已经有调度,先取消 this.cancelUserBackup(userId); const scheduledTime = Date.now() + delay; const timer = setTimeout(async () => { try { await this.backupUserData(userId); } catch (error) { console.error(`❌ Scheduled backup failed for user ${userId}:`, error); // 重试机制 this.retryBackup(userId, 1); } finally { this.backupQueue.delete(userId); } }, delay); this.backupQueue.set(userId, { timer, scheduledTime }); console.log(`⏰ Backup scheduled for user ${userId} in ${Math.round(delay / 1000 / 60)} minutes`); } /** * 取消用户备份调度 * @param {string} userId - 用户ID */ cancelUserBackup(userId) { const backup = this.backupQueue.get(userId); if (backup) { clearTimeout(backup.timer); this.backupQueue.delete(userId); console.log(`❌ Backup cancelled for user ${userId}`); } } /** * 立即备份用户数据 * @param {string} userId - 用户ID * @returns {Promise} 备份结果 */ async backupUserData(userId) { if (this.backupInProgress.has(userId)) { console.log(`⏳ Backup already in progress for user ${userId}`); return false; } this.backupInProgress.add(userId); try { console.log(`🔄 Starting backup for user ${userId}`); // 获取用户的所有数据 const userData = await this.collectUserData(userId); if (!userData || Object.keys(userData).length === 0) { console.log(`📭 No data to backup for user ${userId}`); return true; } // 备份到GitHub const backupResult = await this.backupToGitHub(userId, userData); if (backupResult.success) { console.log(`✅ Backup completed for user ${userId}`); return true; } else { throw new Error(backupResult.error || 'Backup failed'); } } catch (error) { console.error(`❌ Backup failed for user ${userId}:`, error); throw error; } finally { this.backupInProgress.delete(userId); } } /** * 重试备份 * @param {string} userId - 用户ID * @param {number} attempt - 重试次数 */ retryBackup(userId, attempt) { if (attempt > this.MAX_BACKUP_RETRIES) { console.error(`❌ Max backup retries exceeded for user ${userId}`); return; } console.log(`🔄 Retrying backup for user ${userId} (attempt ${attempt}/${this.MAX_BACKUP_RETRIES})`); setTimeout(async () => { try { await this.backupUserData(userId); } catch (error) { this.retryBackup(userId, attempt + 1); } }, this.BACKUP_RETRY_DELAY * attempt); // 递增延迟 } /** * 收集用户数据 * @param {string} userId - 用户ID * @returns {Promise} 用户数据 */ async collectUserData(userId) { try { const userData = { userId, backupTime: new Date().toISOString(), ppts: {}, images: [] }; // 获取用户的PPT数据 const pptList = await githubService.getUserPPTList(userId); for (const ppt of pptList) { try { const pptData = await githubService.getPPT(userId, ppt.id); if (pptData && pptData.content) { userData.ppts[ppt.id] = { id: ppt.id, title: ppt.title, content: pptData.content, updatedAt: ppt.updatedAt, createdAt: ppt.createdAt }; } } catch (error) { console.warn(`⚠️ Failed to get PPT ${ppt.id} for backup:`, error.message); } } // 获取用户的图片数据 const userImages = await huggingfaceStorageService.getUserImages(userId); userData.images = userImages; return userData; } catch (error) { console.error(`❌ Failed to collect user data for ${userId}:`, error); throw error; } } /** * 备份数据到GitHub * @param {string} userId - 用户ID * @param {Object} userData - 用户数据 * @returns {Promise} 备份结果 */ async backupToGitHub(userId, userData) { try { const backupFileName = `backup-${userId}-${Date.now()}.json`; const backupPath = `backups/${userId}/${backupFileName}`; // 压缩数据 const compressedData = JSON.stringify(userData); // 上传到GitHub const result = await githubService.uploadFile( backupPath, compressedData, `Backup user data for ${userId}`, 0 // 使用第一个仓库进行备份 ); if (result.success) { // 更新备份索引 await this.updateBackupIndex(userId, { fileName: backupFileName, path: backupPath, timestamp: Date.now(), size: compressedData.length, pptCount: Object.keys(userData.ppts).length, imageCount: userData.images.length }); return { success: true, path: backupPath }; } else { return { success: false, error: result.error }; } } catch (error) { return { success: false, error: error.message }; } } /** * 更新备份索引 * @param {string} userId - 用户ID * @param {Object} backupInfo - 备份信息 */ async updateBackupIndex(userId, backupInfo) { try { const indexPath = `backups/${userId}/index.json`; // 获取现有索引 let index = { backups: [] }; try { const existingIndex = await githubService.getFile(indexPath, 0); if (existingIndex.success) { index = JSON.parse(existingIndex.content); } } catch (error) { // 索引不存在,使用默认值 } // 添加新备份记录 index.backups.unshift(backupInfo); // 保留最近10个备份记录 index.backups = index.backups.slice(0, 10); // 更新索引文件 await githubService.uploadFile( indexPath, JSON.stringify(index, null, 2), `Update backup index for ${userId}`, 0 ); } catch (error) { console.warn(`⚠️ Failed to update backup index for ${userId}:`, error.message); } } /** * 检查并从GitHub恢复数据 */ async checkAndRestoreFromGitHub() { if (this.isRestoring) return; this.isRestoring = true; try { console.log('🔍 Checking if data restoration is needed...'); // 检查Huggingface存储是否为空或需要恢复 const storageStats = await huggingfaceStorageService.getStorageStats(); if (storageStats.totalImages === 0) { console.log('📦 No existing data found, attempting to restore from GitHub...'); await this.restoreAllDataFromGitHub(); } else { console.log(`📊 Found ${storageStats.totalImages} existing images, skipping restoration`); } } catch (error) { console.error('❌ Failed to check/restore data from GitHub:', error); } finally { this.isRestoring = false; } } /** * 从GitHub恢复所有数据 */ async restoreAllDataFromGitHub() { try { console.log('🔄 Starting data restoration from GitHub...'); // 获取所有备份用户 const backupUsers = await this.getBackupUsers(); let restoredCount = 0; for (const userId of backupUsers) { try { const restored = await this.restoreUserDataFromGitHub(userId); if (restored) { restoredCount++; } } catch (error) { console.warn(`⚠️ Failed to restore data for user ${userId}:`, error.message); } } console.log(`✅ Data restoration completed. Restored ${restoredCount} users.`); } catch (error) { console.error('❌ Failed to restore all data from GitHub:', error); throw error; } } /** * 获取所有有备份的用户 * @returns {Promise} 用户ID数组 */ async getBackupUsers() { try { // 尝试获取备份目录列表 const backupDirs = await githubService.listDirectory('backups', 0); if (backupDirs.success && backupDirs.items) { return backupDirs.items .filter(item => item.type === 'dir') .map(item => item.name); } return []; } catch (error) { console.warn('⚠️ Failed to get backup users:', error.message); return []; } } /** * 从GitHub恢复用户数据 * @param {string} userId - 用户ID * @returns {Promise} 恢复结果 */ async restoreUserDataFromGitHub(userId) { try { console.log(`🔄 Restoring data for user ${userId}...`); // 获取用户的备份索引 const indexPath = `backups/${userId}/index.json`; const indexResult = await githubService.getFile(indexPath, 0); if (!indexResult.success) { console.log(`📭 No backup index found for user ${userId}`); return false; } const index = JSON.parse(indexResult.content); if (!index.backups || index.backups.length === 0) { console.log(`📭 No backups found for user ${userId}`); return false; } // 获取最新的备份 const latestBackup = index.backups[0]; const backupResult = await githubService.getFile(latestBackup.path, 0); if (!backupResult.success) { console.error(`❌ Failed to get backup file for user ${userId}`); return false; } const userData = JSON.parse(backupResult.content); // 恢复PPT数据 let restoredPPTs = 0; for (const [pptId, pptData] of Object.entries(userData.ppts || {})) { try { await githubService.savePPT(userId, pptId, pptData.content); restoredPPTs++; } catch (error) { console.warn(`⚠️ Failed to restore PPT ${pptId}:`, error.message); } } console.log(`✅ Restored ${restoredPPTs} PPTs for user ${userId}`); // 注意:图片数据需要重新生成,因为备份中只有元数据 console.log(`ℹ️ Image data for user ${userId} will be regenerated on demand`); return true; } catch (error) { console.error(`❌ Failed to restore user data for ${userId}:`, error); return false; } } /** * 获取备份状态 * @returns {Object} 备份状态信息 */ getBackupStatus() { const scheduledBackups = []; for (const [userId, backup] of this.backupQueue.entries()) { scheduledBackups.push({ userId, scheduledTime: new Date(backup.scheduledTime).toISOString(), remainingTime: Math.max(0, backup.scheduledTime - Date.now()) }); } return { initialized: this.initialized, isRestoring: this.isRestoring, scheduledBackups: scheduledBackups.length, backupsInProgress: this.backupInProgress.size, scheduledBackupDetails: scheduledBackups }; } /** * 清理资源 */ cleanup() { // 取消所有调度的备份 for (const [userId] of this.backupQueue.entries()) { this.cancelUserBackup(userId); } console.log('✅ BackupSchedulerService cleaned up'); } } // 创建单例实例 const backupSchedulerService = new BackupSchedulerService(); // 优雅关闭处理 process.on('SIGTERM', () => { backupSchedulerService.cleanup(); }); process.on('SIGINT', () => { backupSchedulerService.cleanup(); }); export default backupSchedulerService;