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:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -203,6 +203,7 @@ export type WebDavConfig = {
|
||||
webdavUser: string
|
||||
webdavPass: string
|
||||
webdavPath: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
export type AppInfo = {
|
||||
|
||||
Reference in New Issue
Block a user