Spaces:
Paused
Paused
| 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<boolean>} 备份结果 | |
| */ | |
| 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<Object>} 用户数据 | |
| */ | |
| 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<Object>} 备份结果 | |
| */ | |
| 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<Array>} 用户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<boolean>} 恢复结果 | |
| */ | |
| 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; |