347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
|
|
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)
|
|||
|
|
});
|