feat: 添加 WebDAV 配置项
为应用程序添加了 WebDAV 配置项,包括主机、用户、密码和路径。这样用户就可以将备份文件定时上传到 WebDAV 服务器,并从 WebDAV 服务器恢复备份文件。 - 添加了新的依赖项 "webdav": "^5.7.1" - 修改了 package.json 文件 - 修改了 zh-tw.json、zh-cn.json 和 en-us.json 文件 - 修改了 settings.ts 文件 - 修改了 GeneralSettings.tsx 文件 https://github.com/kangfenmao/cherry-studio/issues/129
This commit is contained in:
parent
2771a842fe
commit
2e1b433365
@ -39,7 +39,8 @@
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"unzipper": "^0.12.3"
|
||||
"unzipper": "^0.12.3",
|
||||
"webdav": "^5.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.3",
|
||||
|
||||
@ -180,6 +180,13 @@
|
||||
"general.backup.title": "Data Backup and Recovery",
|
||||
"general.backup.button": "Backup",
|
||||
"general.restore.button": "Restore",
|
||||
"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.backup.button": "Backup to WebDAV",
|
||||
"general.webdav.restore.button": "Restore from WebDAV",
|
||||
"general.reset.title": "Data Reset",
|
||||
"general.reset.button": "Reset",
|
||||
"general.check_update_setting": "Check for updates",
|
||||
|
||||
@ -182,6 +182,13 @@
|
||||
"general.restore.button": "恢复",
|
||||
"general.reset.title": "重置数据",
|
||||
"general.reset.button": "重置",
|
||||
"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.backup.button": "备份到 WebDAV",
|
||||
"general.webdav.restore.button": "从 WebDAV 恢复",
|
||||
"general.check_update_setting": "更新设置",
|
||||
"general.manual_update_check": "手动检查更新",
|
||||
"general.auto_update_check": "自动检查更新",
|
||||
|
||||
@ -180,6 +180,13 @@
|
||||
"general.backup.title": "資料備份與復原",
|
||||
"general.backup.button": "備份",
|
||||
"general.restore.button": "復原",
|
||||
"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.backup.button": "從 WebDAV 備份",
|
||||
"general.webdav.restore.button": "從 WebDAV 恢復",
|
||||
"general.reset.title": "資料重置",
|
||||
"general.reset.button": "重置",
|
||||
"general.check_update_setting": "更新設定",
|
||||
|
||||
@ -3,10 +3,16 @@ 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 } from '@renderer/services/backup'
|
||||
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'
|
||||
@ -26,9 +32,19 @@ const GeneralSettings: FC = () => {
|
||||
manualUpdateCheck,
|
||||
setTheme,
|
||||
setWindowStyle,
|
||||
setTopicPosition
|
||||
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()
|
||||
|
||||
@ -48,6 +64,20 @@ const GeneralSettings: FC = () => {
|
||||
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>
|
||||
@ -147,6 +177,42 @@ const GeneralSettings: FC = () => {
|
||||
/>
|
||||
</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">
|
||||
@ -156,6 +222,13 @@ const GeneralSettings: FC = () => {
|
||||
<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 />
|
||||
|
||||
@ -2,6 +2,9 @@ import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import localforage from 'localforage'
|
||||
import store from '@renderer/store'
|
||||
|
||||
import { createClient } from 'webdav'
|
||||
|
||||
export async function backup() {
|
||||
const version = 3
|
||||
@ -40,33 +43,10 @@ export async function restore() {
|
||||
data = JSON.parse(await window.api.decompress(file.content))
|
||||
}
|
||||
|
||||
if (data.version === 1) {
|
||||
await clearDatabase()
|
||||
// 处理文件内容
|
||||
console.log('Parsed file content:', data)
|
||||
|
||||
for (const { key, value } of data.indexedDB) {
|
||||
if (key.startsWith('topic:')) {
|
||||
await db.table('topics').add({ id: value.id, messages: value.messages })
|
||||
}
|
||||
if (key === 'image://avatar') {
|
||||
await db.table('settings').add({ id: key, value })
|
||||
}
|
||||
}
|
||||
|
||||
await localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
||||
setTimeout(() => window.api.reload(), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.version >= 2) {
|
||||
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||
await restoreDatabase(data.indexedDB)
|
||||
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
||||
setTimeout(() => window.api.reload(), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||
await handleData(data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||
@ -96,8 +76,158 @@ 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)
|
||||
|
||||
// 创建 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)
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 从 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 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)
|
||||
|
||||
// 处理文件内容
|
||||
const data = parseFileContent(fileContent.toString())
|
||||
console.log('Parsed file content:', data)
|
||||
|
||||
await handleData(data)
|
||||
} catch (error) {
|
||||
console.error('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 handleData(data: any) {
|
||||
if (data.version === 1) {
|
||||
await clearDatabase()
|
||||
|
||||
for (const { key, value } of data.indexedDB) {
|
||||
if (key.startsWith('topic:')) {
|
||||
await db.table('topics').add({ id: value.id, messages: value.messages })
|
||||
}
|
||||
if (key === 'image://avatar') {
|
||||
await db.table('settings').add({ id: key, value })
|
||||
}
|
||||
}
|
||||
|
||||
await localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
||||
setTimeout(() => window.api.reload(), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.version >= 2) {
|
||||
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||
await restoreDatabase(data.indexedDB)
|
||||
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
||||
setTimeout(() => window.api.reload(), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||
}
|
||||
|
||||
async function backupDatabase() {
|
||||
const tables = db.tables
|
||||
const backup = {}
|
||||
|
||||
@ -20,6 +20,12 @@ export interface SettingsState {
|
||||
pasteLongTextAsFile: boolean
|
||||
clickAssistantToShowTopic: boolean
|
||||
manualUpdateCheck: boolean
|
||||
|
||||
// webdav 配置 host, user, pass, path
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
webdavPass: string
|
||||
webdavPath: string
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
@ -38,7 +44,12 @@ const initialState: SettingsState = {
|
||||
topicPosition: 'right',
|
||||
pasteLongTextAsFile: true,
|
||||
clickAssistantToShowTopic: false,
|
||||
manualUpdateCheck: false
|
||||
manualUpdateCheck: false,
|
||||
|
||||
webdavHost: '',
|
||||
webdavUser: '',
|
||||
webdavPass: '',
|
||||
webdavPath: '/cherry-studio'
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@ -99,6 +110,18 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setManualUpdateCheck: (state, action: PayloadAction<boolean>) => {
|
||||
state.manualUpdateCheck = action.payload
|
||||
},
|
||||
setWebdavHost: (state, action: PayloadAction<string>) => {
|
||||
state.webdavHost = action.payload
|
||||
},
|
||||
setWebdavUser: (state, action: PayloadAction<string>) => {
|
||||
state.webdavUser = action.payload
|
||||
},
|
||||
setWebdavPass: (state, action: PayloadAction<string>) => {
|
||||
state.webdavPass = action.payload
|
||||
},
|
||||
setWebdavPath: (state, action: PayloadAction<string>) => {
|
||||
state.webdavPath = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -121,7 +144,11 @@ export const {
|
||||
setTopicPosition,
|
||||
setPasteLongTextAsFile,
|
||||
setClickAssistantToShowTopic,
|
||||
setManualUpdateCheck
|
||||
setManualUpdateCheck,
|
||||
setWebdavHost,
|
||||
setWebdavUser,
|
||||
setWebdavPass,
|
||||
setWebdavPath
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user