diff --git a/package.json b/package.json index 1244d01f..ea05e7a4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index f29c6920..5bf8578e 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -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((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((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 }) }) diff --git a/src/renderer/src/components/Popups/BackupPopup.tsx b/src/renderer/src/components/Popups/BackupPopup.tsx index d3f99f6b..a6e8d03e 100644 --- a/src/renderer/src/components/Popups/BackupPopup.tsx +++ b/src/renderer/src/components/Popups/BackupPopup.tsx @@ -57,6 +57,8 @@ const PopupContainer: React.FC = ({ resolve }) => { BackupPopup.hide = onCancel + const isDisabled = progressData ? progressData.stage !== 'completed' : false + return ( = ({ 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 &&
{t('backup.content')}
} {progressData && ( diff --git a/src/renderer/src/components/Popups/RestorePopup.tsx b/src/renderer/src/components/Popups/RestorePopup.tsx index 36c11944..7a54c149 100644 --- a/src/renderer/src/components/Popups/RestorePopup.tsx +++ b/src/renderer/src/components/Popups/RestorePopup.tsx @@ -57,6 +57,8 @@ const PopupContainer: React.FC = ({ resolve }) => { RestorePopup.hide = onCancel + const isDisabled = progressData ? progressData.stage !== 'completed' : false + return ( = ({ 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 &&
{t('restore.content')}
} {progressData && ( diff --git a/yarn.lock b/yarn.lock index 04dc5767..95aeee47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"