feat(BackupManager): replace AdmZip with archiver for improved backup compression and add extract-zip for unzipping functionality

- Updated BackupManager to use archiver for creating ZIP files, enabling better performance and support for large files.
- Integrated extract-zip for unzipping backup files, enhancing the backup restoration process.
- Adjusted progress reporting during backup and restore operations for better user feedback.
- Updated package.json and yarn.lock to include archiver and extract-zip dependencies.
This commit is contained in:
kangfenmao 2025-04-22 18:03:16 +08:00
parent 80618b2331
commit 0fa10627bc
5 changed files with 287 additions and 21 deletions

View File

@ -77,6 +77,7 @@
"@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"bufferutil": "^4.0.9",
"color": "^5.0.0",
@ -87,12 +88,14 @@
"electron-updater": "^6.3.9",
"electron-window-state": "^5.0.3",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"extract-zip": "^2.0.1",
"fast-xml-parser": "^5.2.0",
"fetch-socks": "^1.3.2",
"fs-extra": "^11.2.0",
"got-scraping": "^4.1.1",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.1",
"proxy-agent": "^6.5.0",

View File

@ -1,9 +1,10 @@
import { IpcChannel } from '@shared/IpcChannel'
import { WebDavConfig } from '@types'
import AdmZip from 'adm-zip'
import archiver from 'archiver'
import { exec } from 'child_process'
import { app } from 'electron'
import Logger from 'electron-log'
import extract from 'extract-zip'
import * as fs from 'fs-extra'
import * as path from 'path'
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
@ -91,6 +92,7 @@ class BackupManager {
// 使用流的方式写入 data.json
const tempDataPath = path.join(this.tempDir, 'data.json')
await new Promise<void>((resolve, reject) => {
const writeStream = fs.createWriteStream(tempDataPath)
writeStream.write(data)
@ -99,6 +101,7 @@ class BackupManager {
writeStream.on('finish', () => resolve())
writeStream.on('error', (error) => reject(error))
})
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录
@ -112,18 +115,92 @@ class BackupManager {
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'compressing', progress: 80, total: 100 })
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
// 使用 adm-zip 创建压缩文件
const zip = new AdmZip()
zip.addLocalFolder(this.tempDir)
// 创建输出文件流
const backupedFilePath = path.join(destinationPath, fileName)
zip.writeZip(backupedFilePath)
const output = fs.createWriteStream(backupedFilePath)
// 创建 archiver 实例,启用 ZIP64 支持
const archive = archiver('zip', {
zlib: { level: 1 }, // 使用最低压缩级别以提高速度
zip64: true // 启用 ZIP64 支持以处理大文件
})
let lastProgress = 50
let totalEntries = 0
let processedEntries = 0
let totalBytes = 0
let processedBytes = 0
// 首先计算总文件数和总大小
const calculateTotals = async (dirPath: string) => {
const items = await fs.readdir(dirPath, { withFileTypes: true })
for (const item of items) {
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
await calculateTotals(fullPath)
} else {
totalEntries++
const stats = await fs.stat(fullPath)
totalBytes += stats.size
}
}
}
await calculateTotals(this.tempDir)
// 监听文件添加事件
archive.on('entry', () => {
processedEntries++
if (totalEntries > 0) {
const progressPercent = Math.min(55, 50 + Math.floor((processedEntries / totalEntries) * 5))
if (progressPercent > lastProgress) {
lastProgress = progressPercent
onProgress({ stage: 'compressing', progress: progressPercent, total: 100 })
}
}
})
// 监听数据写入事件
archive.on('data', (chunk) => {
processedBytes += chunk.length
if (totalBytes > 0) {
const progressPercent = Math.min(99, 55 + Math.floor((processedBytes / totalBytes) * 44))
if (progressPercent > lastProgress) {
lastProgress = progressPercent
onProgress({ stage: 'compressing', progress: progressPercent, total: 100 })
}
}
})
// 使用 Promise 等待压缩完成
await new Promise<void>((resolve, reject) => {
output.on('close', () => {
onProgress({ stage: 'compressing', progress: 100, total: 100 })
resolve()
})
archive.on('error', reject)
archive.on('warning', (err: any) => {
if (err.code !== 'ENOENT') {
Logger.warn('[BackupManager] Archive warning:', err)
}
})
// 将输出流连接到压缩器
archive.pipe(output)
// 添加整个临时目录到压缩文件
archive.directory(this.tempDir, false)
// 完成压缩
archive.finalize()
})
// 清理临时目录
await fs.remove(this.tempDir)
@ -133,6 +210,8 @@ class BackupManager {
return backupedFilePath
} catch (error) {
Logger.error('[BackupManager] Backup failed:', error)
// 确保清理临时目录
await fs.remove(this.tempDir).catch(() => {})
throw error
}
}
@ -151,16 +230,22 @@ class BackupManager {
onProgress({ stage: 'preparing', progress: 0, total: 100 })
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
// 使用 adm-zip 解压
const zip = new AdmZip(backupPath)
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
onProgress({ stage: 'extracting', progress: 20, total: 100 })
// 使用 extract-zip 解压
await extract(backupPath, {
dir: this.tempDir,
onEntry: () => {
// 这里可以处理进度,但 extract-zip 不提供总条目数信息
onProgress({ stage: 'extracting', progress: 15, total: 100 })
}
})
onProgress({ stage: 'extracting', progress: 25, total: 100 })
Logger.log('[backup] step 2: read data.json')
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
onProgress({ stage: 'reading_data', progress: 40, total: 100 })
onProgress({ stage: 'reading_data', progress: 35, total: 100 })
Logger.log('[backup] step 3: restore Data directory')
// 恢复 Data 目录
@ -177,7 +262,7 @@ class BackupManager {
// 使用流式复制
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
copiedSize += size
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})

View File

@ -57,6 +57,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
BackupPopup.hide = onCancel
const isDisabled = progressData ? progressData.stage !== 'completed' : false
return (
<Modal
title={t('backup.title')}
@ -64,8 +66,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
okButtonProps={{ disabled: isDisabled }}
cancelButtonProps={{ disabled: isDisabled }}
okText={t('backup.confirm.button')}
maskClosable={false}
centered>
{!progressData && <div>{t('backup.content')}</div>}
{progressData && (

View File

@ -57,6 +57,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
RestorePopup.hide = onCancel
const isDisabled = progressData ? progressData.stage !== 'completed' : false
return (
<Modal
title={t('restore.title')}
@ -64,8 +66,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
okText={t('restore.confirm.button')}
okButtonProps={{ disabled: isDisabled }}
cancelButtonProps={{ disabled: isDisabled }}
maskClosable={false}
centered>
{!progressData && <div>{t('restore.content')}</div>}
{progressData && (

182
yarn.lock
View File

@ -4272,6 +4272,7 @@ __metadata:
adm-zip: "npm:^0.5.16"
antd: "npm:^5.22.5"
applescript: "npm:^1.0.0"
archiver: "npm:^7.0.1"
async-mutex: "npm:^0.5.0"
axios: "npm:^1.7.3"
babel-plugin-styled-components: "npm:^2.1.4"
@ -4300,6 +4301,7 @@ __metadata:
eslint-plugin-react-hooks: "npm:^5.2.0"
eslint-plugin-simple-import-sort: "npm:^12.1.1"
eslint-plugin-unused-imports: "npm:^4.1.4"
extract-zip: "npm:^2.0.1"
fast-xml-parser: "npm:^5.2.0"
fetch-socks: "npm:^1.3.2"
fs-extra: "npm:^11.2.0"
@ -4314,6 +4316,7 @@ __metadata:
lucide-react: "npm:^0.487.0"
markdown-it: "npm:^14.1.0"
mime: "npm:^4.0.4"
node-stream-zip: "npm:^1.15.0"
npx-scope-finder: "npm:^1.2.0"
officeparser: "npm:^4.1.1"
openai: "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch"
@ -4748,6 +4751,36 @@ __metadata:
languageName: node
linkType: hard
"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2":
version: 5.0.2
resolution: "archiver-utils@npm:5.0.2"
dependencies:
glob: "npm:^10.0.0"
graceful-fs: "npm:^4.2.0"
is-stream: "npm:^2.0.1"
lazystream: "npm:^1.0.0"
lodash: "npm:^4.17.15"
normalize-path: "npm:^3.0.0"
readable-stream: "npm:^4.0.0"
checksum: 10c0/3782c5fa9922186aa1a8e41ed0c2867569faa5f15c8e5e6418ea4c1b730b476e21bd68270b3ea457daf459ae23aaea070b2b9f90cf90a59def8dc79b9e4ef538
languageName: node
linkType: hard
"archiver@npm:^7.0.1":
version: 7.0.1
resolution: "archiver@npm:7.0.1"
dependencies:
archiver-utils: "npm:^5.0.2"
async: "npm:^3.2.4"
buffer-crc32: "npm:^1.0.0"
readable-stream: "npm:^4.0.0"
readdir-glob: "npm:^1.1.2"
tar-stream: "npm:^3.0.0"
zip-stream: "npm:^6.0.1"
checksum: 10c0/02afd87ca16f6184f752db8e26884e6eff911c476812a0e7f7b26c4beb09f06119807f388a8e26ed2558aa8ba9db28646ebd147a4f99e46813b8b43158e1438e
languageName: node
linkType: hard
"are-we-there-yet@npm:^3.0.0":
version: 3.0.1
resolution: "are-we-there-yet@npm:3.0.1"
@ -4858,7 +4891,7 @@ __metadata:
languageName: node
linkType: hard
"async@npm:^3.2.3":
"async@npm:^3.2.3, async@npm:^3.2.4":
version: 3.2.6
resolution: "async@npm:3.2.6"
checksum: 10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70
@ -4911,6 +4944,13 @@ __metadata:
languageName: node
linkType: hard
"b4a@npm:^1.6.4":
version: 1.6.7
resolution: "b4a@npm:1.6.7"
checksum: 10c0/ec2f004d1daae04be8c5a1f8aeb7fea213c34025e279db4958eb0b82c1729ee25f7c6e89f92a5f65c8a9cf2d017ce27e3dda912403341d1781bd74528a4849d4
languageName: node
linkType: hard
"babel-plugin-styled-components@npm:^2.1.4":
version: 2.1.4
resolution: "babel-plugin-styled-components@npm:2.1.4"
@ -4947,6 +4987,13 @@ __metadata:
languageName: node
linkType: hard
"bare-events@npm:^2.2.0":
version: 2.5.4
resolution: "bare-events@npm:2.5.4"
checksum: 10c0/877a9cea73d545e2588cdbd6fd01653e27dac48ad6b44985cdbae73e1f57f292d4ba52e25d1fba53674c1053c463d159f3d5c7bc36a2e6e192e389b499ddd627
languageName: node
linkType: hard
"base-64@npm:^1.0.0":
version: 1.0.0
resolution: "base-64@npm:1.0.0"
@ -5134,6 +5181,13 @@ __metadata:
languageName: node
linkType: hard
"buffer-crc32@npm:^1.0.0":
version: 1.0.0
resolution: "buffer-crc32@npm:1.0.0"
checksum: 10c0/8b86e161cee4bb48d5fa622cbae4c18f25e4857e5203b89e23de59e627ab26beb82d9d7999f2b8de02580165f61f83f997beaf02980cdf06affd175b651921ab
languageName: node
linkType: hard
"buffer-crc32@npm:~0.2.3":
version: 0.2.13
resolution: "buffer-crc32@npm:0.2.13"
@ -5846,6 +5900,19 @@ __metadata:
languageName: node
linkType: hard
"compress-commons@npm:^6.0.2":
version: 6.0.2
resolution: "compress-commons@npm:6.0.2"
dependencies:
crc-32: "npm:^1.2.0"
crc32-stream: "npm:^6.0.0"
is-stream: "npm:^2.0.1"
normalize-path: "npm:^3.0.0"
readable-stream: "npm:^4.0.0"
checksum: 10c0/2347031b7c92c8ed5011b07b93ec53b298fa2cd1800897532ac4d4d1aeae06567883f481b6e35f13b65fc31b190c751df6635434d525562f0203fde76f1f0814
languageName: node
linkType: hard
"compute-scroll-into-view@npm:^3.0.2":
version: 3.1.1
resolution: "compute-scroll-into-view@npm:3.1.1"
@ -5993,6 +6060,25 @@ __metadata:
languageName: node
linkType: hard
"crc-32@npm:^1.2.0":
version: 1.2.2
resolution: "crc-32@npm:1.2.2"
bin:
crc32: bin/crc32.njs
checksum: 10c0/11dcf4a2e77ee793835d49f2c028838eae58b44f50d1ff08394a610bfd817523f105d6ae4d9b5bef0aad45510f633eb23c903e9902e4409bed1ce70cb82b9bf0
languageName: node
linkType: hard
"crc32-stream@npm:^6.0.0":
version: 6.0.0
resolution: "crc32-stream@npm:6.0.0"
dependencies:
crc-32: "npm:^1.2.0"
readable-stream: "npm:^4.0.0"
checksum: 10c0/bf9c84571ede2d119c2b4f3a9ef5eeb9ff94b588493c0d3862259af86d3679dcce1c8569dd2b0a6eff2f35f5e2081cc1263b846d2538d4054da78cf34f262a3d
languageName: node
linkType: hard
"crc@npm:^3.8.0":
version: 3.8.0
resolution: "crc@npm:3.8.0"
@ -7963,6 +8049,13 @@ __metadata:
languageName: node
linkType: hard
"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2":
version: 1.3.2
resolution: "fast-fifo@npm:1.3.2"
checksum: 10c0/d53f6f786875e8b0529f784b59b4b05d4b5c31c651710496440006a398389a579c8dbcd2081311478b5bf77f4b0b21de69109c5a4eabea9d8e8783d1eb864e4c
languageName: node
linkType: hard
"fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2":
version: 3.3.3
resolution: "fast-glob@npm:3.3.3"
@ -8683,7 +8776,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1":
"glob@npm:^10.0.0, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1":
version: 10.4.5
resolution: "glob@npm:10.4.5"
dependencies:
@ -9872,7 +9965,7 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^2.0.0":
"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1":
version: 2.0.1
resolution: "is-stream@npm:2.0.1"
checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5
@ -10473,6 +10566,15 @@ __metadata:
languageName: node
linkType: hard
"lazystream@npm:^1.0.0":
version: 1.0.1
resolution: "lazystream@npm:1.0.1"
dependencies:
readable-stream: "npm:^2.0.5"
checksum: 10c0/ea4e509a5226ecfcc303ba6782cc269be8867d372b9bcbd625c88955df1987ea1a20da4643bf9270336415a398d33531ebf0d5f0d393b9283dc7c98bfcbd7b69
languageName: node
linkType: hard
"lcid@npm:^1.0.0":
version: 1.0.0
resolution: "lcid@npm:1.0.0"
@ -12029,7 +12131,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^5.0.1":
"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0":
version: 5.1.6
resolution: "minimatch@npm:5.1.6"
dependencies:
@ -12426,6 +12528,13 @@ __metadata:
languageName: node
linkType: hard
"node-stream-zip@npm:^1.15.0":
version: 1.15.0
resolution: "node-stream-zip@npm:1.15.0"
checksum: 10c0/429fce95d7e90e846adbe096c61d2ea8d18defc155c0345d25d0f98dd6fc72aeb95039318484a4e0a01dc3814b6d0d1ae0fe91847a29669dff8676ec064078c9
languageName: node
linkType: hard
"noop-logger@npm:^0.1.1":
version: 0.1.1
resolution: "noop-logger@npm:0.1.1"
@ -12456,6 +12565,13 @@ __metadata:
languageName: node
linkType: hard
"normalize-path@npm:^3.0.0":
version: 3.0.0
resolution: "normalize-path@npm:3.0.0"
checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046
languageName: node
linkType: hard
"normalize-url@npm:^6.0.1":
version: 6.1.0
resolution: "normalize-url@npm:6.1.0"
@ -14553,7 +14669,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6":
"readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
dependencies:
@ -14568,7 +14684,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^4.7.0":
"readable-stream@npm:^4.0.0, readable-stream@npm:^4.7.0":
version: 4.7.0
resolution: "readable-stream@npm:4.7.0"
dependencies:
@ -14590,6 +14706,15 @@ __metadata:
languageName: node
linkType: hard
"readdir-glob@npm:^1.1.2":
version: 1.1.3
resolution: "readdir-glob@npm:1.1.3"
dependencies:
minimatch: "npm:^5.1.0"
checksum: 10c0/a37e0716726650845d761f1041387acd93aa91b28dd5381950733f994b6c349ddc1e21e266ec7cc1f9b92e205a7a972232f9b89d5424d07361c2c3753d5dbace
languageName: node
linkType: hard
"readdirp@npm:^4.0.1":
version: 4.1.2
resolution: "readdirp@npm:4.1.2"
@ -15838,6 +15963,20 @@ __metadata:
languageName: node
linkType: hard
"streamx@npm:^2.15.0":
version: 2.22.0
resolution: "streamx@npm:2.22.0"
dependencies:
bare-events: "npm:^2.2.0"
fast-fifo: "npm:^1.3.2"
text-decoder: "npm:^1.1.0"
dependenciesMeta:
bare-events:
optional: true
checksum: 10c0/f5017998a5b6360ba652599d20ef308c8c8ab0e26c8e5f624f0706f0ea12624e94fdf1ec18318124498529a1b106a1ab1c94a1b1e1ad6c2eec7cb9c8ac1b9198
languageName: node
linkType: hard
"string-argv@npm:^0.3.2":
version: 0.3.2
resolution: "string-argv@npm:0.3.2"
@ -16188,6 +16327,17 @@ __metadata:
languageName: node
linkType: hard
"tar-stream@npm:^3.0.0":
version: 3.1.7
resolution: "tar-stream@npm:3.1.7"
dependencies:
b4a: "npm:^1.6.4"
fast-fifo: "npm:^1.2.0"
streamx: "npm:^2.15.0"
checksum: 10c0/a09199d21f8714bd729993ac49b6c8efcb808b544b89f23378ad6ffff6d1cb540878614ba9d4cfec11a64ef39e1a6f009a5398371491eb1fda606ffc7f70f718
languageName: node
linkType: hard
"tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2, tar@npm:^6.2.1":
version: 6.2.1
resolution: "tar@npm:6.2.1"
@ -16237,6 +16387,15 @@ __metadata:
languageName: node
linkType: hard
"text-decoder@npm:^1.1.0":
version: 1.2.3
resolution: "text-decoder@npm:1.2.3"
dependencies:
b4a: "npm:^1.6.4"
checksum: 10c0/569d776b9250158681c83656ef2c3e0a5d5c660c27ca69f87eedef921749a4fbf02095e5f9a0f862a25cf35258379b06e31dee9c125c9f72e273b7ca1a6d1977
languageName: node
linkType: hard
"text-encoding@npm:0.7.0":
version: 0.7.0
resolution: "text-encoding@npm:0.7.0"
@ -17836,6 +17995,17 @@ __metadata:
languageName: node
linkType: hard
"zip-stream@npm:^6.0.1":
version: 6.0.1
resolution: "zip-stream@npm:6.0.1"
dependencies:
archiver-utils: "npm:^5.0.0"
compress-commons: "npm:^6.0.2"
readable-stream: "npm:^4.0.0"
checksum: 10c0/50f2fb30327fb9d09879abf7ae2493705313adf403e794b030151aaae00009162419d60d0519e807673ec04d442e140c8879ca14314df0a0192de3b233e8f28b
languageName: node
linkType: hard
"zipread@npm:^1.3.3":
version: 1.3.3
resolution: "zipread@npm:1.3.3"