diff --git a/src/renderer/src/components/LocalBackupManager.tsx b/src/renderer/src/components/LocalBackupManager.tsx index 2b4f949b38..de84b0ea74 100644 --- a/src/renderer/src/components/LocalBackupManager.tsx +++ b/src/renderer/src/components/LocalBackupManager.tsx @@ -1,5 +1,5 @@ import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined } from '@ant-design/icons' -import { restoreFromLocalBackup } from '@renderer/services/BackupService' +import { restoreFromLocal } from '@renderer/services/BackupService' import { formatFileSize } from '@renderer/utils' import { Button, message, Modal, Table, Tooltip } from 'antd' import dayjs from 'dayjs' @@ -147,7 +147,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe onOk: async () => { setRestoring(true) try { - await (restoreMethod || restoreFromLocalBackup)(fileName) + await (restoreMethod || restoreFromLocal)(fileName) message.success(t('settings.data.local.backup.manager.restore.success')) onClose() // Close the modal } catch (error: any) { diff --git a/src/renderer/src/components/LocalBackupModals.tsx b/src/renderer/src/components/LocalBackupModals.tsx index 9f2a700dd2..420c9c2f37 100644 --- a/src/renderer/src/components/LocalBackupModals.tsx +++ b/src/renderer/src/components/LocalBackupModals.tsx @@ -1,4 +1,4 @@ -import { backupToLocalDir } from '@renderer/services/BackupService' +import { backupToLocal } from '@renderer/services/BackupService' import { Button, Input, Modal } from 'antd' import dayjs from 'dayjs' import { useCallback, useState } from 'react' @@ -74,9 +74,9 @@ export function useLocalBackupModal(localBackupDir: string | undefined) { setBackuping(true) try { - await backupToLocalDir({ + await backupToLocal({ showMessage: true, - customFileName + customFileName: customFileName || undefined }) setIsModalVisible(false) } catch (error) { diff --git a/src/renderer/src/init.ts b/src/renderer/src/init.ts index 91e5e54c65..6ce10c16b8 100644 --- a/src/renderer/src/init.ts +++ b/src/renderer/src/init.ts @@ -1,6 +1,6 @@ import KeyvStorage from '@kangfenmao/keyv-storage' -import { startAutoSync, startLocalBackupAutoSync } from './services/BackupService' +import { startAutoSync } from './services/BackupService' import { startNutstoreAutoSync } from './services/NutstoreService' import storeSyncService from './services/StoreSyncService' import store from './store' @@ -14,15 +14,12 @@ function initAutoSync() { setTimeout(() => { const { webdavAutoSync, localBackupAutoSync, s3 } = store.getState().settings const { nutstoreAutoSync } = store.getState().nutstore - if (webdavAutoSync || (s3 && s3.autoSync)) { + if (webdavAutoSync || (s3 && s3.autoSync) || localBackupAutoSync) { startAutoSync() } if (nutstoreAutoSync) { startNutstoreAutoSync() } - if (localBackupAutoSync) { - startLocalBackupAutoSync() - } }, 8000) } diff --git a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx index 077ee96bbf..ef579ec49d 100644 --- a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx @@ -2,9 +2,10 @@ import { DeleteOutlined, FolderOpenOutlined, SaveOutlined, SyncOutlined, Warning import { HStack } from '@renderer/components/Layout' import { LocalBackupManager } from '@renderer/components/LocalBackupManager' import { LocalBackupModal, useLocalBackupModal } from '@renderer/components/LocalBackupModals' +import Selector from '@renderer/components/Selector' import { useTheme } from '@renderer/context/ThemeProvider' import { useSettings } from '@renderer/hooks/useSettings' -import { startLocalBackupAutoSync, stopLocalBackupAutoSync } from '@renderer/services/BackupService' +import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setLocalBackupAutoSync, @@ -14,14 +15,16 @@ import { setLocalBackupSyncInterval as _setLocalBackupSyncInterval } from '@renderer/store/settings' import { AppInfo } from '@renderer/types' -import { Button, Input, Select, Switch, Tooltip } from 'antd' +import { Button, Input, Switch, Tooltip } from 'antd' import dayjs from 'dayjs' -import { FC, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' -const LocalBackupSettings: FC = () => { +const LocalBackupSettings: React.FC = () => { + const dispatch = useAppDispatch() + const { localBackupDir: localBackupDirSetting, localBackupSyncInterval: localBackupSyncIntervalSetting, @@ -42,7 +45,6 @@ const LocalBackupSettings: FC = () => { window.api.getAppInfo().then(setAppInfo) }, []) - const dispatch = useAppDispatch() const { theme } = useTheme() const { t } = useTranslation() @@ -54,10 +56,10 @@ const LocalBackupSettings: FC = () => { dispatch(_setLocalBackupSyncInterval(value)) if (value === 0) { dispatch(setLocalBackupAutoSync(false)) - stopLocalBackupAutoSync() + stopAutoSync('local') } else { dispatch(setLocalBackupAutoSync(true)) - startLocalBackupAutoSync() + startAutoSync(false, 'local') } } @@ -98,14 +100,14 @@ const LocalBackupSettings: FC = () => { await window.api.backup.setLocalBackupDir(value) dispatch(setLocalBackupAutoSync(true)) - startLocalBackupAutoSync(true) + startAutoSync(true, 'local') return } setLocalBackupDir('') dispatch(_setLocalBackupDir('')) dispatch(setLocalBackupAutoSync(false)) - stopLocalBackupAutoSync() + stopAutoSync('local') } const onMaxBackupsChange = (value: number) => { @@ -139,7 +141,7 @@ const LocalBackupSettings: FC = () => { setLocalBackupDir('') dispatch(_setLocalBackupDir('')) dispatch(setLocalBackupAutoSync(false)) - stopLocalBackupAutoSync() + stopAutoSync('local') } const renderSyncStatus = () => { @@ -213,39 +215,43 @@ const LocalBackupSettings: FC = () => { {t('settings.data.local.autoSync')} - + options={[ + { label: t('settings.data.local.autoSync.off'), value: 0 }, + { label: t('settings.data.local.minute_interval', { count: 1 }), value: 1 }, + { label: t('settings.data.local.minute_interval', { count: 5 }), value: 5 }, + { label: t('settings.data.local.minute_interval', { count: 15 }), value: 15 }, + { label: t('settings.data.local.minute_interval', { count: 30 }), value: 30 }, + { label: t('settings.data.local.hour_interval', { count: 1 }), value: 60 }, + { label: t('settings.data.local.hour_interval', { count: 2 }), value: 120 }, + { label: t('settings.data.local.hour_interval', { count: 6 }), value: 360 }, + { label: t('settings.data.local.hour_interval', { count: 12 }), value: 720 }, + { label: t('settings.data.local.hour_interval', { count: 24 }), value: 1440 } + ]} + /> {t('settings.data.local.maxBackups')} - + options={[ + { label: t('settings.data.local.maxBackups.unlimited'), value: 0 }, + { label: '1', value: 1 }, + { label: '3', value: 3 }, + { label: '5', value: 5 }, + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '50', value: 50 } + ]} + /> diff --git a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx index c261c5b736..c24d67cd44 100644 --- a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx @@ -2,6 +2,7 @@ import { FolderOpenOutlined, InfoCircleOutlined, SaveOutlined, SyncOutlined, War import { HStack } from '@renderer/components/Layout' import { S3BackupManager } from '@renderer/components/S3BackupManager' import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals' +import Selector from '@renderer/components/Selector' import { useTheme } from '@renderer/context/ThemeProvider' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useSettings } from '@renderer/hooks/useSettings' @@ -9,7 +10,7 @@ import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setS3Partial } from '@renderer/store/settings' import { S3Config } from '@renderer/types' -import { Button, Input, Select, Switch, Tooltip } from 'antd' +import { Button, Input, Switch, Tooltip } from 'antd' import dayjs from 'dayjs' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -54,9 +55,9 @@ const S3Settings: FC = () => { setSyncInterval(value) dispatch(setS3Partial({ syncInterval: value, autoSync: value !== 0 })) if (value === 0) { - stopAutoSync() + stopAutoSync('s3') } else { - startAutoSync() + startAutoSync(false, 's3') } } @@ -211,39 +212,43 @@ const S3Settings: FC = () => { {t('settings.data.s3.autoSync')} - + options={[ + { label: t('settings.data.s3.autoSync.off'), value: 0 }, + { label: t('settings.data.s3.autoSync.minute', { count: 1 }), value: 1 }, + { label: t('settings.data.s3.autoSync.minute', { count: 5 }), value: 5 }, + { label: t('settings.data.s3.autoSync.minute', { count: 15 }), value: 15 }, + { label: t('settings.data.s3.autoSync.minute', { count: 30 }), value: 30 }, + { label: t('settings.data.s3.autoSync.hour', { count: 1 }), value: 60 }, + { label: t('settings.data.s3.autoSync.hour', { count: 2 }), value: 120 }, + { label: t('settings.data.s3.autoSync.hour', { count: 6 }), value: 360 }, + { label: t('settings.data.s3.autoSync.hour', { count: 12 }), value: 720 }, + { label: t('settings.data.s3.autoSync.hour', { count: 24 }), value: 1440 } + ]} + /> {t('settings.data.s3.maxBackups')} - + options={[ + { label: t('settings.data.s3.maxBackups.unlimited'), value: 0 }, + { label: '1', value: 1 }, + { label: '3', value: 3 }, + { label: '5', value: 5 }, + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '50', value: 50 } + ]} + /> diff --git a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx index 977c5c1329..7f42c9fe30 100644 --- a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx @@ -59,10 +59,10 @@ const WebDavSettings: FC = () => { dispatch(_setWebdavSyncInterval(value)) if (value === 0) { dispatch(setWebdavAutoSync(false)) - stopAutoSync() + stopAutoSync('webdav') } else { dispatch(setWebdavAutoSync(true)) - startAutoSync() + startAutoSync(false, 'webdav') } } diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index bf5fea3c5a..9e594f85c2 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -444,85 +444,144 @@ export async function restoreFromS3(fileName?: string) { }) const data = JSON.parse(restoreData) await handleData(data) - store.dispatch( - setS3SyncState({ - lastSyncTime: Date.now(), - syncing: false, - lastSyncError: null - }) - ) } } -let autoSyncStarted = false -let syncTimeout: NodeJS.Timeout | null = null -let isAutoBackupRunning = false let isManualBackupRunning = false -export function startAutoSync(immediate = false) { - if (autoSyncStarted) { - return - } +// 为每种备份类型维护独立的状态 +let webdavAutoSyncStarted = false +let webdavSyncTimeout: NodeJS.Timeout | null = null +let isWebdavAutoBackupRunning = false - const settings = store.getState().settings - const { webdavAutoSync, webdavHost } = settings - const s3Settings = settings.s3 +let s3AutoSyncStarted = false +let s3SyncTimeout: NodeJS.Timeout | null = null +let isS3AutoBackupRunning = false - const s3AutoSync = s3Settings?.autoSync - const s3Endpoint = s3Settings?.endpoint +let localAutoSyncStarted = false +let localSyncTimeout: NodeJS.Timeout | null = null +let isLocalAutoBackupRunning = false - const localBackupAutoSync = settings.localBackupAutoSync - const localBackupDir = settings.localBackupDir - - // 检查WebDAV或S3自动同步配置 - const hasWebdavConfig = webdavAutoSync && webdavHost - const hasS3Config = s3AutoSync && s3Endpoint - const hasLocalConfig = localBackupAutoSync && localBackupDir - - if (!hasWebdavConfig && !hasS3Config && !hasLocalConfig) { - Logger.log('[AutoSync] Invalid sync settings, auto sync disabled') - return - } - - autoSyncStarted = true - - stopAutoSync() - - scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime') - - /** - * @param type 'immediate' | 'fromLastSyncTime' | 'fromNow' - * 'immediate', first backup right now - * 'fromLastSyncTime', schedule next backup from last sync time - * 'fromNow', schedule next backup from now - */ - function scheduleNextBackup(type: 'immediate' | 'fromLastSyncTime' | 'fromNow' = 'fromLastSyncTime') { - if (syncTimeout) { - clearTimeout(syncTimeout) - syncTimeout = null - } +type BackupType = 'webdav' | 's3' | 'local' +export function startAutoSync(immediate = false, type?: BackupType) { + // 如果没有指定类型,启动所有配置的自动同步 + if (!type) { const settings = store.getState().settings - const _webdavSyncInterval = settings.webdavSyncInterval - const _s3SyncInterval = settings.s3?.syncInterval - const { webdavSync, s3Sync } = store.getState().backup + const { webdavAutoSync, webdavHost, localBackupAutoSync, localBackupDir } = settings + const s3Settings = settings.s3 - // 使用当前激活的同步配置 - const syncInterval = hasWebdavConfig ? _webdavSyncInterval : _s3SyncInterval - const lastSyncTime = hasWebdavConfig ? webdavSync?.lastSyncTime : s3Sync?.lastSyncTime + if (webdavAutoSync && webdavHost) { + startAutoSync(immediate, 'webdav') + } + if (s3Settings?.autoSync && s3Settings?.endpoint) { + startAutoSync(immediate, 's3') + } + if (localBackupAutoSync && localBackupDir) { + startAutoSync(immediate, 'local') + } + return + } - if (!syncInterval || syncInterval <= 0) { - Logger.log('[AutoSync] Invalid sync interval, auto sync disabled') - stopAutoSync() + // 根据类型启动特定的自动同步 + if (type === 'webdav') { + if (webdavAutoSyncStarted) { return } - // 用户指定的自动备份时间间隔(毫秒) - const requiredInterval = syncInterval * 60 * 1000 + const settings = store.getState().settings + const { webdavAutoSync, webdavHost } = settings - let timeUntilNextSync = 1000 //also immediate - switch (type) { - case 'fromLastSyncTime': // 如果存在最后一次同步的时间,以它为参考计算下一次同步的时间 + if (!webdavAutoSync || !webdavHost) { + Logger.log('[WebdavAutoSync] Invalid sync settings, auto sync disabled') + return + } + + webdavAutoSyncStarted = true + stopAutoSync('webdav') + scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime', 'webdav') + } else if (type === 's3') { + if (s3AutoSyncStarted) { + return + } + + const settings = store.getState().settings + const s3Settings = settings.s3 + + if (!s3Settings?.autoSync || !s3Settings?.endpoint) { + Logger.log('[S3AutoSync] Invalid sync settings, auto sync disabled') + return + } + + s3AutoSyncStarted = true + stopAutoSync('s3') + scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime', 's3') + } else if (type === 'local') { + if (localAutoSyncStarted) { + return + } + + const settings = store.getState().settings + const { localBackupAutoSync, localBackupDir } = settings + + if (!localBackupAutoSync || !localBackupDir) { + Logger.log('[LocalAutoSync] Invalid sync settings, auto sync disabled') + return + } + + localAutoSyncStarted = true + stopAutoSync('local') + scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime', 'local') + } + + function scheduleNextBackup(scheduleType: 'immediate' | 'fromLastSyncTime' | 'fromNow', backupType: BackupType) { + let syncInterval: number + let lastSyncTime: number | undefined + let logPrefix: string + + // 根据备份类型获取相应的配置和状态 + const settings = store.getState().settings + const backup = store.getState().backup + + if (backupType === 'webdav') { + if (webdavSyncTimeout) { + clearTimeout(webdavSyncTimeout) + webdavSyncTimeout = null + } + syncInterval = settings.webdavSyncInterval + lastSyncTime = backup.webdavSync?.lastSyncTime || undefined + logPrefix = '[WebdavAutoSync]' + } else if (backupType === 's3') { + if (s3SyncTimeout) { + clearTimeout(s3SyncTimeout) + s3SyncTimeout = null + } + syncInterval = settings.s3?.syncInterval || 0 + lastSyncTime = backup.s3Sync?.lastSyncTime || undefined + logPrefix = '[S3AutoSync]' + } else if (backupType === 'local') { + if (localSyncTimeout) { + clearTimeout(localSyncTimeout) + localSyncTimeout = null + } + syncInterval = settings.localBackupSyncInterval + lastSyncTime = backup.localBackupSync?.lastSyncTime || undefined + logPrefix = '[LocalAutoSync]' + } else { + return + } + + if (!syncInterval || syncInterval <= 0) { + Logger.log(`${logPrefix} Invalid sync interval, auto sync disabled`) + stopAutoSync(backupType) + return + } + + const requiredInterval = syncInterval * 60 * 1000 + let timeUntilNextSync = 1000 + + switch (scheduleType) { + case 'fromLastSyncTime': timeUntilNextSync = Math.max(1000, (lastSyncTime || 0) + requiredInterval - Date.now()) break case 'fromNow': @@ -530,33 +589,64 @@ export function startAutoSync(immediate = false) { break } - syncTimeout = setTimeout(performAutoBackup, timeUntilNextSync) + const timeout = setTimeout(() => performAutoBackup(backupType), timeUntilNextSync) + + // 保存对应类型的 timeout + if (backupType === 'webdav') { + webdavSyncTimeout = timeout + } else if (backupType === 's3') { + s3SyncTimeout = timeout + } else if (backupType === 'local') { + localSyncTimeout = timeout + } - const backupType = hasWebdavConfig ? 'WebDAV' : 'S3' Logger.log( - `[AutoSync] Next ${backupType} sync scheduled in ${Math.floor(timeUntilNextSync / 1000 / 60)} minutes ${Math.floor( + `${logPrefix} Next sync scheduled in ${Math.floor(timeUntilNextSync / 1000 / 60)} minutes ${Math.floor( (timeUntilNextSync / 1000) % 60 )} seconds` ) } - async function performAutoBackup() { - if (isAutoBackupRunning || isManualBackupRunning) { - Logger.log('[AutoSync] Backup already in progress, rescheduling') - scheduleNextBackup() + async function performAutoBackup(backupType: BackupType) { + let isRunning: boolean + let logPrefix: string + + if (backupType === 'webdav') { + isRunning = isWebdavAutoBackupRunning + logPrefix = '[WebdavAutoSync]' + } else if (backupType === 's3') { + isRunning = isS3AutoBackupRunning + logPrefix = '[S3AutoSync]' + } else if (backupType === 'local') { + isRunning = isLocalAutoBackupRunning + logPrefix = '[LocalAutoSync]' + } else { return } - isAutoBackupRunning = true + if (isRunning || isManualBackupRunning) { + Logger.log(`${logPrefix} Backup already in progress, rescheduling`) + scheduleNextBackup('fromNow', backupType) + return + } + + // 设置运行状态 + if (backupType === 'webdav') { + isWebdavAutoBackupRunning = true + } else if (backupType === 's3') { + isS3AutoBackupRunning = true + } else if (backupType === 'local') { + isLocalAutoBackupRunning = true + } + const maxRetries = 4 let retryCount = 0 while (retryCount < maxRetries) { try { - const backupType = hasWebdavConfig ? 'WebDAV' : 'S3' - Logger.log(`[AutoSync] Starting auto ${backupType} backup... (attempt ${retryCount + 1}/${maxRetries})`) + Logger.log(`${logPrefix} Starting auto backup... (attempt ${retryCount + 1}/${maxRetries})`) - if (hasWebdavConfig) { + if (backupType === 'webdav') { await backupToWebdav({ autoBackupProcess: true }) store.dispatch( setWebDAVSyncState({ @@ -565,7 +655,7 @@ export function startAutoSync(immediate = false) { syncing: false }) ) - } else if (hasS3Config) { + } else if (backupType === 's3') { await backupToS3({ autoBackupProcess: true }) store.dispatch( setS3SyncState({ @@ -574,19 +664,34 @@ export function startAutoSync(immediate = false) { syncing: false }) ) + } else if (backupType === 'local') { + await backupToLocal({ autoBackupProcess: true }) + store.dispatch( + setLocalBackupSyncState({ + lastSyncError: null, + lastSyncTime: Date.now(), + syncing: false + }) + ) } - isAutoBackupRunning = false - scheduleNextBackup() + // 重置运行状态 + if (backupType === 'webdav') { + isWebdavAutoBackupRunning = false + } else if (backupType === 's3') { + isS3AutoBackupRunning = false + } else if (backupType === 'local') { + isLocalAutoBackupRunning = false + } + scheduleNextBackup('fromNow', backupType) break } catch (error: any) { retryCount++ if (retryCount === maxRetries) { - const backupType = hasWebdavConfig ? 'WebDAV' : 'S3' - Logger.error(`[AutoSync] Auto ${backupType} backup failed after all retries:`, error) + Logger.error(`${logPrefix} Auto backup failed after all retries:`, error) - if (hasWebdavConfig) { + if (backupType === 'webdav') { store.dispatch( setWebDAVSyncState({ lastSyncError: 'Auto backup failed', @@ -594,7 +699,7 @@ export function startAutoSync(immediate = false) { syncing: false }) ) - } else if (hasS3Config) { + } else if (backupType === 's3') { store.dispatch( setS3SyncState({ lastSyncError: 'Auto backup failed', @@ -602,26 +707,49 @@ export function startAutoSync(immediate = false) { syncing: false }) ) + } else if (backupType === 'local') { + store.dispatch( + setLocalBackupSyncState({ + lastSyncError: 'Auto backup failed', + lastSyncTime: Date.now(), + syncing: false + }) + ) } - //only show 1 time error modal, and autoback stopped until user click ok await window.modal.error({ title: i18n.t('message.backup.failed'), - content: `[${backupType} Auto Backup] ${new Date().toLocaleString()} ` + error.message + content: `${logPrefix} ${new Date().toLocaleString()} ` + error.message }) - scheduleNextBackup('fromNow') - isAutoBackupRunning = false + scheduleNextBackup('fromNow', backupType) + + // 重置运行状态 + if (backupType === 'webdav') { + isWebdavAutoBackupRunning = false + } else if (backupType === 's3') { + isS3AutoBackupRunning = false + } else if (backupType === 'local') { + isLocalAutoBackupRunning = false + } } else { - //Exponential Backoff with Base 2: 7s、17s、37s const backoffDelay = Math.pow(2, retryCount - 1) * 10000 - 3000 - Logger.log(`[AutoSync] Failed, retry ${retryCount}/${maxRetries} after ${backoffDelay / 1000}s`) + Logger.log(`${logPrefix} Failed, retry ${retryCount}/${maxRetries} after ${backoffDelay / 1000}s`) await new Promise((resolve) => setTimeout(resolve, backoffDelay)) - //in case auto backup is stopped by user - if (!isAutoBackupRunning) { - Logger.log('[AutoSync] retry cancelled by user, exit') + // 检查是否被用户停止 + let currentRunning: boolean + if (backupType === 'webdav') { + currentRunning = isWebdavAutoBackupRunning + } else if (backupType === 's3') { + currentRunning = isS3AutoBackupRunning + } else { + currentRunning = isLocalAutoBackupRunning + } + + if (!currentRunning) { + Logger.log(`${logPrefix} retry cancelled by user, exit`) break } } @@ -630,14 +758,40 @@ export function startAutoSync(immediate = false) { } } -export function stopAutoSync() { - if (syncTimeout) { - Logger.log('[AutoSync] Stopping auto sync') - clearTimeout(syncTimeout) - syncTimeout = null +export function stopAutoSync(type?: BackupType) { + // 如果没有指定类型,停止所有自动同步 + if (!type) { + stopAutoSync('webdav') + stopAutoSync('s3') + stopAutoSync('local') + return + } + + if (type === 'webdav') { + if (webdavSyncTimeout) { + Logger.log('[WebdavAutoSync] Stopping auto sync') + clearTimeout(webdavSyncTimeout) + webdavSyncTimeout = null + } + isWebdavAutoBackupRunning = false + webdavAutoSyncStarted = false + } else if (type === 's3') { + if (s3SyncTimeout) { + Logger.log('[S3AutoSync] Stopping auto sync') + clearTimeout(s3SyncTimeout) + s3SyncTimeout = null + } + isS3AutoBackupRunning = false + s3AutoSyncStarted = false + } else if (type === 'local') { + if (localSyncTimeout) { + Logger.log('[LocalAutoSync] Stopping auto sync') + clearTimeout(localSyncTimeout) + localSyncTimeout = null + } + isLocalAutoBackupRunning = false + localAutoSyncStarted = false } - isAutoBackupRunning = false - autoSyncStarted = false } export async function getBackupData() { @@ -727,7 +881,7 @@ async function clearDatabase() { /** * Backup to local directory */ -export async function backupToLocalDir({ +export async function backupToLocal({ showMessage = false, customFileName = '', autoBackupProcess = false @@ -812,10 +966,31 @@ export async function backupToLocalDir({ Logger.error('[LocalBackup] Failed to clean up old backups:', error) } } + } else { + if (autoBackupProcess) { + throw new Error(i18n.t('message.backup.failed')) + } + + store.dispatch( + setLocalBackupSyncState({ + lastSyncError: 'Backup failed' + }) + ) + + if (showMessage) { + window.modal.error({ + title: i18n.t('message.backup.failed'), + content: 'Backup failed' + }) + } } return result } catch (error: any) { + if (autoBackupProcess) { + throw error + } + Logger.error('[LocalBackup] Backup failed:', error) store.dispatch( @@ -845,157 +1020,18 @@ export async function backupToLocalDir({ } } -export async function restoreFromLocalBackup(fileName: string) { +export async function restoreFromLocal(fileName: string) { + const { localBackupDir } = store.getState().settings + try { - const { localBackupDir } = store.getState().settings - await window.api.backup.restoreFromLocalBackup(fileName, localBackupDir) + const restoreData = await window.api.backup.restoreFromLocalBackup(fileName, localBackupDir) + const data = JSON.parse(restoreData) + await handleData(data) + return true } catch (error) { Logger.error('[LocalBackup] Restore failed:', error) + window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' }) throw error } } - -// Local backup auto sync -let localBackupAutoSyncStarted = false -let localBackupSyncTimeout: NodeJS.Timeout | null = null -let isLocalBackupAutoRunning = false - -export function startLocalBackupAutoSync(immediate = false) { - if (localBackupAutoSyncStarted) { - return - } - - const { localBackupAutoSync, localBackupDir } = store.getState().settings - - if (!localBackupAutoSync || !localBackupDir) { - Logger.log('[LocalBackupAutoSync] Invalid sync settings, auto sync disabled') - return - } - - localBackupAutoSyncStarted = true - - stopLocalBackupAutoSync() - - scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime') - - /** - * @param type 'immediate' | 'fromLastSyncTime' | 'fromNow' - * 'immediate', first backup right now - * 'fromLastSyncTime', schedule next backup from last sync time - * 'fromNow', schedule next backup from now - */ - function scheduleNextBackup(type: 'immediate' | 'fromLastSyncTime' | 'fromNow' = 'fromLastSyncTime') { - if (localBackupSyncTimeout) { - clearTimeout(localBackupSyncTimeout) - localBackupSyncTimeout = null - } - - const { localBackupSyncInterval } = store.getState().settings - const { localBackupSync } = store.getState().backup - - if (localBackupSyncInterval <= 0) { - Logger.log('[LocalBackupAutoSync] Invalid sync interval, auto sync disabled') - stopLocalBackupAutoSync() - return - } - - // User specified auto backup interval (milliseconds) - const requiredInterval = localBackupSyncInterval * 60 * 1000 - - let timeUntilNextSync = 1000 // immediate by default - switch (type) { - case 'fromLastSyncTime': // If last sync time exists, use it as reference - timeUntilNextSync = Math.max(1000, (localBackupSync?.lastSyncTime || 0) + requiredInterval - Date.now()) - break - case 'fromNow': - timeUntilNextSync = requiredInterval - break - } - - localBackupSyncTimeout = setTimeout(performAutoBackup, timeUntilNextSync) - - Logger.log( - `[LocalBackupAutoSync] Next sync scheduled in ${Math.floor(timeUntilNextSync / 1000 / 60)} minutes ${Math.floor( - (timeUntilNextSync / 1000) % 60 - )} seconds` - ) - } - - async function performAutoBackup() { - if (isLocalBackupAutoRunning || isManualBackupRunning) { - Logger.log('[LocalBackupAutoSync] Backup already in progress, rescheduling') - scheduleNextBackup() - return - } - - isLocalBackupAutoRunning = true - const maxRetries = 4 - let retryCount = 0 - - while (retryCount < maxRetries) { - try { - Logger.log(`[LocalBackupAutoSync] Starting auto backup... (attempt ${retryCount + 1}/${maxRetries})`) - - await backupToLocalDir({ autoBackupProcess: true }) - - store.dispatch( - setLocalBackupSyncState({ - lastSyncError: null, - lastSyncTime: Date.now(), - syncing: false - }) - ) - - isLocalBackupAutoRunning = false - scheduleNextBackup() - - break - } catch (error: any) { - retryCount++ - if (retryCount === maxRetries) { - Logger.error('[LocalBackupAutoSync] Auto backup failed after all retries:', error) - - store.dispatch( - setLocalBackupSyncState({ - lastSyncError: 'Auto backup failed', - lastSyncTime: Date.now(), - syncing: false - }) - ) - - // Only show error modal once and wait for user acknowledgment - await window.modal.error({ - title: i18n.t('message.backup.failed'), - content: `[Local Backup Auto Backup] ${new Date().toLocaleString()} ` + error.message - }) - - scheduleNextBackup('fromNow') - isLocalBackupAutoRunning = false - } else { - // Exponential Backoff with Base 2: 7s, 17s, 37s - const backoffDelay = Math.pow(2, retryCount - 1) * 10000 - 3000 - Logger.log(`[LocalBackupAutoSync] Failed, retry ${retryCount}/${maxRetries} after ${backoffDelay / 1000}s`) - - await new Promise((resolve) => setTimeout(resolve, backoffDelay)) - - // Check if auto backup was stopped by user - if (!isLocalBackupAutoRunning) { - Logger.log('[LocalBackupAutoSync] retry cancelled by user, exit') - break - } - } - } - } - } -} - -export function stopLocalBackupAutoSync() { - if (localBackupSyncTimeout) { - Logger.log('[LocalBackupAutoSync] Stopping auto sync') - clearTimeout(localBackupSyncTimeout) - localBackupSyncTimeout = null - } - isLocalBackupAutoRunning = false - localBackupAutoSyncStarted = false -} diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 615103a11d..25b822cd21 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -248,7 +248,7 @@ export async function getMessageTitle(message: Message, length = 30): Promise