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)
|
||
});
|