web_ppt_7.7 / backend /src /services /backupSchedulerService.js
CatPtain's picture
Upload 85 files
28e1dba verified
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;