feat: add webdav settings component and backup user data files #69
This commit is contained in:
+3
-1
@@ -29,8 +29,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
ipcMain.handle('reload', () => mainWindow.reload())
|
||||
|
||||
ipcMain.handle('backup:save', backupManager.backup)
|
||||
ipcMain.handle('backup:backup', backupManager.backup)
|
||||
ipcMain.handle('backup:restore', backupManager.restore)
|
||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||
|
||||
ipcMain.handle('file:open', fileManager.open)
|
||||
ipcMain.handle('file:save', fileManager.save)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import archiver from 'archiver'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
@@ -5,16 +6,25 @@ import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as unzipper from 'unzipper'
|
||||
|
||||
import WebDav from './WebDav'
|
||||
|
||||
class BackupManager {
|
||||
private tempDir: string
|
||||
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
||||
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
|
||||
|
||||
constructor() {
|
||||
this.tempDir = path.join(app.getPath('temp'), 'CherryStudio', 'backup')
|
||||
this.backup = this.backup.bind(this)
|
||||
this.restore = this.restore.bind(this)
|
||||
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||
}
|
||||
|
||||
async backup(_: Electron.IpcMainInvokeEvent, data: string, fileName: string, destinationPath: string): Promise<void> {
|
||||
async backup(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
fileName: string,
|
||||
data: string,
|
||||
destinationPath: string = this.backupDir
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
@@ -29,7 +39,7 @@ class BackupManager {
|
||||
await fs.copy(sourcePath, tempDataDir)
|
||||
|
||||
// 创建 zip 文件
|
||||
const output = fs.createWriteStream(path.join(destinationPath, `${fileName}.zip`))
|
||||
const output = fs.createWriteStream(path.join(destinationPath, fileName))
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
archive.pipe(output)
|
||||
@@ -40,42 +50,60 @@ class BackupManager {
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('Backup completed successfully')
|
||||
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
|
||||
return backupedFilePath
|
||||
} catch (error) {
|
||||
Logger.error('Backup failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<{ data: string; success: boolean }> {
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 解压备份文件到临时目录
|
||||
await fs
|
||||
.createReadStream(backupPath)
|
||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
||||
.promise()
|
||||
// 解压备份文件到临时目录
|
||||
await fs
|
||||
.createReadStream(backupPath)
|
||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
||||
.promise()
|
||||
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('Restore completed successfully')
|
||||
return { data, success: true }
|
||||
} catch (error) {
|
||||
Logger.error('Restore failed:', error)
|
||||
return { data: '', success: false }
|
||||
}
|
||||
Logger.log('Restore completed successfully')
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||
const filename = 'cherry-studio.backup.zip'
|
||||
const backupedFilePath = await this.backup(_, filename, data)
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||
overwrite: true
|
||||
})
|
||||
}
|
||||
|
||||
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||
const filename = 'cherry-studio.backup.zip'
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
const retrievedFile = await webdavClient.getFileContents(filename)
|
||||
const backupedFilePath = path.join(this.backupDir, filename)
|
||||
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||
return await this.restore(_, backupedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import Stream from 'stream'
|
||||
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||
|
||||
export default class WebDav {
|
||||
public instance: WebDAVClient | undefined
|
||||
private webdavPath: string
|
||||
|
||||
constructor(params: WebDavConfig) {
|
||||
this.webdavPath = params.webdavPath.replace('/', '')
|
||||
|
||||
this.instance = createClient(params.webdavHost, {
|
||||
username: params.webdavUser,
|
||||
password: params.webdavPass
|
||||
})
|
||||
|
||||
this.putFileContents = this.putFileContents.bind(this)
|
||||
this.getFileContents = this.getFileContents.bind(this)
|
||||
}
|
||||
|
||||
public putFileContents = async (
|
||||
filename: string,
|
||||
data: string | BufferLike | Stream.Readable,
|
||||
options?: PutFileContentsOptions
|
||||
) => {
|
||||
if (!this.instance) {
|
||||
return new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await this.instance.exists(this.webdavPath))) {
|
||||
await this.instance.createDirectory(this.webdavPath, {
|
||||
recursive: true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
|
||||
try {
|
||||
return await this.instance.putFileContents(remoteFilePath, data, options)
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error putting file contents on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public getFileContents = async (filename: string, options?: GetFileContentsOptions) => {
|
||||
if (!this.instance) {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
|
||||
try {
|
||||
return await this.instance.getFileContents(remoteFilePath, options)
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error getting file contents on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+6
-2
@@ -1,6 +1,8 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -20,8 +22,10 @@ declare global {
|
||||
compress: (text: string) => Promise<Buffer>
|
||||
decompress: (text: Buffer) => Promise<string>
|
||||
backup: {
|
||||
save: (data: string, fileName: string, destinationPath: string) => Promise<void>
|
||||
restore: (backupPath: string) => Promise<{ data: string; success: boolean }>
|
||||
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
|
||||
restore: (backupPath: string) => Promise<string>
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||
}
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -11,10 +12,12 @@ const api = {
|
||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||
reload: () => ipcRenderer.invoke('reload'),
|
||||
backup: {
|
||||
save: (data: string, fileName: string, destinationPath: string) => {
|
||||
ipcRenderer.invoke('backup:save', data, fileName, destinationPath)
|
||||
},
|
||||
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath)
|
||||
backup: (fileName: string, data: string, destinationPath?: string) =>
|
||||
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
|
||||
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
||||
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
|
||||
},
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||
|
||||
@@ -46,12 +46,14 @@
|
||||
"error.enter.api.host": "Please enter your API host first",
|
||||
"error.enter.model": "Please select a model first",
|
||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||
"api.connection.failed": "Connection failed",
|
||||
"api.connection.success": "Connection successful",
|
||||
"chat.completion.paused": "Chat completion paused",
|
||||
"switch.disabled": "Switching is disabled while the assistant is generating",
|
||||
"restore.success": "Restored successfully",
|
||||
"backup.success": "Backup successful",
|
||||
"backup.failed": "Backup failed",
|
||||
"reset.confirm.content": "Are you sure you want to clear all data?",
|
||||
"reset.double.confirm.title": "DATA LOST !!!",
|
||||
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
||||
@@ -180,11 +182,14 @@
|
||||
"general.backup.title": "Data Backup and Recovery",
|
||||
"general.backup.button": "Backup",
|
||||
"general.restore.button": "Restore",
|
||||
"general.view_webdav_settings": "View WebDAV settings",
|
||||
"general.webdav.title": "WebDAV",
|
||||
"general.webdav.host": "WebDAV Host, e.g. http://localhost:8080",
|
||||
"general.webdav.host": "WebDAV Host",
|
||||
"general.webdav.host.placeholder": "http://localhost:8080",
|
||||
"general.webdav.user": "WebDAV User",
|
||||
"general.webdav.password": "WebDAV Password",
|
||||
"general.webdav.path": "WebDAV Path, e.g. /backup",
|
||||
"general.webdav.path": "WebDAV Path",
|
||||
"general.webdav.path.placeholder": "/backup",
|
||||
"general.webdav.backup.button": "Backup to WebDAV",
|
||||
"general.webdav.restore.button": "Restore from WebDAV",
|
||||
"general.reset.title": "Data Reset",
|
||||
|
||||
@@ -46,12 +46,14 @@
|
||||
"error.enter.api.host": "请输入您的 API 地址",
|
||||
"error.enter.model": "请选择一个模型",
|
||||
"error.invalid.proxy.url": "无效的代理地址",
|
||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||
"api.connection.failed": "连接失败",
|
||||
"api.connection.success": "连接成功",
|
||||
"chat.completion.paused": "会话已停止",
|
||||
"switch.disabled": "模型回复完成后才能切换",
|
||||
"restore.success": "恢复成功",
|
||||
"backup.success": "备份成功",
|
||||
"backup.failed": "备份失败",
|
||||
"reset.confirm.content": "确定要重置所有数据吗?",
|
||||
"reset.double.confirm.title": "数据丢失!!!",
|
||||
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
||||
@@ -182,11 +184,14 @@
|
||||
"general.restore.button": "恢复",
|
||||
"general.reset.title": "重置数据",
|
||||
"general.reset.button": "重置",
|
||||
"general.view_webdav_settings": "查看 WebDAV 设置",
|
||||
"general.webdav.title": "WebDAV",
|
||||
"general.webdav.host": "WebDAV Host, e.g. http://localhost:8080",
|
||||
"general.webdav.user": "WebDAV User",
|
||||
"general.webdav.password": "WebDAV Password",
|
||||
"general.webdav.path": "WebDAV Path, e.g. /backup",
|
||||
"general.webdav.host": "WebDAV 地址",
|
||||
"general.webdav.host.placeholder": "http://localhost:8080",
|
||||
"general.webdav.user": "WebDAV 用户名",
|
||||
"general.webdav.password": "WebDAV 密码",
|
||||
"general.webdav.path": "WebDAV 路径",
|
||||
"general.webdav.path.placeholder": "/backup",
|
||||
"general.webdav.backup.button": "备份到 WebDAV",
|
||||
"general.webdav.restore.button": "从 WebDAV 恢复",
|
||||
"general.check_update_setting": "更新设置",
|
||||
|
||||
@@ -46,12 +46,14 @@
|
||||
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
||||
"error.enter.model": "請先選擇一個模型",
|
||||
"error.invalid.proxy.url": "無效的代理 URL",
|
||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||
"api.connection.failed": "連接失敗",
|
||||
"api.connection.success": "連接成功",
|
||||
"chat.completion.paused": "聊天完成已暫停",
|
||||
"switch.disabled": "助手生成回覆時無法切換",
|
||||
"restore.success": "恢復成功",
|
||||
"backup.success": "備份成功",
|
||||
"backup.failed": "備份失敗",
|
||||
"reset.confirm.content": "確定要清除所有資料嗎?",
|
||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
|
||||
@@ -180,11 +182,14 @@
|
||||
"general.backup.title": "資料備份與復原",
|
||||
"general.backup.button": "備份",
|
||||
"general.restore.button": "復原",
|
||||
"general.view_webdav_settings": "查看 WebDAV 設定",
|
||||
"general.webdav.title": "WebDAV",
|
||||
"general.webdav.host": "WebDAV Host, e.g. http://localhost:8080",
|
||||
"general.webdav.user": "WebDAV User",
|
||||
"general.webdav.password": "WebDAV Password",
|
||||
"general.webdav.path": "WebDAV Path, e.g. /backup",
|
||||
"general.webdav.host": "WebDAV 主機位址",
|
||||
"general.webdav.host.placeholder": "http://localhost:8080",
|
||||
"general.webdav.user": "WebDAV 使用者名稱",
|
||||
"general.webdav.password": "WebDAV 密碼",
|
||||
"general.webdav.path": "WebDAV Path",
|
||||
"general.webdav.path.placeholder": "/backup",
|
||||
"general.webdav.backup.button": "從 WebDAV 備份",
|
||||
"general.webdav.restore.button": "從 WebDAV 恢復",
|
||||
"general.reset.title": "資料重置",
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { backup, reset, restore, backupToWebdav, restoreFromWebdav } from '@renderer/services/backup'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings'
|
||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import {
|
||||
setWebdavHost as _setWebdavHost,
|
||||
setWebdavPass as _setWebdavPass,
|
||||
setWebdavPath as _setWebdavPath,
|
||||
setWebdavUser as _setWebdavUser
|
||||
} from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { isValidProxyUrl } from '@renderer/utils'
|
||||
import { Button, Input, Select, Switch } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const {
|
||||
language,
|
||||
proxyUrl: storeProxyUrl,
|
||||
theme,
|
||||
windowStyle,
|
||||
topicPosition,
|
||||
clickAssistantToShowTopic,
|
||||
manualUpdateCheck,
|
||||
setTheme,
|
||||
setWindowStyle,
|
||||
setTopicPosition,
|
||||
|
||||
webdavHost: webDAVHost,
|
||||
webdavUser: webDAVUser,
|
||||
webdavPass: webDAVPass,
|
||||
webdavPath: webDAVPath
|
||||
} = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
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 dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onSelectLanguage = (value: string) => {
|
||||
dispatch(setLanguage(value))
|
||||
localStorage.setItem('language', value)
|
||||
i18n.changeLanguage(value)
|
||||
}
|
||||
|
||||
const onSetProxyUrl = () => {
|
||||
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
|
||||
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(_setProxyUrl(proxyUrl))
|
||||
window.api.setProxy(proxyUrl)
|
||||
}
|
||||
|
||||
const onSetWebdav = () => {
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
||||
return
|
||||
}
|
||||
|
||||
console.log('webdav', webdavHost, webdavUser, webdavPass, webdavPath)
|
||||
|
||||
dispatch(_setWebdavHost(webdavHost))
|
||||
dispatch(_setWebdavUser(webdavUser))
|
||||
dispatch(_setWebdavPass(webdavPass))
|
||||
dispatch(_setWebdavPath(webdavPath))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.general.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={language || 'en-US'}
|
||||
style={{ width: 180 }}
|
||||
onChange={onSelectLanguage}
|
||||
options={[
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
{ value: 'zh-TW', label: '中文(繁体)' },
|
||||
{ value: 'en-US', label: 'English' }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={theme}
|
||||
style={{ width: 180 }}
|
||||
onChange={setTheme}
|
||||
options={[
|
||||
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
||||
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
|
||||
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
{isMac && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={windowStyle || 'opaque'}
|
||||
style={{ width: 180 }}
|
||||
onChange={setWindowStyle}
|
||||
options={[
|
||||
{ value: 'transparent', label: t('settings.theme.window.style.transparent') },
|
||||
{ value: 'opaque', label: t('settings.theme.window.style.opaque') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={topicPosition || 'right'}
|
||||
style={{ width: 180 }}
|
||||
onChange={setTopicPosition}
|
||||
options={[
|
||||
{ value: 'left', label: t('settings.topic.position.left') },
|
||||
{ value: 'right', label: t('settings.topic.position.right') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
{topicPosition === 'left' && (
|
||||
<>
|
||||
<SettingRow style={{ minHeight: 32 }}>
|
||||
<SettingRowTitle>{t('settings.advanced.click_assistant_switch_to_topics')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={clickAssistantToShowTopic}
|
||||
onChange={(checked) => dispatch(setClickAssistantToShowTopic(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</>
|
||||
)}
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.check_update_setting')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={manualUpdateCheck ?? false}
|
||||
style={{ width: 180 }}
|
||||
onChange={(v) => dispatch(setManualUpdateCheck(v))}
|
||||
options={[
|
||||
{ value: false, label: t('settings.general.auto_update_check') },
|
||||
{ value: true, label: t('settings.general.manual_update_check') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder="socks5://127.0.0.1:6153"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
style={{ width: 180 }}
|
||||
onBlur={() => onSetProxyUrl()}
|
||||
type="url"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
{/* 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path */}
|
||||
<SettingRowTitle>{t('settings.general.webdav.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Input
|
||||
placeholder={t('settings.general.webdav.host')}
|
||||
value={webdavHost}
|
||||
onChange={(e) => setWebdavHost(e.target.value)}
|
||||
style={{ width: 280 }}
|
||||
type="url"
|
||||
onBlur={onSetWebdav}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('settings.general.webdav.user')}
|
||||
value={webdavUser}
|
||||
onChange={(e) => setWebdavUser(e.target.value)}
|
||||
style={{ width: 120 }}
|
||||
onBlur={onSetWebdav}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('settings.general.webdav.password')}
|
||||
value={webdavPass}
|
||||
onChange={(e) => setWebdavPass(e.target.value)}
|
||||
style={{ width: 140 }}
|
||||
onBlur={onSetWebdav}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('settings.general.webdav.path')}
|
||||
value={webdavPath}
|
||||
onChange={(e) => setWebdavPath(e.target.value)}
|
||||
style={{ width: 220 }}
|
||||
onBlur={onSetWebdav}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button onClick={backup} icon={<SaveOutlined />}>
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
{/* 添加 在线备份 在线还原 按钮 */}
|
||||
<Button onClick={backupToWebdav} icon={<SaveOutlined />}>
|
||||
{t('settings.general.webdav.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={restoreFromWebdav} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.general.webdav.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button onClick={reset} danger>
|
||||
{t('settings.general.reset.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralSettings
|
||||
@@ -0,0 +1,195 @@
|
||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { backup, reset, restore } from '@renderer/services/backup'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings'
|
||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { isValidProxyUrl } from '@renderer/utils'
|
||||
import { Button, Input, Select, Switch } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import WebDavSettings from './WebDavSettings'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const {
|
||||
language,
|
||||
proxyUrl: storeProxyUrl,
|
||||
theme,
|
||||
windowStyle,
|
||||
topicPosition,
|
||||
clickAssistantToShowTopic,
|
||||
manualUpdateCheck,
|
||||
setTheme,
|
||||
setWindowStyle,
|
||||
setTopicPosition
|
||||
} = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onSelectLanguage = (value: string) => {
|
||||
dispatch(setLanguage(value))
|
||||
localStorage.setItem('language', value)
|
||||
i18n.changeLanguage(value)
|
||||
}
|
||||
|
||||
const onSetProxyUrl = () => {
|
||||
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
|
||||
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(_setProxyUrl(proxyUrl))
|
||||
window.api.setProxy(proxyUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.general.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={language || 'en-US'}
|
||||
style={{ width: 180 }}
|
||||
onChange={onSelectLanguage}
|
||||
options={[
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
{ value: 'zh-TW', label: '中文(繁体)' },
|
||||
{ value: 'en-US', label: 'English' }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={theme}
|
||||
style={{ width: 180 }}
|
||||
onChange={setTheme}
|
||||
options={[
|
||||
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
||||
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
|
||||
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
{isMac && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={windowStyle || 'opaque'}
|
||||
style={{ width: 180 }}
|
||||
onChange={setWindowStyle}
|
||||
options={[
|
||||
{ value: 'transparent', label: t('settings.theme.window.style.transparent') },
|
||||
{ value: 'opaque', label: t('settings.theme.window.style.opaque') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={topicPosition || 'right'}
|
||||
style={{ width: 180 }}
|
||||
onChange={setTopicPosition}
|
||||
options={[
|
||||
{ value: 'left', label: t('settings.topic.position.left') },
|
||||
{ value: 'right', label: t('settings.topic.position.right') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
{topicPosition === 'left' && (
|
||||
<>
|
||||
<SettingRow style={{ minHeight: 32 }}>
|
||||
<SettingRowTitle>{t('settings.advanced.click_assistant_switch_to_topics')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={clickAssistantToShowTopic}
|
||||
onChange={(checked) => dispatch(setClickAssistantToShowTopic(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</>
|
||||
)}
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.check_update_setting')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={manualUpdateCheck ?? false}
|
||||
style={{ width: 180 }}
|
||||
onChange={(v) => dispatch(setManualUpdateCheck(v))}
|
||||
options={[
|
||||
{ value: false, label: t('settings.general.auto_update_check') },
|
||||
{ value: true, label: t('settings.general.manual_update_check') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder="socks5://127.0.0.1:6153"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
style={{ width: 180 }}
|
||||
onBlur={() => onSetProxyUrl()}
|
||||
type="url"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow style={{ minHeight: 32 }}>
|
||||
<SettingRowTitle>{t('settings.general.webdav.title')}</SettingRowTitle>
|
||||
<VStack gap="5px">
|
||||
<Link to="/settings/general/webdav" style={{ color: 'var(--color-text-2)' }}>
|
||||
{t('settings.general.view_webdav_settings')}
|
||||
</Link>
|
||||
</VStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button onClick={backup} icon={<SaveOutlined />}>
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button onClick={reset} danger>
|
||||
{t('settings.general.reset.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
}
|
||||
/>
|
||||
<Route path="webdav" element={<WebDavSettings />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralSettings
|
||||
@@ -0,0 +1,137 @@
|
||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { backupToWebdav, restoreFromWebdav } from '@renderer/services/backup'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setWebdavHost as _setWebdavHost,
|
||||
setWebdavPass as _setWebdavPass,
|
||||
setWebdavPath as _setWebdavPath,
|
||||
setWebdavUser as _setWebdavUser
|
||||
} from '@renderer/store/settings'
|
||||
import { Breadcrumb, Button, Input } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const WebDavSettings: FC = () => {
|
||||
const {
|
||||
webdavHost: webDAVHost,
|
||||
webdavUser: webDAVUser,
|
||||
webdavPass: webDAVPass,
|
||||
webdavPath: webDAVPath
|
||||
} = 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 [backuping, setBackuping] = useState(false)
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 把之前备份的文件定时上传到 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()
|
||||
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)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: t('settings.general.title'),
|
||||
href: '#/settings/general'
|
||||
},
|
||||
{
|
||||
title: t('settings.general.webdav.title')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<SettingTitle style={{ marginTop: 20 }}>{t('settings.general.webdav.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.webdav.host')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.general.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.general.webdav.user')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.general.webdav.user')}
|
||||
value={webdavUser}
|
||||
onChange={(e) => setWebdavUser(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={() => dispatch(_setWebdavUser(webdavUser || ''))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.webdav.password')}</SettingRowTitle>
|
||||
<Input.Password
|
||||
placeholder={t('settings.general.webdav.password')}
|
||||
value={webdavPass}
|
||||
onChange={(e) => setWebdavPass(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={() => dispatch(_setWebdavPass(webdavPass || ''))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.webdav.path')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.general.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={onBackup} icon={<SaveOutlined />} loading={backuping}>
|
||||
{t('settings.general.webdav.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={onRestore} icon={<FolderOpenOutlined />} loading={restoring}>
|
||||
{t('settings.general.webdav.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebDavSettings
|
||||
@@ -8,7 +8,7 @@ import styled from 'styled-components'
|
||||
|
||||
import AboutSettings from './AboutSettings'
|
||||
import AssistantSettings from './AssistantSettings'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
import GeneralSettings from './GeneralSettings/GeneralSettings'
|
||||
import ModelSettings from './ModelSettings'
|
||||
import ProvidersList from './ProviderSettings'
|
||||
|
||||
@@ -16,7 +16,7 @@ const SettingsPage: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -65,7 +65,7 @@ const SettingsPage: FC = () => {
|
||||
<Route path="provider" element={<ProvidersList />} />
|
||||
<Route path="model" element={<ModelSettings />} />
|
||||
<Route path="assistant" element={<AssistantSettings />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="general/*" element={<GeneralSettings />} />
|
||||
<Route path="about" element={<AboutSettings />} />
|
||||
</Routes>
|
||||
</SettingContent>
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import dayjs from 'dayjs'
|
||||
import localforage from 'localforage'
|
||||
import store from '@renderer/store'
|
||||
|
||||
import { createClient } from 'webdav'
|
||||
|
||||
export async function backup() {
|
||||
const version = 3
|
||||
const time = new Date().getTime()
|
||||
|
||||
const data = {
|
||||
time,
|
||||
version,
|
||||
localStorage,
|
||||
indexedDB: await backupDatabase()
|
||||
}
|
||||
|
||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}`
|
||||
const fileContnet = JSON.stringify(data)
|
||||
|
||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
||||
const fileContnet = await getBackupData()
|
||||
const selectFolder = await window.api.file.selectFolder()
|
||||
|
||||
if (selectFolder) {
|
||||
await window.api.backup.save(fileContnet, filename, selectFolder)
|
||||
await window.api.backup.backup(filename, fileContnet, selectFolder)
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
}
|
||||
}
|
||||
@@ -38,7 +24,7 @@ export async function restore() {
|
||||
// zip backup file
|
||||
if (file?.fileName.endsWith('.zip')) {
|
||||
const restoreData = await window.api.backup.restore(file.filePath)
|
||||
data = JSON.parse(restoreData.data)
|
||||
data = JSON.parse(restoreData)
|
||||
} else {
|
||||
data = JSON.parse(await window.api.decompress(file.content))
|
||||
}
|
||||
@@ -78,127 +64,73 @@ export async function reset() {
|
||||
|
||||
// 备份到 webdav
|
||||
export async function backupToWebdav() {
|
||||
// 先走之前的 backup 流程,存储到临时文件
|
||||
const version = 3
|
||||
const time = new Date().getTime()
|
||||
|
||||
const data = {
|
||||
time,
|
||||
version,
|
||||
localStorage,
|
||||
indexedDB: await backupDatabase()
|
||||
}
|
||||
|
||||
const filename = `cherry-studio.backup.json`
|
||||
const fileContent = JSON.stringify(data)
|
||||
|
||||
// 获取 userSetting 里的 WebDAV 配置
|
||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||
// console.log('backup.backupToWebdav', webdavHost, webdavUser, webdavPass, webdavPath)
|
||||
|
||||
let host = webdavHost
|
||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
||||
host = `http://${host}`
|
||||
}
|
||||
console.log('backup.backupToWebdav', host)
|
||||
const backupData = await getBackupData()
|
||||
|
||||
// 创建 WebDAV 客户端
|
||||
const client = createClient(
|
||||
host, // WebDAV 服务器地址
|
||||
{
|
||||
username: webdavUser, // 用户名
|
||||
password: webdavPass // 密码
|
||||
}
|
||||
)
|
||||
|
||||
// 上传文件到 WebDAV
|
||||
const remoteFilePath = `${webdavPath}/${filename}`
|
||||
|
||||
// 先检查创建目录
|
||||
try {
|
||||
if (!(await client.exists(webdavPath))) {
|
||||
await client.createDirectory(webdavPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating directory on WebDAV:', error)
|
||||
}
|
||||
console.debug({
|
||||
webdavHost,
|
||||
webdavUser,
|
||||
webdavPass,
|
||||
webdavPath
|
||||
})
|
||||
|
||||
// 上传文件
|
||||
try {
|
||||
await client.putFileContents(remoteFilePath, fileContent, { overwrite: true })
|
||||
console.log('File uploaded successfully!')
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
} catch (error) {
|
||||
console.error('Error uploading file to WebDAV:', error)
|
||||
const success = await window.api.backup.backupToWebdav(backupData, {
|
||||
webdavHost,
|
||||
webdavUser,
|
||||
webdavPass,
|
||||
webdavPath
|
||||
})
|
||||
if (success) {
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
} else {
|
||||
window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||
window.modal.error({
|
||||
title: i18n.t('message.backup.failed'),
|
||||
content: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 从 webdav 恢复
|
||||
export async function restoreFromWebdav() {
|
||||
const filename = `cherry-studio.backup.json`
|
||||
|
||||
// 获取 userSetting 里的 WebDAV 配置
|
||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||
// console.log('backup.restoreFromWebdav', webdavHost, webdavUser, webdavPass, webdavPath)
|
||||
let data = ''
|
||||
|
||||
let host = webdavHost
|
||||
if (!host.startsWith('http://') && !host.startsWith('https://')) {
|
||||
host = `http://${host}`
|
||||
}
|
||||
console.log('backup.restoreFromWebdav', host)
|
||||
|
||||
// 创建 WebDAV 客户端
|
||||
const client = createClient(
|
||||
host, // WebDAV 服务器地址
|
||||
{
|
||||
username: webdavUser, // 用户名
|
||||
password: webdavPass // 密码
|
||||
}
|
||||
)
|
||||
|
||||
// 上传文件到 WebDAV
|
||||
const remoteFilePath = `${webdavPath}/${filename}`
|
||||
|
||||
// 下载文件
|
||||
try {
|
||||
// 下载文件内容
|
||||
const fileContent = await client.getFileContents(remoteFilePath, { format: 'text' })
|
||||
console.log('File downloaded successfully!', fileContent)
|
||||
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath })
|
||||
} catch (error: any) {
|
||||
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
||||
window.modal.error({
|
||||
title: i18n.t('message.restore.failed'),
|
||||
content: error.message
|
||||
})
|
||||
}
|
||||
|
||||
// 处理文件内容
|
||||
const data = parseFileContent(fileContent.toString())
|
||||
console.log('Parsed file content:', data)
|
||||
|
||||
await handleData(data)
|
||||
try {
|
||||
await handleData(JSON.parse(data))
|
||||
} catch (error) {
|
||||
console.error('Error downloading file from WebDAV:', error)
|
||||
console.error('[backup] Error downloading file from WebDAV:', error)
|
||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||
}
|
||||
}
|
||||
|
||||
/************************************* Backup Utils ************************************** */
|
||||
|
||||
function parseFileContent(fileContent: string | Buffer | { data: string | Buffer } | ArrayBuffer): any {
|
||||
let fileContentString: string
|
||||
|
||||
if (typeof fileContent === 'string') {
|
||||
fileContentString = fileContent
|
||||
} else if (Buffer.isBuffer(fileContent)) {
|
||||
fileContentString = fileContent.toString('utf-8')
|
||||
} else if (fileContent instanceof ArrayBuffer) {
|
||||
fileContentString = Buffer.from(fileContent).toString('utf-8')
|
||||
} else if (fileContent && typeof fileContent.data === 'string') {
|
||||
fileContentString = fileContent.data
|
||||
} else if (fileContent && Buffer.isBuffer(fileContent.data)) {
|
||||
fileContentString = fileContent.data.toString('utf-8')
|
||||
} else {
|
||||
throw new Error('Unsupported file content type')
|
||||
}
|
||||
|
||||
return JSON.parse(fileContentString)
|
||||
async function getBackupData() {
|
||||
return JSON.stringify({
|
||||
time: new Date().getTime(),
|
||||
version: 3,
|
||||
localStorage,
|
||||
indexedDB: await backupDatabase()
|
||||
})
|
||||
}
|
||||
|
||||
async function handleData(data: any) {
|
||||
/************************************* Backup Utils ************************************** */
|
||||
async function handleData(data: Record<string, any>) {
|
||||
if (data.version === 1) {
|
||||
await clearDatabase()
|
||||
|
||||
|
||||
@@ -116,3 +116,10 @@ export enum ThemeMode {
|
||||
dark = 'dark',
|
||||
auto = 'auto'
|
||||
}
|
||||
|
||||
export type WebDavConfig = {
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
webdavPass: string
|
||||
webdavPath: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user