cherry-studio/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx
kangfenmao 7e3f51dffc feat: update WebDAV integration and dependencies
- Added 'webdav' to the list of plugins in electron.vite.config.ts.
- Upgraded 'webdav' package from version 4.11.4 to 5.8.0 in package.json and yarn.lock.
- Introduced a utility function for formatting file sizes in WebDavSettings component.
- Updated file size display logic to use the new formatting utility.
2025-03-21 13:53:01 +08:00

311 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
setWebdavAutoSync,
setWebdavHost as _setWebdavHost,
setWebdavPass as _setWebdavPass,
setWebdavPath as _setWebdavPath,
setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser
} from '@renderer/store/settings'
import { Button, Input, Modal, Select, Spin, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import { formatFileSize } from '@renderer/utils'
interface BackupFile {
fileName: string
modifiedTime: string
size: number
}
const WebDavSettings: FC = () => {
const {
webdavHost: webDAVHost,
webdavUser: webDAVUser,
webdavPass: webDAVPass,
webdavPath: webDAVPath,
webdavSyncInterval: webDAVSyncInterval
} = useSettings()
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser)
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
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()
const { t } = useTranslation()
const { webdavSync } = useAppSelector((state) => state.backup)
// 把之前备份的文件定时上传到 webdav首先先配置 webdav 的 host, port, user, pass, path
const onSyncIntervalChange = (value: number) => {
setSyncInterval(value)
dispatch(_setWebdavSyncInterval(value))
if (value === 0) {
dispatch(setWebdavAutoSync(false))
stopAutoSync()
} else {
dispatch(setWebdavAutoSync(true))
startAutoSync()
}
}
const renderSyncStatus = () => {
if (!webdavHost) return null
if (!webdavSync.lastSyncTime && !webdavSync.syncing && !webdavSync.lastSyncError) {
return <span style={{ color: 'var(--text-secondary)' }}>{t('settings.data.webdav.noSync')}</span>
}
return (
<HStack gap="5px" alignItems="center">
{webdavSync.syncing && <SyncOutlined spin />}
{!webdavSync.syncing && webdavSync.lastSyncError && (
<Tooltip title={`${t('settings.data.webdav.syncError')}: ${webdavSync.lastSyncError}`}>
<WarningOutlined style={{ color: 'red' }} />
</Tooltip>
)}
{webdavSync.lastSyncTime && (
<span style={{ color: 'var(--text-secondary)' }}>
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
</span>
)}
</HStack>
)
}
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 = formatFileSize(file.size)
return {
label: `${file.fileName} (${date}, ${size})`,
value: file.fileName
}
}
return (
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.host')}</SettingRowTitle>
<Input
placeholder={t('settings.data.webdav.host.placeholder')}
value={webdavHost}
onChange={(e) => setWebdavHost(e.target.value)}
style={{ width: 250 }}
type="url"
onBlur={() => dispatch(_setWebdavHost(webdavHost || ''))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.user')}</SettingRowTitle>
<Input
placeholder={t('settings.data.webdav.user')}
value={webdavUser}
onChange={(e) => setWebdavUser(e.target.value)}
style={{ width: 250 }}
onBlur={() => dispatch(_setWebdavUser(webdavUser || ''))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.password')}</SettingRowTitle>
<Input.Password
placeholder={t('settings.data.webdav.password')}
value={webdavPass}
onChange={(e) => setWebdavPass(e.target.value)}
style={{ width: 250 }}
onBlur={() => dispatch(_setWebdavPass(webdavPass || ''))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.path')}</SettingRowTitle>
<Input
placeholder={t('settings.data.webdav.path.placeholder')}
value={webdavPath}
onChange={(e) => setWebdavPath(e.target.value)}
style={{ width: 250 }}
onBlur={() => dispatch(_setWebdavPath(webdavPath || ''))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px" justifyContent="space-between">
<Button onClick={showBackupModal} icon={<SaveOutlined />} loading={backuping}>
{t('settings.data.webdav.backup.button')}
</Button>
<Button onClick={showRestoreModal} icon={<FolderOpenOutlined />} loading={restoring}>
{t('settings.data.webdav.restore.button')}
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
<Select.Option value={1}>{t('settings.data.webdav.minute_interval', { count: 1 })}</Select.Option>
<Select.Option value={5}>{t('settings.data.webdav.minute_interval', { count: 5 })}</Select.Option>
<Select.Option value={15}>{t('settings.data.webdav.minute_interval', { count: 15 })}</Select.Option>
<Select.Option value={30}>{t('settings.data.webdav.minute_interval', { count: 30 })}</Select.Option>
<Select.Option value={60}>{t('settings.data.webdav.hour_interval', { count: 1 })}</Select.Option>
<Select.Option value={120}>{t('settings.data.webdav.hour_interval', { count: 2 })}</Select.Option>
<Select.Option value={360}>{t('settings.data.webdav.hour_interval', { count: 6 })}</Select.Option>
<Select.Option value={720}>{t('settings.data.webdav.hour_interval', { count: 12 })}</Select.Option>
<Select.Option value={1440}>{t('settings.data.webdav.hour_interval', { count: 24 })}</Select.Option>
</Select>
</SettingRow>
{webdavSync && syncInterval > 0 && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
{renderSyncStatus()}
</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>
)
}
export default WebDavSettings