fix:优化备份和恢复功能

This commit is contained in:
初衷
2025-08-14 14:36:39 +08:00
parent a8fb9a1dd8
commit 6b48c7fd0d
3 changed files with 229 additions and 129 deletions

View File

@@ -0,0 +1,157 @@
/**
* 恢复功能检查工具
*/
import { getDatabase } from '../../utils/databaseAdapter.js';
export async function onRequest(context) {
var env = context.env;
var url = new URL(context.request.url);
var action = url.searchParams.get('action') || 'status';
try {
var db = getDatabase(env);
var results = {
action: action,
timestamp: new Date().toISOString()
};
if (action === 'status') {
// 检查当前数据库状态
// 统计文件数量
var fileCount = 0;
var cursor = null;
while (true) {
var response = await db.listFiles({
limit: 1000,
cursor: cursor
});
if (!response || !response.keys || !Array.isArray(response.keys)) {
break;
}
for (var item of response.keys) {
if (!item.name.startsWith('manage@') && !item.name.startsWith('chunk_')) {
if (item.metadata && item.metadata.TimeStamp) {
fileCount++;
}
}
}
cursor = response.cursor;
if (!cursor) break;
}
// 统计设置数量
var settingsResponse = await db.listSettings({});
var settingsCount = 0;
if (settingsResponse && settingsResponse.keys) {
settingsCount = settingsResponse.keys.length;
}
// 检查关键设置
var keySettings = {};
var settingKeys = ['manage@sysConfig@page', 'manage@sysConfig@security'];
for (var key of settingKeys) {
try {
var value = await db.get(key);
keySettings[key] = {
exists: !!value,
length: value ? value.length : 0
};
} catch (error) {
keySettings[key] = {
exists: false,
error: error.message
};
}
}
results.status = {
fileCount: fileCount,
settingsCount: settingsCount,
keySettings: keySettings
};
} else if (action === 'test') {
// 测试恢复一个简单的设置
var testKey = 'test_restore_' + Date.now();
var testValue = 'test_value_' + Date.now();
try {
// 写入测试数据
await db.put(testKey, testValue);
// 读取验证
var retrieved = await db.get(testKey);
// 清理测试数据
await db.delete(testKey);
results.test = {
success: true,
valueMatch: retrieved === testValue,
testKey: testKey,
testValue: testValue,
retrievedValue: retrieved
};
} catch (error) {
results.test = {
success: false,
error: error.message
};
}
} else if (action === 'sample') {
// 提供样本恢复数据
results.sampleData = {
timestamp: Date.now(),
version: "2.0.2",
data: {
fileCount: 1,
files: {
"test_file_" + Date.now(): {
metadata: {
FileName: "test.jpg",
FileType: "image/jpeg",
FileSize: "0.1",
TimeStamp: Date.now(),
Channel: "Test",
ListType: "None"
},
value: null
}
},
settings: {
"test_setting_" + Date.now(): "test_value_" + Date.now()
}
}
};
results.instructions = {
usage: "Use this sample data to test restore functionality",
endpoint: "/api/manage/sysConfig/backup",
method: "POST with action=restore"
};
}
return new Response(JSON.stringify(results, null, 2), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
return new Response(JSON.stringify({
error: error.message,
stack: error.stack
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
}

View File

@@ -1,103 +0,0 @@
/**
* 恢复功能测试工具
*/
import { getDatabase } from '../../utils/databaseAdapter.js';
export async function onRequest(context) {
var env = context.env;
var request = context.request;
try {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
var testData = await request.json();
var db = getDatabase(env);
var results = {
settings: [],
files: [],
errors: []
};
// 测试恢复设置
if (testData.settings) {
for (var key in testData.settings) {
try {
var value = testData.settings[key];
await db.put(key, value);
// 验证是否成功保存
var retrieved = await db.get(key);
results.settings.push({
key: key,
saved: true,
retrieved: retrieved !== null,
valueMatch: retrieved === value,
originalLength: value.length,
retrievedLength: retrieved ? retrieved.length : 0
});
} catch (error) {
results.errors.push({
type: 'setting',
key: key,
error: error.message
});
}
}
}
// 测试恢复文件
if (testData.files) {
for (var fileId in testData.files) {
try {
var fileData = testData.files[fileId];
if (fileData.value) {
await db.put(fileId, fileData.value, {
metadata: fileData.metadata
});
} else if (fileData.metadata) {
await db.put(fileId, '', {
metadata: fileData.metadata
});
}
// 验证是否成功保存
var retrieved = await db.getWithMetadata(fileId);
results.files.push({
fileId: fileId,
saved: true,
retrieved: retrieved !== null,
hasMetadata: retrieved && retrieved.metadata !== null
});
} catch (error) {
results.errors.push({
type: 'file',
fileId: fileId,
error: error.message
});
}
}
}
return new Response(JSON.stringify(results, null, 2), {
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
return new Response(JSON.stringify({
error: error.message,
stack: error.stack
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
}

View File

@@ -42,23 +42,53 @@ async function handleBackup(context) {
}
};
// 首先从索引中读取所有文件信息
const indexResult = await readIndex(context, {
count: -1, // 获取所有文件
start: 0,
includeSubdirFiles: true // 包含子目录下的文件
});
backupData.data.fileCount = indexResult.files.length;
// 直接从数据库读取所有文件信息,不依赖索引
const db = getDatabase(env);
let allFiles = [];
let cursor = null;
// 分批获取所有文件
while (true) {
const response = await db.listFiles({
limit: 1000,
cursor: cursor
});
if (!response || !response.keys || !Array.isArray(response.keys)) {
break;
}
for (const item of response.keys) {
// 跳过管理相关的键和分块数据
if (item.name.startsWith('manage@') || item.name.startsWith('chunk_')) {
continue;
}
// 跳过没有元数据的文件
if (!item.metadata || !item.metadata.TimeStamp) {
continue;
}
allFiles.push({
id: item.name,
metadata: item.metadata
});
}
cursor = response.cursor;
if (!cursor) break;
}
backupData.data.fileCount = allFiles.length;
// 备份文件数据
for (const file of indexResult.files) {
for (const file of allFiles) {
const fileId = file.id;
const metadata = file.metadata;
// 对于TelegramNew渠道且IsChunked为true的文件需要从数据库读取其值
if (metadata.Channel === 'TelegramNew' && metadata.IsChunked === true) {
try {
const db = getDatabase(env);
const fileData = await db.getWithMetadata(fileId);
backupData.data.files[fileId] = {
metadata: metadata,
@@ -82,7 +112,7 @@ async function handleBackup(context) {
}
// 备份系统设置
const db = getDatabase(env);
// db 已经在上面定义了
// 备份所有设置不仅仅是manage@开头的
const allSettingsList = await db.listSettings({});
@@ -149,27 +179,42 @@ async function handleRestore(request, env) {
// 恢复文件数据
const db = getDatabase(env);
for (const [key, fileData] of Object.entries(backupData.data.files)) {
try {
if (fileData.value) {
// 对于有value的文件如telegram分块文件恢复完整数据
await db.put(key, fileData.value, {
metadata: fileData.metadata
});
} else if (fileData.metadata) {
// 只恢复元数据
await db.put(key, '', {
metadata: fileData.metadata
});
const fileEntries = Object.entries(backupData.data.files);
const batchSize = 50; // 批量处理,避免超时
for (let i = 0; i < fileEntries.length; i += batchSize) {
const batch = fileEntries.slice(i, i + batchSize);
for (const [key, fileData] of batch) {
try {
if (fileData.value) {
// 对于有value的文件如telegram分块文件恢复完整数据
await db.put(key, fileData.value, {
metadata: fileData.metadata
});
} else if (fileData.metadata) {
// 只恢复元数据
await db.put(key, '', {
metadata: fileData.metadata
});
}
restoredFiles++;
} catch (error) {
console.error(`恢复文件 ${key} 失败:`, error);
}
restoredFiles++;
} catch (error) {
console.error(`恢复文件 ${key} 失败:`, error);
}
// 每批处理后短暂暂停,避免过载
if (i + batchSize < fileEntries.length) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// 恢复系统设置
for (const [key, value] of Object.entries(backupData.data.settings)) {
const settingEntries = Object.entries(backupData.data.settings);
console.log(`开始恢复 ${settingEntries.length} 个设置`);
for (const [key, value] of settingEntries) {
try {
console.log(`恢复设置: ${key}, 长度: ${value.length}`);
await db.put(key, value);
@@ -181,6 +226,7 @@ async function handleRestore(request, env) {
console.log(`设置 ${key} 恢复成功`);
} else {
console.error(`设置 ${key} 恢复后验证失败`);
console.error(`原始长度: ${value.length}, 检索长度: ${retrieved ? retrieved.length : 'null'}`);
}
} catch (error) {
console.error(`恢复设置 ${key} 失败:`, error);