mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-06-16 03:40:53 -04:00
feat: implement backup functionality using streaming zip creation
This commit is contained in:
123
main.js
123
main.js
@@ -19,6 +19,7 @@ const log = require("electron-log/main");
|
||||
const os = require("os");
|
||||
const store = new Store();
|
||||
const fs = require("fs");
|
||||
const JSZip = require("jszip");
|
||||
const configDir = app.getPath("userData");
|
||||
const dirPath = path.join(configDir, "uploads");
|
||||
const packageJson = require("./package.json");
|
||||
@@ -198,6 +199,116 @@ const removePickerUtil = (config) => {
|
||||
pickerUtilCache[config.service] = null;
|
||||
}
|
||||
};
|
||||
const addDirectoryToZip = async (zip, sourceDir, zipDir) => {
|
||||
const entries = await fs.promises.readdir(sourceDir, { withFileTypes: true });
|
||||
|
||||
if (entries.length === 0) {
|
||||
zip.file(zipDir, null, { dir: true, createFolders: true });
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const zipPath = path.posix.join(zipDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await addDirectoryToZip(zip, sourcePath, zipPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stats = await fs.promises.stat(sourcePath);
|
||||
zip.file(zipPath, fs.createReadStream(sourcePath), {
|
||||
binary: true,
|
||||
createFolders: true,
|
||||
date: stats.mtime,
|
||||
});
|
||||
}
|
||||
};
|
||||
const createBackupArchive = async (config) => {
|
||||
const { dataPath, targetPath, fileName, databaseList = [] } = config;
|
||||
const destinationPath = path.join(targetPath, fileName);
|
||||
const tempPath = destinationPath + ".tmp";
|
||||
const zip = new JSZip();
|
||||
|
||||
await fs.promises.mkdir(targetPath, { recursive: true });
|
||||
|
||||
if (fs.existsSync(tempPath)) {
|
||||
await fs.promises.unlink(tempPath);
|
||||
}
|
||||
|
||||
const directories = [
|
||||
{ source: path.join(dataPath, "book"), target: "book" },
|
||||
{ source: path.join(dataPath, "cover"), target: "cover" },
|
||||
];
|
||||
for (const directory of directories) {
|
||||
if (fs.existsSync(directory.source)) {
|
||||
await addDirectoryToZip(zip, directory.source, directory.target);
|
||||
}
|
||||
}
|
||||
|
||||
const configFiles = ["config.json", "sync.json"];
|
||||
for (const configFile of configFiles) {
|
||||
const sourcePath = path.join(dataPath, "config", configFile);
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
continue;
|
||||
}
|
||||
const stats = await fs.promises.stat(sourcePath);
|
||||
zip.file(path.posix.join("config", configFile), fs.createReadStream(sourcePath), {
|
||||
binary: true,
|
||||
createFolders: true,
|
||||
date: stats.mtime,
|
||||
});
|
||||
}
|
||||
|
||||
for (const dbName of databaseList) {
|
||||
const sourcePath = path.join(dataPath, "config", `${dbName}.db`);
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
continue;
|
||||
}
|
||||
const stats = await fs.promises.stat(sourcePath);
|
||||
zip.file(path.posix.join("config", `${dbName}.db`), fs.createReadStream(sourcePath), {
|
||||
binary: true,
|
||||
createFolders: true,
|
||||
date: stats.mtime,
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(tempPath);
|
||||
const stream = zip.generateNodeStream({
|
||||
type: "nodebuffer",
|
||||
streamFiles: true,
|
||||
compression: "DEFLATE",
|
||||
compressionOptions: { level: 6 },
|
||||
});
|
||||
|
||||
const handleError = async (error) => {
|
||||
output.destroy();
|
||||
if (fs.existsSync(tempPath)) {
|
||||
try {
|
||||
await fs.promises.unlink(tempPath);
|
||||
} catch (_) {}
|
||||
}
|
||||
reject(error);
|
||||
};
|
||||
|
||||
output.on("close", resolve);
|
||||
output.on("error", handleError);
|
||||
stream.on("error", handleError);
|
||||
stream.pipe(output);
|
||||
});
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
await fs.promises.unlink(destinationPath);
|
||||
}
|
||||
|
||||
await fs.promises.rename(tempPath, destinationPath);
|
||||
return destinationPath;
|
||||
};
|
||||
// Simple encryption function
|
||||
const encrypt = (text, key) => {
|
||||
let result = "";
|
||||
@@ -696,6 +807,18 @@ const createMainWin = () => {
|
||||
});
|
||||
return result.filePaths[0];
|
||||
});
|
||||
ipcMain.handle("stream-backup-zip", async (event, config) => {
|
||||
try {
|
||||
await createBackupArchive(config);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to create backup archive:", error);
|
||||
return {
|
||||
ok: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
});
|
||||
ipcMain.handle("encrypt-data", async (event, config) => {
|
||||
const { TokenService } =
|
||||
await import("./src/assets/lib/kookit-extra.min.mjs");
|
||||
|
||||
@@ -44,7 +44,10 @@ export const backup = async (service: string): Promise<Boolean> => {
|
||||
});
|
||||
// 让 UI 有时间渲染 toast
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await backupFromPath(targetPath, fileName);
|
||||
const backupResult = await backupFromPath(targetPath, fileName);
|
||||
if (!backupResult) {
|
||||
return false;
|
||||
}
|
||||
if (service === "local") {
|
||||
return true;
|
||||
} else {
|
||||
@@ -158,44 +161,37 @@ export const getSnapshots = () => {
|
||||
};
|
||||
export const backupFromPath = async (targetPath: string, fileName: string) => {
|
||||
const path = window.require("path");
|
||||
const AdmZip = window.require("adm-zip");
|
||||
const dataPath = getStorageLocation() || "";
|
||||
let zip = new AdmZip();
|
||||
const fs = window.require("fs");
|
||||
const { ipcRenderer } = window.require("electron");
|
||||
if (!fs.existsSync(path.join(targetPath))) {
|
||||
fs.mkdirSync(path.join(targetPath), { recursive: true });
|
||||
}
|
||||
await backupToConfigJson();
|
||||
|
||||
if (fs.existsSync(path.join(dataPath, "book"))) {
|
||||
zip.addLocalFolder(path.join(dataPath, "book"), "book");
|
||||
}
|
||||
if (fs.existsSync(path.join(dataPath, "cover"))) {
|
||||
zip.addLocalFolder(path.join(dataPath, "cover"), "cover");
|
||||
}
|
||||
if (fs.existsSync(path.join(dataPath, "config", "config.json"))) {
|
||||
zip.addLocalFile(path.join(dataPath, "config", "config.json"), "config");
|
||||
}
|
||||
if (fs.existsSync(path.join(dataPath, "config", "sync.json"))) {
|
||||
zip.addLocalFile(path.join(dataPath, "config", "sync.json"), "config");
|
||||
}
|
||||
await backupToSyncJson();
|
||||
let databaseList = CommonTool.databaseList;
|
||||
for (let i = 0; i < databaseList.length; i++) {
|
||||
await window.require("electron").ipcRenderer.invoke("close-database", {
|
||||
await ipcRenderer.invoke("close-database", {
|
||||
dbName: databaseList[i],
|
||||
storagePath: getStorageLocation(),
|
||||
});
|
||||
if (fs.existsSync(path.join(dataPath, "config", databaseList[i] + ".db"))) {
|
||||
zip.addLocalFile(
|
||||
path.join(dataPath, "config", databaseList[i] + ".db"),
|
||||
"config"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await zip.writeZip(path.join(targetPath, fileName));
|
||||
const result = await ipcRenderer.invoke("stream-backup-zip", {
|
||||
dataPath,
|
||||
targetPath,
|
||||
fileName,
|
||||
databaseList,
|
||||
});
|
||||
if (!result?.ok) {
|
||||
console.error("backup zip stream error:", result?.message);
|
||||
toast.error(result?.message || i18n.t("Backup failed"), {
|
||||
id: "backup",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// return new Blob([zip.toBuffer()], { type: "application/zip" });
|
||||
return true;
|
||||
};
|
||||
export const backupFromStorage = async () => {
|
||||
let zip = new JSZip();
|
||||
|
||||
Reference in New Issue
Block a user