feat: enhance WebDAV backup and restore functionality (#2522)

Co-authored-by: zhsama <zhcf1ess@qq.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
zhsama
2025-03-21 11:13:44 +08:00
committed by GitHub
parent 2139d9205d
commit 7ea3bfe1fb
10 changed files with 229 additions and 49 deletions
+6
View File
@@ -762,6 +762,12 @@
"password": "WebDAV Password",
"path": "WebDAV Path",
"path.placeholder": "/backup",
"backup.modal.title": "Backup to WebDAV",
"backup.modal.filename.placeholder": "Please enter backup filename",
"restore.modal.title": "Restore from WebDAV",
"restore.modal.select.placeholder": "Please select a backup file to restore",
"restore.confirm.title": "Confirm Restore",
"restore.confirm.content": "Restoring from WebDAV will overwrite current data. Do you want to continue?",
"restore.button": "Restore from WebDAV",
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
"restore.title": "Restore from WebDAV",
+6
View File
@@ -762,6 +762,12 @@
"password": "WebDAV 密码",
"path": "WebDAV 路径",
"path.placeholder": "/backup",
"backup.modal.title": "备份到 WebDAV",
"backup.modal.filename.placeholder": "请输入备份文件名",
"restore.modal.title": "从 WebDAV 恢复",
"restore.modal.select.placeholder": "请选择要恢复的备份文件",
"restore.confirm.title": "确认恢复",
"restore.confirm.content": "从 WebDAV 恢复将会覆盖当前数据,是否继续?",
"restore.button": "从 WebDAV 恢复",
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
"restore.title": "从 WebDAV 恢复",
@@ -13,13 +13,19 @@ import {
setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser
} from '@renderer/store/settings'
import { Button, Input, Select } from 'antd'
import { Button, Input, Modal, Select, Spin } from 'antd'
import dayjs from 'dayjs'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
interface BackupFile {
fileName: string
modifiedTime: string
size: number
}
const WebDavSettings: FC = () => {
const {
webdavHost: webDAVHost,
@@ -38,6 +44,12 @@ const WebDavSettings: FC = () => {
const [backuping, setBackuping] = useState(false)
const [restoring, setRestoring] = useState(false)
const [isModalVisible, setIsModalVisible] = useState(false)
const [customFileName, setCustomFileName] = useState('')
const [isRestoreModalVisible, setIsRestoreModalVisible] = useState(false)
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([])
const [selectedFile, setSelectedFile] = useState<string>('')
const [loadingFiles, setLoadingFiles] = useState(false)
const dispatch = useAppDispatch()
const { theme } = useTheme()
@@ -48,35 +60,6 @@ const WebDavSettings: FC = () => {
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
const onBackup = async () => {
if (!webdavHost) {
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
return
}
setBackuping(true)
await backupToWebdav({ showMessage: true })
setBackuping(false)
}
const onRestore = async () => {
if (!webdavHost) {
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
return
}
setRestoring(true)
await restoreFromWebdav()
setRestoring(false)
}
const onPressRestore = () => {
window.modal.confirm({
title: t('settings.data.webdav.restore.title'),
content: t('settings.data.webdav.restore.content'),
centered: true,
onOk: onRestore
})
}
const onSyncIntervalChange = (value: number) => {
setSyncInterval(value)
dispatch(_setWebdavSyncInterval(value))
@@ -113,6 +96,88 @@ const WebDavSettings: FC = () => {
)
}
const showBackupModal = async () => {
// 获取默认文件名
const deviceType = await window.api.system.getDeviceType()
const timestamp = dayjs().format('YYYYMMDDHHmmss')
const defaultFileName = `cherry-studio.${timestamp}.${deviceType}.zip`
setCustomFileName(defaultFileName)
setIsModalVisible(true)
}
const handleBackup = async () => {
setBackuping(true)
try {
await backupToWebdav({ showMessage: true, customFileName })
} finally {
setBackuping(false)
setIsModalVisible(false)
}
}
const handleCancel = () => {
setIsModalVisible(false)
}
const showRestoreModal = async () => {
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
return
}
setIsRestoreModalVisible(true)
setLoadingFiles(true)
try {
const files = await window.api.backup.listWebdavFiles({
webdavHost,
webdavUser,
webdavPass,
webdavPath
})
setBackupFiles(files)
} catch (error: any) {
window.message.error({ content: error.message, key: 'list-files-error' })
} finally {
setLoadingFiles(false)
}
}
const handleRestore = async () => {
if (!selectedFile || !webdavHost || !webdavUser || !webdavPass || !webdavPath) {
window.message.error({
content: !selectedFile ? t('message.error.no.file.selected') : t('message.error.invalid.webdav'),
key: 'restore-error'
})
return
}
window.modal.confirm({
title: t('settings.data.webdav.restore.confirm.title'),
content: t('settings.data.webdav.restore.confirm.content'),
centered: true,
onOk: async () => {
setRestoring(true)
try {
await restoreFromWebdav(selectedFile)
setIsRestoreModalVisible(false)
} catch (error: any) {
window.message.error({ content: error.message, key: 'restore-error' })
} finally {
setRestoring(false)
}
}
})
}
const formatFileOption = (file: BackupFile) => {
const date = dayjs(file.modifiedTime).format('YYYY-MM-DD HH:mm:ss')
const size = `${(file.size / 1024).toFixed(2)} KB`
return {
label: `${file.fileName} (${date}, ${size})`,
value: file.fileName
}
}
return (
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
@@ -165,10 +230,10 @@ const WebDavSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px" justifyContent="space-between">
<Button onClick={onBackup} icon={<SaveOutlined />} loading={backuping}>
<Button onClick={showBackupModal} icon={<SaveOutlined />} loading={backuping}>
{t('settings.data.webdav.backup.button')}
</Button>
<Button onClick={onPressRestore} icon={<FolderOpenOutlined />} loading={restoring}>
<Button onClick={showRestoreModal} icon={<FolderOpenOutlined />} loading={restoring}>
{t('settings.data.webdav.restore.button')}
</Button>
</HStack>
@@ -198,6 +263,46 @@ const WebDavSettings: FC = () => {
</SettingRow>
</>
)}
<>
<Modal
title={t('settings.data.webdav.backup.modal.title')}
open={isModalVisible}
onOk={handleBackup}
onCancel={handleCancel}
okButtonProps={{ loading: backuping }}>
<Input
value={customFileName}
onChange={(e) => setCustomFileName(e.target.value)}
placeholder={t('settings.data.webdav.backup.modal.filename.placeholder')}
/>
</Modal>
<Modal
title={t('settings.data.webdav.restore.modal.title')}
open={isRestoreModalVisible}
onOk={handleRestore}
onCancel={() => setIsRestoreModalVisible(false)}
okButtonProps={{ loading: restoring }}
width={600}>
<div style={{ position: 'relative' }}>
<Select
style={{ width: '100%' }}
placeholder={t('settings.data.webdav.restore.modal.select.placeholder')}
value={selectedFile}
onChange={setSelectedFile}
options={backupFiles.map(formatFileOption)}
loading={loadingFiles}
showSearch
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
{loadingFiles && (
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
<Spin />
</div>
)}
</div>
</Modal>
</>
</SettingGroup>
)
}
+19 -6
View File
@@ -3,6 +3,7 @@ import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { setWebDAVSyncState } from '@renderer/store/runtime'
import dayjs from 'dayjs'
import Logger from 'electron-log'
export async function backup() {
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
@@ -59,16 +60,27 @@ export async function reset() {
}
// 备份到 webdav
export async function backupToWebdav({ showMessage = false }: { showMessage?: boolean } = {}) {
export async function backupToWebdav({
showMessage = false,
customFileName = ''
}: { showMessage?: boolean; customFileName?: string } = {}) {
if (isManualBackupRunning) {
console.log('[Backup] Manual backup already in progress')
Logger.log('[Backup] Manual backup already in progress')
return
}
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
let deviceType = 'unknown'
try {
deviceType = (await window.api.system.getDeviceType()) || 'unknown'
} catch (error) {
Logger.error('[Backup] Failed to get device type:', error)
}
const timestamp = dayjs().format('YYYYMMDDHHmmss')
const backupFileName = customFileName || `cherry-studio.${timestamp}.${deviceType}.zip`
const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
const backupData = await getBackupData()
// 上传文件
@@ -77,7 +89,8 @@ export async function backupToWebdav({ showMessage = false }: { showMessage?: bo
webdavHost,
webdavUser,
webdavPass,
webdavPath
webdavPath,
fileName: finalFileName
})
if (success) {
store.dispatch(
@@ -106,12 +119,12 @@ export async function backupToWebdav({ showMessage = false }: { showMessage?: bo
}
// 从 webdav 恢复
export async function restoreFromWebdav() {
export async function restoreFromWebdav(fileName?: string) {
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
let data = ''
try {
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath })
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath, fileName })
} catch (error: any) {
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
window.modal.error({
+1
View File
@@ -203,6 +203,7 @@ export type WebDavConfig = {
webdavUser: string
webdavPass: string
webdavPath: string
fileName?: string
}
export type AppInfo = {