nodejs / src /utils /storage.js
clash-linux's picture
Upload 27 files
146bdba verified
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { createLogger } from './logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const logger = createLogger('Storage');
/**
* 持久化存储管理器
* 用于保存和加载Cookie数据(包括Thread ID)
*/
class StorageManager {
constructor() {
// 数据文件路径
this.dataFilePath = path.join(dirname(dirname(__dirname)), 'data', 'cookies-data.json');
this.backupFilePath = path.join(dirname(dirname(__dirname)), 'data', 'cookies-data.backup.json');
// 确保数据目录存在
this.ensureDataDirectory();
}
/**
* 确保数据目录存在
*/
ensureDataDirectory() {
const dataDir = path.dirname(this.dataFilePath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
logger.info(`创建数据目录: ${dataDir}`);
}
}
/**
* 保存Cookie数据到文件
* @param {Array} cookieEntries - Cookie条目数组
* @returns {boolean} - 是否保存成功
*/
saveCookieData(cookieEntries) {
try {
// 准备要保存的数据
const dataToSave = {
version: '1.0',
lastUpdated: new Date().toISOString(),
cookies: cookieEntries.map(entry => ({
userId: entry.userId,
spaceId: entry.spaceId,
threadId: entry.threadId,
enabled: entry.enabled,
valid: entry.valid,
lastUsed: entry.lastUsed,
// 不保存实际的cookie值,只保存其哈希或标识
cookieHash: this.hashCookie(entry.cookie)
}))
};
// 先备份现有文件
if (fs.existsSync(this.dataFilePath)) {
fs.copyFileSync(this.dataFilePath, this.backupFilePath);
}
// 保存新数据
fs.writeFileSync(this.dataFilePath, JSON.stringify(dataToSave, null, 2), 'utf8');
logger.info(`成功保存 ${cookieEntries.length} 个Cookie的数据`);
return true;
} catch (error) {
logger.error(`保存Cookie数据失败: ${error.message}`);
return false;
}
}
/**
* 加载Cookie数据
* @returns {Object|null} - 加载的数据或null
*/
loadCookieData() {
try {
if (!fs.existsSync(this.dataFilePath)) {
logger.info('Cookie数据文件不存在');
return null;
}
const fileContent = fs.readFileSync(this.dataFilePath, 'utf8');
const data = JSON.parse(fileContent);
logger.info(`成功加载 ${data.cookies?.length || 0} 个Cookie的数据`);
return data;
} catch (error) {
logger.error(`加载Cookie数据失败: ${error.message}`);
// 尝试从备份恢复
if (fs.existsSync(this.backupFilePath)) {
try {
logger.info('尝试从备份文件恢复...');
const backupContent = fs.readFileSync(this.backupFilePath, 'utf8');
const backupData = JSON.parse(backupContent);
// 将备份恢复为主文件
fs.copyFileSync(this.backupFilePath, this.dataFilePath);
logger.success('成功从备份恢复数据');
return backupData;
} catch (backupError) {
logger.error(`从备份恢复失败: ${backupError.message}`);
}
}
return null;
}
}
/**
* 合并保存的数据和内存中的Cookie条目
* @param {Array} cookieEntries - 内存中的Cookie条目
* @param {Object} savedData - 保存的数据
* @returns {Array} - 合并后的Cookie条目
*/
mergeCookieData(cookieEntries, savedData) {
if (!savedData || !savedData.cookies) {
return cookieEntries;
}
const mergedEntries = [];
// 为每个内存中的cookie条目恢复保存的数据
for (const entry of cookieEntries) {
const savedEntry = savedData.cookies.find(saved =>
saved.userId === entry.userId ||
saved.cookieHash === this.hashCookie(entry.cookie)
);
if (savedEntry) {
// 恢复保存的数据
entry.threadId = savedEntry.threadId || entry.threadId;
entry.enabled = savedEntry.enabled !== undefined ? savedEntry.enabled : entry.enabled;
entry.lastUsed = savedEntry.lastUsed || entry.lastUsed;
logger.info(`恢复用户 ${entry.userId} 的数据: threadId=${entry.threadId}`);
}
mergedEntries.push(entry);
}
return mergedEntries;
}
/**
* 生成Cookie的哈希值(用于匹配,不存储实际cookie)
* @param {string} cookie - Cookie字符串
* @returns {string} - 哈希值
*/
hashCookie(cookie) {
if (!cookie) return '';
// 简单的哈希实现,取cookie的前20个字符和后20个字符
const prefix = cookie.substring(0, 20);
const suffix = cookie.substring(Math.max(0, cookie.length - 20));
return `${prefix}...${suffix}`;
}
/**
* 清理过期数据
* @param {number} daysToKeep - 保留多少天的数据
*/
cleanupOldData(daysToKeep = 30) {
try {
const dataDir = path.dirname(this.dataFilePath);
const files = fs.readdirSync(dataDir);
const now = Date.now();
const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
files.forEach(file => {
if (file.startsWith('cookies-data') && file.endsWith('.backup.json')) {
const filePath = path.join(dataDir, file);
const stats = fs.statSync(filePath);
if (now - stats.mtime.getTime() > maxAge) {
fs.unlinkSync(filePath);
logger.info(`删除过期备份文件: ${file}`);
}
}
});
} catch (error) {
logger.error(`清理过期数据失败: ${error.message}`);
}
}
}
export const storageManager = new StorageManager();