feat(backup): enhance backup functionality to include dict, background, and snapshot directories

refactor(common): optimize cover processing in upgradeStorage function
refactor(configUtil): expand config list to include dict and background configurations
refactor(coverUtil): update cover processing to handle async operations
refactor(restore): improve restore functionality to support additional asset types
This commit is contained in:
troyeguo
2026-05-19 17:20:17 +08:00
parent 1bf3d63a4b
commit 204c4c8573
5 changed files with 57 additions and 46 deletions

View File

@@ -2,13 +2,17 @@ import BookUtil from "./bookUtil";
import { isElectron } from "react-device-detect";
import { checkMissingBook, getStorageLocation } from "../common";
import CoverUtil from "./coverUtil";
import { CommonTool } from "../../assets/lib/kookit-extra-browser.min";
import {
CommonTool,
ConfigService,
} from "../../assets/lib/kookit-extra-browser.min";
import { getCloudConfig } from "./common";
import DatabaseService from "../storage/databaseService";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import ConfigUtil from "./configUtil";
import SyncService from "../storage/syncService";
import BackgroundUtil from "./backgroundUtil";
import toast from "react-hot-toast";
import i18n from "../../i18n";
@@ -219,8 +223,8 @@ export const backupFromPath = async (
}
};
// Add book and cover directories
for (const dir of ["book", "cover"]) {
// Add book, cover, dict, background, snapshot directories
for (const dir of ["book", "cover", "dict", "background", "snapshot"]) {
const sourceDir = path.join(dataPath, dir);
if (fs.existsSync(sourceDir)) {
addDirectoryToZip(zip, sourceDir, dir);
@@ -297,6 +301,7 @@ export const backupFromStorage = async () => {
let sync = JSON.stringify(await ConfigUtil.dumpConfig("sync"));
await zipCover(zip);
await zipBook(zip);
await zipBackground(zip);
let result = await zipConfig(
zip,
books,
@@ -381,17 +386,29 @@ export const zipCover = async (zip: any) => {
} else {
for (let i = 0; i < books.length; i++) {
let cover = await CoverUtil.getCover(books[i]);
if (cover.startsWith("blob")) {
let response = await fetch(cover);
let blob = await response.blob();
cover = await CoverUtil.blobToBase64(blob);
}
const result = CoverUtil.convertCoverBase64(cover);
const result = await CoverUtil.convertCoverBase64(cover);
coverZip.file(`${books[i].key}.${result.extension}`, result.arrayBuffer);
}
}
};
export const zipBackground = async (zip: any) => {
const backgroundIds = ConfigService.getAllListConfig("backgroundList") || [];
const bgZip = zip.folder("background");
for (const id of backgroundIds) {
const meta = BackgroundUtil.getImageMeta(id);
if (!meta) continue;
try {
const dataUrl = await BackgroundUtil.loadImage(id, meta.extension);
if (!dataUrl) continue;
const { arrayBuffer, extension } = BackgroundUtil.convertDataUrl(dataUrl);
bgZip.file(`${id}.${extension}`, arrayBuffer);
} catch (error) {
console.error(`Failed to backup background ${id}:`, error);
}
}
};
export const zipConfig = (
zip: any,
bookBuffer: ArrayBuffer,

View File

@@ -132,21 +132,21 @@ export const upgradeStorage = async (
fs.mkdirSync(path.join(dataPath, "cover"), { recursive: true });
let books: Book[] | null = await localforage.getItem("books");
if (books && books.length > 0) {
books.forEach((item) => {
let cover = item.cover;
for (let i = 0; i < books.length; i++) {
let cover = books[i].cover;
if (cover) {
let result = CoverUtil.convertCoverBase64(cover);
let result = await CoverUtil.convertCoverBase64(cover);
fs.writeFileSync(
path.join(dataPath, "cover", `${item.key}.${result.extension}`),
path.join(dataPath, "cover", `${books[i].key}.${result.extension}`),
Buffer.from(result.arrayBuffer)
);
item.cover = "";
books[i].cover = "";
}
//fix sqlite3 text issue
if (typeof item.author !== "string") {
item.author = "";
if (typeof books[i].author !== "string") {
books[i].author = "";
}
});
}
await DatabaseService.saveAllRecords(books, "books");
}

View File

@@ -470,7 +470,14 @@ class ConfigUtil {
config = ConfigService.getAllSyncRecord();
} else {
let configList = CommonTool.configList;
configList = [...configList, "readerConfig"];
configList = [
...configList,
"dictList",
"backgroundList",
"readerConfig",
"customBackgrounds",
"customDicts",
];
for (let i = 0; i < configList.length; i++) {
let item = configList[i];
if (ConfigService.getItem(item)) {

View File

@@ -206,7 +206,7 @@ class CoverUtil {
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, { recursive: true });
}
const result = this.convertCoverBase64(book.cover);
const result = await this.convertCoverBase64(book.cover);
fs.writeFileSync(
path.join(directoryPath, `${book.key}.${result.extension}`),
Buffer.from(result.arrayBuffer)
@@ -217,7 +217,7 @@ class CoverUtil {
book.cover = "";
} else {
if (ConfigService.getReaderConfig("isUseLocal") === "yes") {
let result = this.convertCoverBase64(coverBase64);
let result = await this.convertCoverBase64(coverBase64);
await LocalFileManager.saveFile(
`${book.key}.${result.extension}`,
result.arrayBuffer,
@@ -230,7 +230,12 @@ class CoverUtil {
// book.cover = "";
}
}
static convertCoverBase64(base64: string) {
static async convertCoverBase64(base64: string) {
if (base64.startsWith("blob") || base64.startsWith("http")) {
let response = await fetch(base64);
let blob = await response.blob();
base64 = await CoverUtil.blobToBase64(blob);
}
let extension = this.base64ToFileType(base64);
const base64Data = base64.replace(/^data:.*;base64,/, "");
@@ -377,7 +382,8 @@ class CoverUtil {
await syncUtil.uploadFile(cover, "cover", coverBuffer);
} else {
if (book && book.cover) {
let result = this.convertCoverBase64(book.cover);
let base64 = book.cover;
let result = await this.convertCoverBase64(base64);
let coverBlob = new Blob([result.arrayBuffer], {
type: `image/${result.extension}`,
});

View File

@@ -189,14 +189,6 @@ export const restoreFromfilePath = async (filePath: string) => {
toast.loading(i18n.t("Restoring...") + ` (${percent}%)`, { id: "backup" });
};
const streamToBuffer = (stream: any): Promise<Buffer> =>
new Promise((res, rej) => {
const chunks: any[] = [];
stream.on("data", (chunk: any) => chunks.push(chunk));
stream.on("end", () => res(Buffer.concat(chunks)));
stream.on("error", rej);
});
const streamToFile = (stream: any, dest: string): Promise<void> =>
new Promise((res, rej) => {
const dir = path.dirname(dest);
@@ -250,7 +242,11 @@ export const restoreFromfilePath = async (filePath: string) => {
const assetFiles = Object.keys(zip.files).filter(
(name) =>
!zip.files[name].dir &&
(name.startsWith("book/") || name.startsWith("cover/"))
(name.startsWith("book/") ||
name.startsWith("cover/") ||
name.startsWith("dict/") ||
name.startsWith("background/") ||
name.startsWith("snapshot/"))
);
await Promise.all(
assetFiles.map(async (fileName) => {
@@ -287,20 +283,6 @@ export const restoreFromOldBackup = async (zipEntries: any) => {
return false;
}
};
export const restoreFromNewBackup = async (zipEntries: any) => {
let result = await unzipConfig(zipEntries);
if (result) {
let res1 = await unzipBook(zipEntries);
let res2 = await unzipCover(zipEntries);
if (res1 || res2) {
return true;
} else {
return false;
}
} else {
return false;
}
};
export const unzipConfig = async (zipEntries: any) => {
const fs = window.require("fs");
const path = window.require("path");
@@ -416,7 +398,6 @@ export const unzipCover = async (zipEntries: any) => {
}
return flag;
};
export const unzipOldConfig = (zipEntries: any) => {
return new Promise<boolean>((resolve) => {
zipEntries.forEach(function (zipEntry) {