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:
dray 2024-09-29 02:27:44 +08:00 committed by 亢奋猫
parent 2771a842fe
commit 2e1b433365
7 changed files with 283 additions and 31 deletions

View File

@ -39,7 +39,8 @@
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"unzipper": "^0.12.3" "unzipper": "^0.12.3",
"webdav": "^5.7.1"
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/sdk": "^0.24.3", "@anthropic-ai/sdk": "^0.24.3",

View File

@ -180,6 +180,13 @@
"general.backup.title": "Data Backup and Recovery", "general.backup.title": "Data Backup and Recovery",
"general.backup.button": "Backup", "general.backup.button": "Backup",
"general.restore.button": "Restore", "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.title": "Data Reset",
"general.reset.button": "Reset", "general.reset.button": "Reset",
"general.check_update_setting": "Check for updates", "general.check_update_setting": "Check for updates",

View File

@ -182,6 +182,13 @@
"general.restore.button": "恢复", "general.restore.button": "恢复",
"general.reset.title": "重置数据", "general.reset.title": "重置数据",
"general.reset.button": "重置", "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.check_update_setting": "更新设置",
"general.manual_update_check": "手动检查更新", "general.manual_update_check": "手动检查更新",
"general.auto_update_check": "自动检查更新", "general.auto_update_check": "自动检查更新",

View File

@ -180,6 +180,13 @@
"general.backup.title": "資料備份與復原", "general.backup.title": "資料備份與復原",
"general.backup.button": "備份", "general.backup.button": "備份",
"general.restore.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.title": "資料重置",
"general.reset.button": "重置", "general.reset.button": "重置",
"general.check_update_setting": "更新設定", "general.check_update_setting": "更新設定",

View File

@ -3,10 +3,16 @@ import { HStack } from '@renderer/components/Layout'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' 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 { useAppDispatch } from '@renderer/store'
import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings' import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings'
import { setProxyUrl as _setProxyUrl } 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 { ThemeMode } from '@renderer/types'
import { isValidProxyUrl } from '@renderer/utils' import { isValidProxyUrl } from '@renderer/utils'
import { Button, Input, Select, Switch } from 'antd' import { Button, Input, Select, Switch } from 'antd'
@ -26,9 +32,19 @@ const GeneralSettings: FC = () => {
manualUpdateCheck, manualUpdateCheck,
setTheme, setTheme,
setWindowStyle, setWindowStyle,
setTopicPosition setTopicPosition,
webdavHost: webDAVHost,
webdavUser: webDAVUser,
webdavPass: webDAVPass,
webdavPath: webDAVPath
} = useSettings() } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl) 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 dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
@ -48,6 +64,20 @@ const GeneralSettings: FC = () => {
window.api.setProxy(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 ( return (
<SettingContainer> <SettingContainer>
<SettingTitle>{t('settings.general.title')}</SettingTitle> <SettingTitle>{t('settings.general.title')}</SettingTitle>
@ -147,6 +177,42 @@ const GeneralSettings: FC = () => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <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> <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">
@ -156,6 +222,13 @@ const GeneralSettings: FC = () => {
<Button onClick={restore} icon={<FolderOpenOutlined />}> <Button onClick={restore} icon={<FolderOpenOutlined />}>
{t('settings.general.restore.button')} {t('settings.general.restore.button')}
</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> </HStack>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />

View File

@ -2,6 +2,9 @@ import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import localforage from 'localforage' import localforage from 'localforage'
import store from '@renderer/store'
import { createClient } from 'webdav'
export async function backup() { export async function backup() {
const version = 3 const version = 3
@ -40,33 +43,10 @@ export async function restore() {
data = JSON.parse(await window.api.decompress(file.content)) 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) { await handleData(data)
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' })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' }) 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 ************************************** */ /************************************* 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() { async function backupDatabase() {
const tables = db.tables const tables = db.tables
const backup = {} const backup = {}

View File

@ -20,6 +20,12 @@ export interface SettingsState {
pasteLongTextAsFile: boolean pasteLongTextAsFile: boolean
clickAssistantToShowTopic: boolean clickAssistantToShowTopic: boolean
manualUpdateCheck: boolean manualUpdateCheck: boolean
// webdav 配置 host, user, pass, path
webdavHost: string
webdavUser: string
webdavPass: string
webdavPath: string
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@ -38,7 +44,12 @@ const initialState: SettingsState = {
topicPosition: 'right', topicPosition: 'right',
pasteLongTextAsFile: true, pasteLongTextAsFile: true,
clickAssistantToShowTopic: false, clickAssistantToShowTopic: false,
manualUpdateCheck: false manualUpdateCheck: false,
webdavHost: '',
webdavUser: '',
webdavPass: '',
webdavPath: '/cherry-studio'
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -99,6 +110,18 @@ const settingsSlice = createSlice({
}, },
setManualUpdateCheck: (state, action: PayloadAction<boolean>) => { setManualUpdateCheck: (state, action: PayloadAction<boolean>) => {
state.manualUpdateCheck = action.payload 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, setTopicPosition,
setPasteLongTextAsFile, setPasteLongTextAsFile,
setClickAssistantToShowTopic, setClickAssistantToShowTopic,
setManualUpdateCheck setManualUpdateCheck,
setWebdavHost,
setWebdavUser,
setWebdavPass,
setWebdavPath
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer