fix(备份): 修复'源数据库文件不存在'报错; 改为内存DB直出备份并热重载还原(免重启)
Deploy to Server / deploy (push) Successful in 1m52s

This commit is contained in:
Trae Assistant
2026-10-07 09:50:09 +08:00
parent 6b4bce4dcf
commit 28ba850bfa
4 changed files with 81 additions and 29 deletions
+22 -13
View File
@@ -1,6 +1,7 @@
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { getDataDir as dbGetDataDir, getDbPath as dbGetDbPath, getDbBuffer, reloadDbFromBuffer } from './db';
const TZ_OFFSET_HOURS = 8;
const BACKUP_FILENAME_RE = /^volunteer-backup-(\d{8})-(\d{6})-([A-Za-z0-9]{8})\.db$/;
@@ -40,7 +41,7 @@ function fmtIso(d: Date) {
}
export function getDataDir(): string {
const p = path.join(__dirname, '..', '..', 'data');
const p = dbGetDataDir() || path.join(__dirname, '..', '..', 'data');
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
return p;
}
@@ -50,7 +51,7 @@ export function getBackupsDir(): string {
return p;
}
export function getDbPath(): string {
return path.join(getDataDir(), 'volunteer.db');
return dbGetDbPath() || path.join(getDataDir(), 'volunteer.db');
}
function getConfigPath(): string {
return path.join(getDataDir(), 'backup-config.json');
@@ -148,18 +149,25 @@ function cleanupByKeep(cfg: BackupConfig): BackupItem[] {
export function createBackup(opts: { trigger?: 'manual' | 'scheduled'; forceSnapshot?: boolean } = {}): BackupItem {
const trigger = opts.trigger || 'manual';
const src = getDbPath();
if (!fs.existsSync(src)) {
throw new Error('源数据库文件不存在,无法备份');
}
const dir = getBackupsDir();
const stamp = fmtStamp(utc8Now());
const rand = crypto.randomBytes(4).toString('hex');
const prefix = opts.forceSnapshot ? 'volunteer-restoresnap' : 'volunteer-backup';
const filename = `${prefix}-${stamp}-${rand}.db`;
const dest = path.join(dir, filename);
// copy with integrity: read -> verify sha -> write
const buf = fs.readFileSync(src);
// 获取 DB 原始二进制:优先从内存 DB 直接 export,磁盘文件仅作为兜底回退路径,避免路径不一致
let buf: Buffer;
try {
const mem = getDbBuffer();
buf = Buffer.from(mem);
} catch (_e) {
const src = getDbPath();
if (!fs.existsSync(src)) {
throw new Error('源数据库文件不存在,无法备份');
}
buf = fs.readFileSync(src);
}
if (!buf || buf.length < 100) throw new Error('数据库为空,已中止备份');
fs.writeFileSync(dest, buf);
const verify = fs.readFileSync(dest);
if (verify.length !== buf.length || crypto.createHash('sha256').update(buf).digest('hex') !== crypto.createHash('sha256').update(verify).digest('hex')) {
@@ -192,17 +200,18 @@ export function deleteBackup(filename: string): { ok: boolean; deleted: boolean
return { ok: true, deleted: true };
}
export function restoreBackup(filename: string): { ok: true; snapshot: BackupItem } {
export function restoreBackup(filename: string): { ok: true; snapshot: BackupItem; db_reinitialized: boolean } {
if (!safeFilename(filename)) throw new Error('备份文件名非法');
const p = path.join(getBackupsDir(), filename);
if (!fs.existsSync(p)) throw new Error('备份文件不存在');
const src = getDbPath();
// 1) 先对当前数据库拍一张 restoresnap,防止误操作
const snap = createBackup({ trigger: 'manual', forceSnapshot: true });
// 2) 覆盖 database file
// 2) 覆盖磁盘 DB 文件,并同时 reload 运行中的内存 DB(避免必须重启进程才生效,也绕过磁盘路径不一致)
const buf = fs.readFileSync(p);
fs.writeFileSync(src, buf);
return { ok: true, snapshot: snap };
if (!buf || buf.length < 100) throw new Error('备份文件过短,已中止还原');
reloadDbFromBuffer(new Uint8Array(buf));
fs.writeFileSync(getDbPath(), buf);
return { ok: true, snapshot: snap, db_reinitialized: true };
}
export function getNextRunMs(cfg: BackupConfig): number | null {