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:
parent
ea990e78a5
commit
994ffa224e
@ -1,6 +1,6 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
|
import { app, ipcMain } from 'electron'
|
||||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||||
import { app } from 'electron'
|
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
@ -44,6 +44,9 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
.then((name) => console.log(`Added Extension: ${name}`))
|
.then((name) => console.log(`Added Extension: ${name}`))
|
||||||
.catch((err) => console.log('An error occurred: ', err))
|
.catch((err) => console.log('An error occurred: ', err))
|
||||||
}
|
}
|
||||||
|
ipcMain.handle('system:getDeviceType', () => {
|
||||||
|
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for second instance
|
// Listen for second instance
|
||||||
|
|||||||
@ -130,6 +130,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle('backup:restore', backupManager.restore)
|
ipcMain.handle('backup:restore', backupManager.restore)
|
||||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||||
|
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles)
|
||||||
|
|
||||||
// file
|
// file
|
||||||
ipcMain.handle('file:open', fileManager.open)
|
ipcMain.handle('file:open', fileManager.open)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { app } from 'electron'
|
|||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import * as fs from 'fs-extra'
|
import * as fs from 'fs-extra'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import { createClient, FileStat } from 'webdav'
|
||||||
|
|
||||||
import WebDav from './WebDav'
|
import WebDav from './WebDav'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
@ -18,6 +19,7 @@ class BackupManager {
|
|||||||
this.restore = this.restore.bind(this)
|
this.restore = this.restore.bind(this)
|
||||||
this.backupToWebdav = this.backupToWebdav.bind(this)
|
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||||
|
this.listWebdavFiles = this.listWebdavFiles.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setWritableRecursive(dirPath: string): Promise<void> {
|
private async setWritableRecursive(dirPath: string): Promise<void> {
|
||||||
@ -186,7 +188,7 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||||
const filename = 'cherry-studio.backup.zip'
|
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||||
const backupedFilePath = await this.backup(_, filename, data)
|
const backupedFilePath = await this.backup(_, filename, data)
|
||||||
const webdavClient = new WebDav(webdavConfig)
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||||
@ -195,18 +197,48 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||||
const filename = 'cherry-studio.backup.zip'
|
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||||
const webdavClient = new WebDav(webdavConfig)
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
const retrievedFile = await webdavClient.getFileContents(filename)
|
try {
|
||||||
const backupedFilePath = path.join(this.backupDir, filename)
|
const retrievedFile = await webdavClient.getFileContents(filename)
|
||||||
|
const backupedFilePath = path.join(this.backupDir, filename)
|
||||||
|
|
||||||
if (!fs.existsSync(this.backupDir)) {
|
if (!fs.existsSync(this.backupDir)) {
|
||||||
fs.mkdirSync(this.backupDir, { recursive: true })
|
fs.mkdirSync(this.backupDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync为同步写,无须await
|
||||||
|
fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||||
|
|
||||||
|
return await this.restore(_, backupedFilePath)
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('[backup] Failed to restore from WebDAV:', error)
|
||||||
|
throw new Error(error.message || 'Failed to restore backup file')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
|
||||||
|
try {
|
||||||
|
const client = createClient(config.webdavHost, {
|
||||||
|
username: config.webdavUser,
|
||||||
|
password: config.webdavPass
|
||||||
|
})
|
||||||
|
|
||||||
return await this.restore(_, backupedFilePath)
|
const response = await client.getDirectoryContents(config.webdavPath)
|
||||||
|
const files = Array.isArray(response) ? response : response.data
|
||||||
|
|
||||||
|
return files
|
||||||
|
.filter((file: FileStat) => file.type === 'file' && file.basename.endsWith('.zip'))
|
||||||
|
.map((file: FileStat) => ({
|
||||||
|
fileName: file.basename,
|
||||||
|
modifiedTime: file.lastmod,
|
||||||
|
size: file.size
|
||||||
|
}))
|
||||||
|
.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Failed to list WebDAV files:', error)
|
||||||
|
throw new Error(error.message || 'Failed to list backup files')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDirSize(dirPath: string): Promise<number> {
|
private async getDirSize(dirPath: string): Promise<number> {
|
||||||
|
|||||||
11
src/preload/index.d.ts
vendored
11
src/preload/index.d.ts
vendored
@ -6,7 +6,12 @@ import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious,
|
|||||||
import type { LoaderReturn } from '@shared/config/types'
|
import type { LoaderReturn } from '@shared/config/types'
|
||||||
import type { OpenDialogOptions } from 'electron'
|
import type { OpenDialogOptions } from 'electron'
|
||||||
import type { UpdateInfo } from 'electron-updater'
|
import type { UpdateInfo } from 'electron-updater'
|
||||||
import { Readable } from 'stream'
|
|
||||||
|
interface BackupFile {
|
||||||
|
fileName: string
|
||||||
|
modifiedTime: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -24,6 +29,9 @@ declare global {
|
|||||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||||
reload: () => void
|
reload: () => void
|
||||||
clearCache: () => Promise<{ success: boolean; error?: string }>
|
clearCache: () => Promise<{ success: boolean; error?: string }>
|
||||||
|
system: {
|
||||||
|
getDeviceType: () => Promise<'mac' | 'windows' | 'linux'>
|
||||||
|
}
|
||||||
zip: {
|
zip: {
|
||||||
compress: (text: string) => Promise<Buffer>
|
compress: (text: string) => Promise<Buffer>
|
||||||
decompress: (text: Buffer) => Promise<string>
|
decompress: (text: Buffer) => Promise<string>
|
||||||
@ -33,6 +41,7 @@ declare global {
|
|||||||
restore: (backupPath: string) => Promise<string>
|
restore: (backupPath: string) => Promise<string>
|
||||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||||
|
listWebdavFiles: (webdavConfig: WebDavConfig) => Promise<BackupFile[]>
|
||||||
}
|
}
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
|
|||||||
@ -17,6 +17,9 @@ const api = {
|
|||||||
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
||||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||||
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
|
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
|
||||||
|
system: {
|
||||||
|
getDeviceType: () => ipcRenderer.invoke('system:getDeviceType')
|
||||||
|
},
|
||||||
zip: {
|
zip: {
|
||||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||||
@ -27,7 +30,8 @@ const api = {
|
|||||||
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
||||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
||||||
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
||||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
||||||
|
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig)
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||||
|
|||||||
@ -762,6 +762,12 @@
|
|||||||
"password": "WebDAV Password",
|
"password": "WebDAV Password",
|
||||||
"path": "WebDAV Path",
|
"path": "WebDAV Path",
|
||||||
"path.placeholder": "/backup",
|
"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.button": "Restore from WebDAV",
|
||||||
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
||||||
"restore.title": "Restore from WebDAV",
|
"restore.title": "Restore from WebDAV",
|
||||||
|
|||||||
@ -762,6 +762,12 @@
|
|||||||
"password": "WebDAV 密码",
|
"password": "WebDAV 密码",
|
||||||
"path": "WebDAV 路径",
|
"path": "WebDAV 路径",
|
||||||
"path.placeholder": "/backup",
|
"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.button": "从 WebDAV 恢复",
|
||||||
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
||||||
"restore.title": "从 WebDAV 恢复",
|
"restore.title": "从 WebDAV 恢复",
|
||||||
|
|||||||
@ -13,13 +13,19 @@ import {
|
|||||||
setWebdavSyncInterval as _setWebdavSyncInterval,
|
setWebdavSyncInterval as _setWebdavSyncInterval,
|
||||||
setWebdavUser as _setWebdavUser
|
setWebdavUser as _setWebdavUser
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { Button, Input, Select } from 'antd'
|
import { Button, Input, Modal, Select, Spin } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
|
||||||
|
interface BackupFile {
|
||||||
|
fileName: string
|
||||||
|
modifiedTime: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
const WebDavSettings: FC = () => {
|
const WebDavSettings: FC = () => {
|
||||||
const {
|
const {
|
||||||
webdavHost: webDAVHost,
|
webdavHost: webDAVHost,
|
||||||
@ -38,6 +44,12 @@ const WebDavSettings: FC = () => {
|
|||||||
|
|
||||||
const [backuping, setBackuping] = useState(false)
|
const [backuping, setBackuping] = useState(false)
|
||||||
const [restoring, setRestoring] = 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 dispatch = useAppDispatch()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
@ -48,35 +60,6 @@ const WebDavSettings: FC = () => {
|
|||||||
|
|
||||||
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
// 把之前备份的文件定时上传到 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) => {
|
const onSyncIntervalChange = (value: number) => {
|
||||||
setSyncInterval(value)
|
setSyncInterval(value)
|
||||||
dispatch(_setWebdavSyncInterval(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 (
|
return (
|
||||||
<SettingGroup theme={theme}>
|
<SettingGroup theme={theme}>
|
||||||
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
|
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
|
||||||
@ -165,10 +230,10 @@ const WebDavSettings: 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={onBackup} icon={<SaveOutlined />} loading={backuping}>
|
<Button onClick={showBackupModal} icon={<SaveOutlined />} loading={backuping}>
|
||||||
{t('settings.data.webdav.backup.button')}
|
{t('settings.data.webdav.backup.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onPressRestore} icon={<FolderOpenOutlined />} loading={restoring}>
|
<Button onClick={showRestoreModal} icon={<FolderOpenOutlined />} loading={restoring}>
|
||||||
{t('settings.data.webdav.restore.button')}
|
{t('settings.data.webdav.restore.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
@ -198,6 +263,46 @@ const WebDavSettings: FC = () => {
|
|||||||
</SettingRow>
|
</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>
|
</SettingGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import i18n from '@renderer/i18n'
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setWebDAVSyncState } from '@renderer/store/runtime'
|
import { setWebDAVSyncState } from '@renderer/store/runtime'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
export async function backup() {
|
export async function backup() {
|
||||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
||||||
@ -59,16 +60,27 @@ export async function reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 备份到 webdav
|
// 备份到 webdav
|
||||||
export async function backupToWebdav({ showMessage = false }: { showMessage?: boolean } = {}) {
|
export async function backupToWebdav({
|
||||||
|
showMessage = false,
|
||||||
|
customFileName = ''
|
||||||
|
}: { showMessage?: boolean; customFileName?: string } = {}) {
|
||||||
if (isManualBackupRunning) {
|
if (isManualBackupRunning) {
|
||||||
console.log('[Backup] Manual backup already in progress')
|
Logger.log('[Backup] Manual backup already in progress')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
|
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
|
||||||
|
|
||||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
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()
|
const backupData = await getBackupData()
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
@ -77,7 +89,8 @@ export async function backupToWebdav({ showMessage = false }: { showMessage?: bo
|
|||||||
webdavHost,
|
webdavHost,
|
||||||
webdavUser,
|
webdavUser,
|
||||||
webdavPass,
|
webdavPass,
|
||||||
webdavPath
|
webdavPath,
|
||||||
|
fileName: finalFileName
|
||||||
})
|
})
|
||||||
if (success) {
|
if (success) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
@ -106,12 +119,12 @@ export async function backupToWebdav({ showMessage = false }: { showMessage?: bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从 webdav 恢复
|
// 从 webdav 恢复
|
||||||
export async function restoreFromWebdav() {
|
export async function restoreFromWebdav(fileName?: string) {
|
||||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||||
let data = ''
|
let data = ''
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error: any) {
|
||||||
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
||||||
window.modal.error({
|
window.modal.error({
|
||||||
|
|||||||
@ -203,6 +203,7 @@ export type WebDavConfig = {
|
|||||||
webdavUser: string
|
webdavUser: string
|
||||||
webdavPass: string
|
webdavPass: string
|
||||||
webdavPath: string
|
webdavPath: string
|
||||||
|
fileName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppInfo = {
|
export type AppInfo = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user