mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 19:30:04 +08:00
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:
parent
16e65d39be
commit
a3d6f32202
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user