Spaces:
Running
Running
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; |