Initial commit
This commit is contained in:
346
preload.js
Normal file
346
preload.js
Normal file
@@ -0,0 +1,346 @@
|
||||
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)
|
||||
});
|
||||
Reference in New Issue
Block a user