Files
backup-manager/preload.js
2026-02-01 15:12:59 +08:00

347 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { contextBridge, ipcRenderer } = require("electron");
const path = require("path");
const fs = require("fs");
// ============ 数据库管理纯JSON实现无需sql.js============
class SimpleDB {
constructor(dataPath) {
this.dataPath = dataPath;
this.currentDbName = null;
this.currentData = null;
}
getDbPath(name) {
return path.join(this.dataPath, `${name}.json`);
}
getStoragePath(name) {
return path.join(this.dataPath, name);
}
listDatabases() {
if (!fs.existsSync(this.dataPath)) return [];
const files = fs.readdirSync(this.dataPath);
const databases = [];
for (const file of files) {
if (file.endsWith(".json")) {
const name = file.replace(".json", "");
try {
const data = JSON.parse(fs.readFileSync(this.getDbPath(name), "utf8"));
const totalSize = data.backups.reduce((sum, b) => sum + (b.file_size || 0), 0);
databases.push({ name, count: data.backups.length, totalSize });
} catch {
databases.push({ name, count: 0, totalSize: 0 });
}
}
}
return databases;
}
createDatabase(name) {
const dbPath = this.getDbPath(name);
const storagePath = this.getStoragePath(name);
if (fs.existsSync(dbPath)) {
throw new Error(`数据库 "${name}" 已存在`);
}
fs.mkdirSync(storagePath, { recursive: true });
fs.writeFileSync(dbPath, JSON.stringify({ backups: [] }, null, 2), "utf8");
return true;
}
deleteDatabase(name) {
const dbPath = this.getDbPath(name);
const storagePath = this.getStoragePath(name);
if (this.currentDbName === name) {
this.currentDbName = null;
this.currentData = null;
}
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
if (fs.existsSync(storagePath)) {
fs.rmSync(storagePath, { recursive: true, force: true });
}
return true;
}
renameDatabase(oldName, newName) {
const oldDbPath = this.getDbPath(oldName);
const newDbPath = this.getDbPath(newName);
const oldStoragePath = this.getStoragePath(oldName);
const newStoragePath = this.getStoragePath(newName);
if (fs.existsSync(newDbPath)) {
throw new Error(`数据库 "${newName}" 已存在`);
}
// 读取并更新数据
const data = JSON.parse(fs.readFileSync(oldDbPath, "utf8"));
data.backups = data.backups.map(b => ({
...b,
filename: b.filename.replace(oldName, newName)
}));
// 保存新数据库
fs.writeFileSync(newDbPath, JSON.stringify(data, null, 2), "utf8");
fs.unlinkSync(oldDbPath);
// 重命名存储目录和文件
if (fs.existsSync(oldStoragePath)) {
fs.renameSync(oldStoragePath, newStoragePath);
const files = fs.readdirSync(newStoragePath);
for (const file of files) {
if (file.startsWith(oldName)) {
const newFilename = file.replace(oldName, newName);
fs.renameSync(
path.join(newStoragePath, file),
path.join(newStoragePath, newFilename)
);
}
}
}
if (this.currentDbName === oldName) {
this.currentDbName = newName;
this.currentData = data;
}
return true;
}
openDatabase(name) {
const dbPath = this.getDbPath(name);
if (!fs.existsSync(dbPath)) {
throw new Error(`数据库 "${name}" 不存在`);
}
this.currentDbName = name;
this.currentData = JSON.parse(fs.readFileSync(dbPath, "utf8"));
return true;
}
saveDatabase() {
if (this.currentDbName && this.currentData) {
fs.writeFileSync(
this.getDbPath(this.currentDbName),
JSON.stringify(this.currentData, null, 2),
"utf8"
);
}
}
getBackups() {
if (!this.currentData) return [];
return [...this.currentData.backups].sort((a, b) =>
b.date_modified.localeCompare(a.date_modified)
);
}
findByDateModified(dateModified) {
if (!this.currentData) return null;
return this.currentData.backups.find(b => b.date_modified === dateModified) || null;
}
upsertBackup(info) {
if (!this.currentData) throw new Error("没有打开的数据库");
const idx = this.currentData.backups.findIndex(b => b.date_modified === info.date_modified);
if (idx >= 0) {
this.currentData.backups[idx] = { ...this.currentData.backups[idx], ...info };
this.saveDatabase();
return { action: "updated", id: idx };
} else {
const id = Date.now();
this.currentData.backups.push({ id, ...info });
this.saveDatabase();
return { action: "inserted", id };
}
}
updateTag(id, tag) {
if (!this.currentData) throw new Error("没有打开的数据库");
const backup = this.currentData.backups.find(b => b.id === id);
if (backup) {
backup.tag = tag;
this.saveDatabase();
}
return true;
}
deleteBackup(id) {
if (!this.currentData) throw new Error("没有打开的数据库");
const idx = this.currentData.backups.findIndex(b => b.id === id);
if (idx >= 0) {
const backup = this.currentData.backups[idx];
const filePath = path.join(this.getStoragePath(this.currentDbName), backup.filename);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
this.currentData.backups.splice(idx, 1);
this.saveDatabase();
}
return true;
}
getCurrentDbName() { return this.currentDbName; }
getCurrentStoragePath() {
return this.currentDbName ? this.getStoragePath(this.currentDbName) : null;
}
}
// ============ 压缩工具 ============
const archiver = require("archiver");
function compressFolder(folderPath, outputPath) {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(outputPath);
const archive = archiver("zip", { zlib: { level: 9 } });
output.on("close", () => resolve({ size: archive.pointer(), path: outputPath }));
archive.on("error", reject);
archive.pipe(output);
archive.directory(folderPath, false);
archive.finalize();
});
}
function copyArchive(source, dest) {
return new Promise((resolve, reject) => {
fs.copyFile(source, dest, err => err ? reject(err) : resolve(dest));
});
}
function isSupportedArchive(filePath) {
const ext = path.extname(filePath).toLowerCase();
return [".zip", ".rar", ".7z", ".tar", ".gz"].includes(ext);
}
function getArchiveType(filePath) {
return path.extname(filePath).toLowerCase().replace(".", "");
}
// ============ 文件工具 ============
function getLatestModifiedTime(folderPath) {
let latest = new Date(0);
function scan(dir) {
const items = fs.readdirSync(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stats = fs.statSync(itemPath);
if (stats.isDirectory()) {
scan(itemPath);
} else if (stats.mtime > latest) {
latest = stats.mtime;
}
}
}
scan(folderPath);
return latest;
}
function formatDateModified(date) {
const d = new Date(date);
const pad = n => String(n).padStart(2, "0");
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}`;
}
function formatDateDisplay(str) {
if (!str || str.length !== 12) return str;
return `${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)} ${str.slice(8, 10)}:${str.slice(10, 12)}`;
}
function formatFileSize(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
}
function generateBackupFilename(dbName, dateModified, archiveType) {
return `${dbName}-${dateModified}.${archiveType}`;
}
function getBaseName(filename) {
return path.basename(filename, path.extname(filename));
}
function isNameMatching(filename, dbName) {
return getBaseName(filename).toLowerCase().startsWith(dbName.toLowerCase());
}
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
return dirPath;
}
// ============ 初始化数据库 ============
let db = null;
// ============ 暴露API ============
contextBridge.exposeInMainWorld("electronAPI", {
// 窗口控制
minimize: () => ipcRenderer.invoke("window-minimize"),
maximize: () => ipcRenderer.invoke("window-maximize"),
close: () => ipcRenderer.invoke("window-close"),
// 路径
getDataPath: () => ipcRenderer.invoke("get-data-path"),
selectExportDir: () => ipcRenderer.invoke("select-export-dir"),
showInFolder: (p) => ipcRenderer.invoke("show-in-folder", p),
// 初始化
initDatabase: async () => {
const dataPath = await ipcRenderer.invoke("get-data-path");
db = new SimpleDB(dataPath);
return dataPath;
},
// 数据库操作
listDatabases: () => db ? db.listDatabases() : [],
createDatabase: (name) => { if (!db) throw new Error("未初始化"); return db.createDatabase(name); },
deleteDatabase: (name) => { if (!db) throw new Error("未初始化"); return db.deleteDatabase(name); },
renameDatabase: (o, n) => { if (!db) throw new Error("未初始化"); return db.renameDatabase(o, n); },
openDatabase: (name) => { if (!db) throw new Error("未初始化"); return db.openDatabase(name); },
getBackups: () => db ? db.getBackups() : [],
findByDateModified: (d) => db ? db.findByDateModified(d) : null,
upsertBackup: (info) => { if (!db) throw new Error("未初始化"); return db.upsertBackup(info); },
updateTag: (id, tag) => { if (!db) throw new Error("未初始化"); return db.updateTag(id, tag); },
deleteBackup: (id) => { if (!db) throw new Error("未初始化"); return db.deleteBackup(id); },
getCurrentDbName: () => db ? db.getCurrentDbName() : null,
getCurrentStoragePath: () => db ? db.getCurrentStoragePath() : null,
// 文件操作
getFileStats: (p) => {
try {
const s = fs.statSync(p);
return { size: s.size, mtime: s.mtime.toISOString(), isDirectory: s.isDirectory() };
} catch { return null; }
},
pathExists: (p) => fs.existsSync(p),
copyFile: (s, d) => { try { fs.copyFileSync(s, d); return true; } catch { return false; } },
deleteFile: (p) => { try { if (fs.existsSync(p)) fs.unlinkSync(p); return true; } catch { return false; } },
ensureDir,
// 压缩
compressFolder,
copyArchive,
isSupportedArchive,
getArchiveType,
// 工具
getLatestModifiedTime,
formatDateModified,
formatDateDisplay,
formatFileSize,
generateBackupFilename,
getBaseName,
isNameMatching,
pathJoin: (...args) => path.join(...args),
pathBasename: (p) => path.basename(p)
});