fix: replace Select component with custom Selector in LocalBackupSetting (#8055)

* fix: replace Select component with custom Selector in LocalBackupSettings

- Updated LocalBackupSettings to use a custom Selector component for better styling and functionality.
- Enhanced the options for auto-sync interval and max backups with improved structure and internationalization support.
- Added success notification handling in restoreFromLocalBackup function in BackupService for better user feedback.

* refactor: streamline backup service and settings management

- Removed local backup auto-sync functionality and integrated its logic into the general auto-sync mechanism.
- Updated backup service methods to handle specific backup types (webdav, s3, local) more efficiently.
- Renamed backup functions for consistency and clarity.
- Enhanced local backup management in settings to utilize the new auto-sync structure.
- Improved error handling and logging for backup operations.

* refactor: replace Select component with custom Selector in S3Settings

- Updated S3Settings to utilize a custom Selector component for improved styling and functionality.
- Enhanced the options for auto-sync interval and max backups with better structure and internationalization support.
- Removed deprecated Select component to streamline the settings interface.
This commit is contained in:
beyondkmp 2025-07-13 07:09:23 +08:00 committed by GitHub
parent 16e65d39be
commit a3d6f32202
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 363 additions and 319 deletions

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
}

View File

@ -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 = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.local.autoSync')}</SettingRowTitle>
<Select
<Selector
size={14}
value={syncInterval}
onChange={(value) => onSyncIntervalChange(value as number)}
onChange={onSyncIntervalChange}
disabled={!localBackupDir}
style={{ minWidth: 120 }}>
<Select.Option value={0}>{t('settings.data.local.autoSync.off')}</Select.Option>
<Select.Option value={1}>{t('settings.data.local.minute_interval', { count: 1 })}</Select.Option>
<Select.Option value={5}>{t('settings.data.local.minute_interval', { count: 5 })}</Select.Option>
<Select.Option value={15}>{t('settings.data.local.minute_interval', { count: 15 })}</Select.Option>
<Select.Option value={30}>{t('settings.data.local.minute_interval', { count: 30 })}</Select.Option>
<Select.Option value={60}>{t('settings.data.local.hour_interval', { count: 1 })}</Select.Option>
<Select.Option value={120}>{t('settings.data.local.hour_interval', { count: 2 })}</Select.Option>
<Select.Option value={360}>{t('settings.data.local.hour_interval', { count: 6 })}</Select.Option>
<Select.Option value={720}>{t('settings.data.local.hour_interval', { count: 12 })}</Select.Option>
<Select.Option value={1440}>{t('settings.data.local.hour_interval', { count: 24 })}</Select.Option>
</Select>
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 }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.local.maxBackups')}</SettingRowTitle>
<Select
<Selector
size={14}
value={maxBackups}
onChange={(value) => onMaxBackupsChange(value as number)}
onChange={onMaxBackupsChange}
disabled={!localBackupDir}
style={{ minWidth: 120 }}>
<Select.Option value={0}>{t('settings.data.local.maxBackups.unlimited')}</Select.Option>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={3}>3</Select.Option>
<Select.Option value={5}>5</Select.Option>
<Select.Option value={10}>10</Select.Option>
<Select.Option value={20}>20</Select.Option>
<Select.Option value={50}>50</Select.Option>
</Select>
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 }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>

View File

@ -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 = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.s3.autoSync')}</SettingRowTitle>
<Select
<Selector
size={14}
value={syncInterval}
onChange={onSyncIntervalChange}
disabled={!endpoint || !accessKeyId || !secretAccessKey}
style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.s3.autoSync.off')}</Select.Option>
<Select.Option value={1}>{t('settings.data.s3.autoSync.minute', { count: 1 })}</Select.Option>
<Select.Option value={5}>{t('settings.data.s3.autoSync.minute', { count: 5 })}</Select.Option>
<Select.Option value={15}>{t('settings.data.s3.autoSync.minute', { count: 15 })}</Select.Option>
<Select.Option value={30}>{t('settings.data.s3.autoSync.minute', { count: 30 })}</Select.Option>
<Select.Option value={60}>{t('settings.data.s3.autoSync.hour', { count: 1 })}</Select.Option>
<Select.Option value={120}>{t('settings.data.s3.autoSync.hour', { count: 2 })}</Select.Option>
<Select.Option value={360}>{t('settings.data.s3.autoSync.hour', { count: 6 })}</Select.Option>
<Select.Option value={720}>{t('settings.data.s3.autoSync.hour', { count: 12 })}</Select.Option>
<Select.Option value={1440}>{t('settings.data.s3.autoSync.hour', { count: 24 })}</Select.Option>
</Select>
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 }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.s3.maxBackups')}</SettingRowTitle>
<Select
<Selector
size={14}
value={maxBackups}
onChange={onMaxBackupsChange}
disabled={!endpoint || !accessKeyId || !secretAccessKey}
style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.s3.maxBackups.unlimited')}</Select.Option>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={3}>3</Select.Option>
<Select.Option value={5}>5</Select.Option>
<Select.Option value={10}>10</Select.Option>
<Select.Option value={20}>20</Select.Option>
<Select.Option value={50}>50</Select.Option>
</Select>
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 }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>

View File

@ -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')
}
}

View File

@ -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
}

View File

@ -248,7 +248,7 @@ export async function getMessageTitle(message: Message, length = 30): Promise<st
export function checkRateLimit(assistant: Assistant): boolean {
const provider = getAssistantProvider(assistant)
if (!provider.rateLimit) {
if (!provider?.rateLimit) {
return false
}