From 28c52317418fbc8e1be7bad5664becf85632b356 Mon Sep 17 00:00:00 2001 From: one Date: Fri, 21 Mar 2025 11:18:38 +0800 Subject: [PATCH] feat: make webdav state persistent, improve webdav autosync (#3690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 亢奋猫 --- src/main/services/BackupManager.ts | 4 +- .../settings/DataSettings/WebDavSettings.tsx | 17 ++++--- src/renderer/src/services/BackupService.ts | 46 +++++++++++++------ src/renderer/src/store/backup.ts | 32 +++++++++++++ src/renderer/src/store/index.ts | 4 +- src/renderer/src/store/migrate.ts | 16 +++++++ src/renderer/src/store/runtime.ts | 16 ------- 7 files changed, 92 insertions(+), 43 deletions(-) create mode 100644 src/renderer/src/store/backup.ts diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index 2d587776..92b5ef65 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -119,10 +119,10 @@ class BackupManager { await fs.remove(this.tempDir) onProgress({ stage: 'completed', progress: 100, total: 100 }) - Logger.log('Backup completed successfully') + Logger.log('[BackupManager] Backup completed successfully') return backupedFilePath } catch (error) { - Logger.error('Backup failed:', error) + Logger.error('[BackupManager] Backup failed:', error) throw error } } diff --git a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx index 913af6f1..e0e7487b 100644 --- a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx @@ -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 { useTheme } from '@renderer/context/ThemeProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService' -import { useAppDispatch } from '@renderer/store' +import { useAppDispatch, useAppSelector } from '@renderer/store' import { setWebdavAutoSync, setWebdavHost as _setWebdavHost, @@ -56,7 +55,7 @@ const WebDavSettings: FC = () => { const { t } = useTranslation() - const { webdavSync } = useRuntime() + const { webdavSync } = useAppSelector((state) => state.backup) // 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path @@ -82,16 +81,16 @@ const WebDavSettings: FC = () => { return ( {webdavSync.syncing && } + {!webdavSync.syncing && webdavSync.lastSyncError && ( + + + + )} {webdavSync.lastSyncTime && ( {t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')} )} - {webdavSync.lastSyncError && ( - - {t('settings.data.webdav.syncError')}: {webdavSync.lastSyncError} - - )} ) } diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index 54d85e13..e78e9e38 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -1,7 +1,7 @@ import db from '@renderer/databases' import i18n from '@renderer/i18n' import store from '@renderer/store' -import { setWebDAVSyncState } from '@renderer/store/runtime' +import { setWebDAVSyncState } from '@renderer/store/backup' import dayjs from 'dayjs' import Logger from 'electron-log' @@ -95,25 +95,28 @@ export async function backupToWebdav({ if (success) { store.dispatch( setWebDAVSyncState({ - lastSyncTime: Date.now(), lastSyncError: null }) ) showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) } else { 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) { store.dispatch(setWebDAVSyncState({ lastSyncError: error.message })) - console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error) - showMessage && - window.modal.error({ - title: i18n.t('message.backup.failed'), - content: error.message - }) + console.error('[Backup] backupToWebdav: Error uploading file to WebDAV:', error) + window.modal.error({ + title: i18n.t('message.backup.failed'), + content: error.message + }) } finally { - store.dispatch(setWebDAVSyncState({ syncing: false })) + store.dispatch( + setWebDAVSyncState({ + lastSyncTime: Date.now(), + syncing: false + }) + ) isManualBackupRunning = false } } @@ -126,7 +129,7 @@ export async function restoreFromWebdav(fileName?: string) { try { data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath, fileName }) } 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({ title: i18n.t('message.restore.failed'), content: error.message @@ -136,7 +139,7 @@ export async function restoreFromWebdav(fileName?: string) { try { await handleData(JSON.parse(data)) } 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' }) } } @@ -171,6 +174,7 @@ export function startAutoSync() { } const { webdavSyncInterval } = store.getState().settings + const { webdavSync } = store.getState().backup if (webdavSyncInterval <= 0) { console.log('[AutoSync] Invalid sync interval, auto sync disabled') @@ -178,9 +182,21 @@ export function startAutoSync() { 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() { @@ -192,7 +208,7 @@ export function startAutoSync() { isAutoBackupRunning = true try { - console.log('[AutoSync] Performing auto backup...') + console.log('[AutoSync] Starting auto backup...') await backupToWebdav({ showMessage: false }) } catch (error) { console.error('[AutoSync] Auto backup failed:', error) diff --git a/src/renderer/src/store/backup.ts b/src/renderer/src/store/backup.ts new file mode 100644 index 00000000..a8b7d342 --- /dev/null +++ b/src/renderer/src/store/backup.ts @@ -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>) => { + state.webdavSync = { ...state.webdavSync, ...action.payload } + } + } +}) + +export const { setWebDAVSyncState } = backupSlice.actions +export default backupSlice.reducer diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 0f4b5a42..9eb96906 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -5,6 +5,7 @@ import storage from 'redux-persist/lib/storage' import agents from './agents' import assistants from './assistants' +import backup from './backup' import copilot from './copilot' import knowledge from './knowledge' import llm from './llm' @@ -21,6 +22,7 @@ import websearch from './websearch' const rootReducer = combineReducers({ assistants, agents, + backup, paintings, llm, settings, @@ -38,7 +40,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 81, + version: 82, blacklist: ['runtime', 'messages'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index edf5f869..ccf55bca 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -772,6 +772,22 @@ const migrateConfig = { '81': (state: RootState) => { addProvider(state, 'copilot') 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 } } diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index e7962ec3..727ee182 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -11,12 +11,6 @@ export interface UpdateState { available: boolean } -export interface WebDAVSyncState { - lastSyncTime: number | null - syncing: boolean - lastSyncError: string | null -} - export interface RuntimeState { avatar: string generating: boolean @@ -25,7 +19,6 @@ export interface RuntimeState { filesPath: string resourcesPath: string update: UpdateState - webdavSync: WebDAVSyncState export: ExportState } @@ -48,11 +41,6 @@ const initialState: RuntimeState = { downloadProgress: 0, available: false }, - webdavSync: { - lastSyncTime: null, - syncing: false, - lastSyncError: null - }, export: { isExporting: false } @@ -83,9 +71,6 @@ const runtimeSlice = createSlice({ setUpdateState: (state, action: PayloadAction>) => { state.update = { ...state.update, ...action.payload } }, - setWebDAVSyncState: (state, action: PayloadAction>) => { - state.webdavSync = { ...state.webdavSync, ...action.payload } - }, setExportState: (state, action: PayloadAction>) => { state.export = { ...state.export, ...action.payload } } @@ -100,7 +85,6 @@ export const { setFilesPath, setResourcesPath, setUpdateState, - setWebDAVSyncState, setExportState } = runtimeSlice.actions