feat: Add comprehensive backup progress tracking and UI
- Implemented detailed backup progress tracking in BackupManager - Added new BackupPopup component for backup process visualization - Enhanced backup process with file copy progress and stage tracking - Updated localization files with backup progress translations - Integrated backup progress reporting to renderer process
This commit is contained in:
parent
581e2fb786
commit
e69d0c89a6
@ -7,6 +7,7 @@ import * as fs from 'fs-extra'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
import WebDav from './WebDav'
|
import WebDav from './WebDav'
|
||||||
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
||||||
@ -72,18 +73,39 @@ class BackupManager {
|
|||||||
data: string,
|
data: string,
|
||||||
destinationPath: string = this.backupDir
|
destinationPath: string = this.backupDir
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
|
||||||
|
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
|
||||||
|
mainWindow?.webContents.send('backup-progress', processData)
|
||||||
|
Logger.log('[BackupManager] backup progress', processData)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.ensureDir(this.tempDir)
|
await fs.ensureDir(this.tempDir)
|
||||||
|
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||||
|
|
||||||
// 将 data 写入临时文件
|
// 将 data 写入临时文件
|
||||||
const tempDataPath = path.join(this.tempDir, 'data.json')
|
const tempDataPath = path.join(this.tempDir, 'data.json')
|
||||||
await fs.writeFile(tempDataPath, data)
|
await fs.writeFile(tempDataPath, data)
|
||||||
|
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
|
||||||
|
|
||||||
// 复制 Data 目录到临时目录
|
// 复制 Data 目录到临时目录
|
||||||
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
||||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||||
await fs.copy(sourcePath, tempDataDir)
|
|
||||||
|
// 获取源目录总大小
|
||||||
|
const totalSize = await this.getDirSize(sourcePath)
|
||||||
|
let copiedSize = 0
|
||||||
|
|
||||||
|
// 使用流式复制
|
||||||
|
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
|
||||||
|
copiedSize += size
|
||||||
|
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
|
||||||
|
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
await this.setWritableRecursive(tempDataDir)
|
await this.setWritableRecursive(tempDataDir)
|
||||||
|
onProgress({ stage: 'compressing', progress: 80, total: 100 })
|
||||||
|
|
||||||
// 使用 adm-zip 创建压缩文件
|
// 使用 adm-zip 创建压缩文件
|
||||||
const zip = new AdmZip()
|
const zip = new AdmZip()
|
||||||
@ -93,6 +115,7 @@ class BackupManager {
|
|||||||
|
|
||||||
// 清理临时目录
|
// 清理临时目录
|
||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
Logger.log('Backup completed successfully')
|
Logger.log('Backup completed successfully')
|
||||||
return backupedFilePath
|
return backupedFilePath
|
||||||
@ -102,6 +125,44 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getDirSize(dirPath: string): Promise<number> {
|
||||||
|
let size = 0
|
||||||
|
const items = await fs.readdir(dirPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dirPath, item.name)
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
size += await this.getDirSize(fullPath)
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(fullPath)
|
||||||
|
size += stats.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyDirWithProgress(
|
||||||
|
source: string,
|
||||||
|
destination: string,
|
||||||
|
onProgress: (size: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const items = await fs.readdir(source, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const sourcePath = path.join(source, item.name)
|
||||||
|
const destPath = path.join(destination, item.name)
|
||||||
|
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
await fs.ensureDir(destPath)
|
||||||
|
await this.copyDirWithProgress(sourcePath, destPath, onProgress)
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(sourcePath)
|
||||||
|
await fs.copy(sourcePath, destPath)
|
||||||
|
onProgress(stats.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// 创建临时目录
|
// 创建临时目录
|
||||||
|
|||||||
100
src/renderer/src/components/Popups/BackupPopup.tsx
Normal file
100
src/renderer/src/components/Popups/BackupPopup.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { backup } from '@renderer/services/BackupService'
|
||||||
|
import { Modal, Progress } from 'antd'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressData {
|
||||||
|
stage: string
|
||||||
|
progress: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [progressData, setProgressData] = useState<ProgressData>()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeListener = window.electron.ipcRenderer.on('backup-progress', (_, data: ProgressData) => {
|
||||||
|
setProgressData(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
await backup()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProgressText = () => {
|
||||||
|
if (!progressData) return ''
|
||||||
|
|
||||||
|
if (progressData.stage === 'copying_files') {
|
||||||
|
return t(`backup.progress.${progressData.stage}`, {
|
||||||
|
progress: Math.floor(progressData.progress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return t(`backup.progress.${progressData.stage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupPopup.hide = onCancel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('backup.title')}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="ant-move-down"
|
||||||
|
okText={t('backup.confirm.button')}
|
||||||
|
centered>
|
||||||
|
{!progressData && <div>{t('backup.content')}</div>}
|
||||||
|
{progressData && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||||
|
<Progress percent={Math.floor(progressData.progress)} strokeColor="var(--color-primary)" />
|
||||||
|
<div style={{ marginTop: 16 }}>{getProgressText()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopViewKey = 'BackupPopup'
|
||||||
|
|
||||||
|
export default class BackupPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
static show() {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
TopViewKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -883,6 +883,20 @@
|
|||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
"show_window": "Show Window",
|
"show_window": "Show Window",
|
||||||
"visualization": "Visualization"
|
"visualization": "Visualization"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"title": "Data Backup",
|
||||||
|
"confirm": "Are you sure you want to backup data?",
|
||||||
|
"confirm.button": "Select Backup Location",
|
||||||
|
"content": "Backup operation will cover all application data, including chat history, settings, and knowledge base. Please note that the backup process may take some time, thank you for your patience.",
|
||||||
|
"progress": {
|
||||||
|
"title": "Backup Progress",
|
||||||
|
"preparing": "Preparing backup...",
|
||||||
|
"writing_data": "Writing data...",
|
||||||
|
"copying_files": "Copying files... {{progress}}%",
|
||||||
|
"compressing": "Compressing files...",
|
||||||
|
"completed": "Backup completed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -883,6 +883,20 @@
|
|||||||
"quit": "終了",
|
"quit": "終了",
|
||||||
"show_window": "ウィンドウを表示",
|
"show_window": "ウィンドウを表示",
|
||||||
"visualization": "可視化"
|
"visualization": "可視化"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"title": "データバックアップ",
|
||||||
|
"confirm": "データをバックアップしますか?",
|
||||||
|
"confirm.button": "バックアップ位置を選択",
|
||||||
|
"content": "バックアップ操作はすべてのアプリデータを含むため、時間がかかる場合があります。",
|
||||||
|
"progress": {
|
||||||
|
"title": "バックアップ進捗",
|
||||||
|
"preparing": "バックアップ準備中...",
|
||||||
|
"writing_data": "データ書き込み中...",
|
||||||
|
"copying_files": "ファイルコピー中... {{progress}}%",
|
||||||
|
"compressing": "圧縮中...",
|
||||||
|
"completed": "バックアップ完了"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -883,6 +883,20 @@
|
|||||||
"quit": "Выйти",
|
"quit": "Выйти",
|
||||||
"show_window": "Показать окно",
|
"show_window": "Показать окно",
|
||||||
"visualization": "Визуализация"
|
"visualization": "Визуализация"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"title": "Резервное копирование данных",
|
||||||
|
"confirm": "Вы уверены, что хотите создать резервную копию?",
|
||||||
|
"confirm.button": "Выбрать папку для резервной копии",
|
||||||
|
"content": "Резервная копия будет содержать все данные приложения, включая чаты, настройки и базу знаний. Это может занять некоторое время.",
|
||||||
|
"progress": {
|
||||||
|
"title": "Прогресс резервного копирования",
|
||||||
|
"preparing": "Подготовка резервной копии...",
|
||||||
|
"writing_data": "Запись данных...",
|
||||||
|
"copying_files": "Копирование файлов... {{progress}}%",
|
||||||
|
"compressing": "Сжатие файлов...",
|
||||||
|
"completed": "Резервная копия создана"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -883,6 +883,20 @@
|
|||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
"show_window": "显示窗口",
|
"show_window": "显示窗口",
|
||||||
"visualization": "可视化"
|
"visualization": "可视化"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"title": "数据备份",
|
||||||
|
"confirm": "确定要备份数据吗?",
|
||||||
|
"confirm.button": "选择备份位置",
|
||||||
|
"content": "备份操作将涵盖所有应用数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待。",
|
||||||
|
"progress": {
|
||||||
|
"title": "备份进度",
|
||||||
|
"preparing": "准备备份...",
|
||||||
|
"writing_data": "写入数据...",
|
||||||
|
"copying_files": "复制文件... {{progress}}%",
|
||||||
|
"compressing": "压缩文件...",
|
||||||
|
"completed": "备份完成"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -883,6 +883,20 @@
|
|||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
"show_window": "顯示視窗",
|
"show_window": "顯示視窗",
|
||||||
"visualization": "可視化"
|
"visualization": "可視化"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"title": "資料備份",
|
||||||
|
"confirm": "確定要備份資料嗎?",
|
||||||
|
"confirm.button": "選擇備份位置",
|
||||||
|
"content": "備份操作將涵蓋所有應用資料,包括聊天記錄、設定、知識庫等全部資料。請注意,備份過程可能需要一些時間,感謝您的耐心等待。",
|
||||||
|
"progress": {
|
||||||
|
"title": "備份進度",
|
||||||
|
"preparing": "準備備份...",
|
||||||
|
"writing_data": "寫入資料...",
|
||||||
|
"copying_files": "複製文件... {{progress}}%",
|
||||||
|
"compressing": "壓縮文件...",
|
||||||
|
"completed": "備份完成"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { FileSearchOutlined, FolderOpenOutlined, InfoCircleOutlined, SaveOutline
|
|||||||
import { Client } from '@notionhq/client'
|
import { Client } from '@notionhq/client'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
import MinApp from '@renderer/components/MinApp'
|
||||||
|
import BackupPopup from '@renderer/components/Popups/BackupPopup'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { backup, reset, restore } from '@renderer/services/BackupService'
|
import { reset, restore } from '@renderer/services/BackupService'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setNotionApiKey,
|
setNotionApiKey,
|
||||||
@ -332,6 +333,8 @@ const DataSettings: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBackup = () => BackupPopup.show()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer theme={theme}>
|
<SettingContainer theme={theme}>
|
||||||
<SettingGroup theme={theme}>
|
<SettingGroup theme={theme}>
|
||||||
@ -340,7 +343,7 @@ const DataSettings: FC = () => {
|
|||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
<HStack gap="5px" justifyContent="space-between">
|
<HStack gap="5px" justifyContent="space-between">
|
||||||
<Button onClick={backup} icon={<SaveOutlined />}>
|
<Button onClick={handleBackup} icon={<SaveOutlined />}>
|
||||||
{t('settings.general.backup.button')}
|
{t('settings.general.backup.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user