feat: make webdav state persistent, improve webdav autosync (#3690)
* feat: persist webdav state * feat: schedule autosync by taking the last autosync time * fix: correct scheduling behaviour with last error, improve messages * refactor: delay setting lastSyncTime --------- Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
parent
994ffa224e
commit
28c5231741
@ -119,10 +119,10 @@ class BackupManager {
|
|||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
Logger.log('Backup completed successfully')
|
Logger.log('[BackupManager] Backup completed successfully')
|
||||||
return backupedFilePath
|
return backupedFilePath
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('Backup failed:', error)
|
Logger.error('[BackupManager] Backup failed:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { FolderOpenOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
|
import { FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setWebdavAutoSync,
|
setWebdavAutoSync,
|
||||||
setWebdavHost as _setWebdavHost,
|
setWebdavHost as _setWebdavHost,
|
||||||
@ -56,7 +55,7 @@ const WebDavSettings: FC = () => {
|
|||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { webdavSync } = useRuntime()
|
const { webdavSync } = useAppSelector((state) => state.backup)
|
||||||
|
|
||||||
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
||||||
|
|
||||||
@ -82,16 +81,16 @@ const WebDavSettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<HStack gap="5px" alignItems="center">
|
<HStack gap="5px" alignItems="center">
|
||||||
{webdavSync.syncing && <SyncOutlined spin />}
|
{webdavSync.syncing && <SyncOutlined spin />}
|
||||||
|
{!webdavSync.syncing && webdavSync.lastSyncError && (
|
||||||
|
<Tooltip title={`${t('settings.data.webdav.syncError')}: ${webdavSync.lastSyncError}`}>
|
||||||
|
<WarningOutlined style={{ color: 'red' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{webdavSync.lastSyncTime && (
|
{webdavSync.lastSyncTime && (
|
||||||
<span style={{ color: 'var(--text-secondary)' }}>
|
<span style={{ color: 'var(--text-secondary)' }}>
|
||||||
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
|
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{webdavSync.lastSyncError && (
|
|
||||||
<span style={{ color: 'var(--error-color)' }}>
|
|
||||||
{t('settings.data.webdav.syncError')}: {webdavSync.lastSyncError}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
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/backup'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
@ -95,25 +95,28 @@ export async function backupToWebdav({
|
|||||||
if (success) {
|
if (success) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
setWebDAVSyncState({
|
setWebDAVSyncState({
|
||||||
lastSyncTime: Date.now(),
|
|
||||||
lastSyncError: null
|
lastSyncError: null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||||
} else {
|
} else {
|
||||||
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
|
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
|
||||||
showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
||||||
console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
console.error('[Backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||||
showMessage &&
|
|
||||||
window.modal.error({
|
window.modal.error({
|
||||||
title: i18n.t('message.backup.failed'),
|
title: i18n.t('message.backup.failed'),
|
||||||
content: error.message
|
content: error.message
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
store.dispatch(setWebDAVSyncState({ syncing: false }))
|
store.dispatch(
|
||||||
|
setWebDAVSyncState({
|
||||||
|
lastSyncTime: Date.now(),
|
||||||
|
syncing: false
|
||||||
|
})
|
||||||
|
)
|
||||||
isManualBackupRunning = false
|
isManualBackupRunning = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +129,7 @@ export async function restoreFromWebdav(fileName?: string) {
|
|||||||
try {
|
try {
|
||||||
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath, fileName })
|
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({
|
||||||
title: i18n.t('message.restore.failed'),
|
title: i18n.t('message.restore.failed'),
|
||||||
content: error.message
|
content: error.message
|
||||||
@ -136,7 +139,7 @@ export async function restoreFromWebdav(fileName?: string) {
|
|||||||
try {
|
try {
|
||||||
await handleData(JSON.parse(data))
|
await handleData(JSON.parse(data))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[backup] 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' })
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,6 +174,7 @@ export function startAutoSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { webdavSyncInterval } = store.getState().settings
|
const { webdavSyncInterval } = store.getState().settings
|
||||||
|
const { webdavSync } = store.getState().backup
|
||||||
|
|
||||||
if (webdavSyncInterval <= 0) {
|
if (webdavSyncInterval <= 0) {
|
||||||
console.log('[AutoSync] Invalid sync interval, auto sync disabled')
|
console.log('[AutoSync] Invalid sync interval, auto sync disabled')
|
||||||
@ -178,9 +182,21 @@ export function startAutoSync() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syncTimeout = setTimeout(performAutoBackup, webdavSyncInterval * 60 * 1000)
|
// 用户指定的自动备份时间间隔(毫秒)
|
||||||
|
const requiredInterval = webdavSyncInterval * 60 * 1000
|
||||||
|
|
||||||
console.log(`[AutoSync] Next sync scheduled in ${webdavSyncInterval} minutes`)
|
// 如果存在最后一次同步WebDAV的时间,以它为参考计算下一次同步的时间
|
||||||
|
const timeUntilNextSync = webdavSync?.lastSyncTime
|
||||||
|
? Math.max(1000, webdavSync.lastSyncTime + requiredInterval - Date.now())
|
||||||
|
: requiredInterval
|
||||||
|
|
||||||
|
syncTimeout = setTimeout(performAutoBackup, timeUntilNextSync)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AutoSync] Next sync scheduled in ${Math.floor(timeUntilNextSync / 1000 / 60)} minutes ${Math.floor(
|
||||||
|
(timeUntilNextSync / 1000) % 60
|
||||||
|
)} seconds`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performAutoBackup() {
|
async function performAutoBackup() {
|
||||||
@ -192,7 +208,7 @@ export function startAutoSync() {
|
|||||||
|
|
||||||
isAutoBackupRunning = true
|
isAutoBackupRunning = true
|
||||||
try {
|
try {
|
||||||
console.log('[AutoSync] Performing auto backup...')
|
console.log('[AutoSync] Starting auto backup...')
|
||||||
await backupToWebdav({ showMessage: false })
|
await backupToWebdav({ showMessage: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AutoSync] Auto backup failed:', error)
|
console.error('[AutoSync] Auto backup failed:', error)
|
||||||
|
|||||||
32
src/renderer/src/store/backup.ts
Normal file
32
src/renderer/src/store/backup.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export interface WebDAVSyncState {
|
||||||
|
lastSyncTime: number | null
|
||||||
|
syncing: boolean
|
||||||
|
lastSyncError: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupState {
|
||||||
|
webdavSync: WebDAVSyncState
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: BackupState = {
|
||||||
|
webdavSync: {
|
||||||
|
lastSyncTime: null,
|
||||||
|
syncing: false,
|
||||||
|
lastSyncError: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupSlice = createSlice({
|
||||||
|
name: 'backup',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setWebDAVSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
|
||||||
|
state.webdavSync = { ...state.webdavSync, ...action.payload }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { setWebDAVSyncState } = backupSlice.actions
|
||||||
|
export default backupSlice.reducer
|
||||||
@ -5,6 +5,7 @@ import storage from 'redux-persist/lib/storage'
|
|||||||
|
|
||||||
import agents from './agents'
|
import agents from './agents'
|
||||||
import assistants from './assistants'
|
import assistants from './assistants'
|
||||||
|
import backup from './backup'
|
||||||
import copilot from './copilot'
|
import copilot from './copilot'
|
||||||
import knowledge from './knowledge'
|
import knowledge from './knowledge'
|
||||||
import llm from './llm'
|
import llm from './llm'
|
||||||
@ -21,6 +22,7 @@ import websearch from './websearch'
|
|||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
assistants,
|
assistants,
|
||||||
agents,
|
agents,
|
||||||
|
backup,
|
||||||
paintings,
|
paintings,
|
||||||
llm,
|
llm,
|
||||||
settings,
|
settings,
|
||||||
@ -38,7 +40,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 81,
|
version: 82,
|
||||||
blacklist: ['runtime', 'messages'],
|
blacklist: ['runtime', 'messages'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -772,6 +772,22 @@ const migrateConfig = {
|
|||||||
'81': (state: RootState) => {
|
'81': (state: RootState) => {
|
||||||
addProvider(state, 'copilot')
|
addProvider(state, 'copilot')
|
||||||
return state
|
return state
|
||||||
|
},
|
||||||
|
'82': (state: RootState) => {
|
||||||
|
const runtimeState = state.runtime as any
|
||||||
|
if (runtimeState?.webdavSync) {
|
||||||
|
state.backup = state.backup || {}
|
||||||
|
state.backup = {
|
||||||
|
...state.backup,
|
||||||
|
webdavSync: {
|
||||||
|
lastSyncTime: runtimeState.webdavSync.lastSyncTime || null,
|
||||||
|
syncing: runtimeState.webdavSync.syncing || false,
|
||||||
|
lastSyncError: runtimeState.webdavSync.lastSyncError || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete runtimeState.webdavSync
|
||||||
|
}
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,12 +11,6 @@ export interface UpdateState {
|
|||||||
available: boolean
|
available: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebDAVSyncState {
|
|
||||||
lastSyncTime: number | null
|
|
||||||
syncing: boolean
|
|
||||||
lastSyncError: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuntimeState {
|
export interface RuntimeState {
|
||||||
avatar: string
|
avatar: string
|
||||||
generating: boolean
|
generating: boolean
|
||||||
@ -25,7 +19,6 @@ export interface RuntimeState {
|
|||||||
filesPath: string
|
filesPath: string
|
||||||
resourcesPath: string
|
resourcesPath: string
|
||||||
update: UpdateState
|
update: UpdateState
|
||||||
webdavSync: WebDAVSyncState
|
|
||||||
export: ExportState
|
export: ExportState
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,11 +41,6 @@ const initialState: RuntimeState = {
|
|||||||
downloadProgress: 0,
|
downloadProgress: 0,
|
||||||
available: false
|
available: false
|
||||||
},
|
},
|
||||||
webdavSync: {
|
|
||||||
lastSyncTime: null,
|
|
||||||
syncing: false,
|
|
||||||
lastSyncError: null
|
|
||||||
},
|
|
||||||
export: {
|
export: {
|
||||||
isExporting: false
|
isExporting: false
|
||||||
}
|
}
|
||||||
@ -83,9 +71,6 @@ const runtimeSlice = createSlice({
|
|||||||
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
||||||
state.update = { ...state.update, ...action.payload }
|
state.update = { ...state.update, ...action.payload }
|
||||||
},
|
},
|
||||||
setWebDAVSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
|
|
||||||
state.webdavSync = { ...state.webdavSync, ...action.payload }
|
|
||||||
},
|
|
||||||
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
||||||
state.export = { ...state.export, ...action.payload }
|
state.export = { ...state.export, ...action.payload }
|
||||||
}
|
}
|
||||||
@ -100,7 +85,6 @@ export const {
|
|||||||
setFilesPath,
|
setFilesPath,
|
||||||
setResourcesPath,
|
setResourcesPath,
|
||||||
setUpdateState,
|
setUpdateState,
|
||||||
setWebDAVSyncState,
|
|
||||||
setExportState
|
setExportState
|
||||||
} = runtimeSlice.actions
|
} = runtimeSlice.actions
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user