feat: add max backups for NutStore (#9020)

This commit is contained in:
George·Dong 2025-08-10 15:25:49 +08:00 committed by GitHub
parent 27c9ceab9f
commit 6b8ba9d273
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 9 deletions

View File

@ -17,6 +17,7 @@ import {
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
setNutstoreAutoSync, setNutstoreAutoSync,
setNutstoreMaxBackups,
setNutstorePath, setNutstorePath,
setNutstoreSkipBackupFile, setNutstoreSkipBackupFile,
setNutstoreSyncInterval, setNutstoreSyncInterval,
@ -41,7 +42,8 @@ const NutstoreSettings: FC = () => {
nutstoreSyncInterval, nutstoreSyncInterval,
nutstoreAutoSync, nutstoreAutoSync,
nutstoreSyncState, nutstoreSyncState,
nutstoreSkipBackupFile nutstoreSkipBackupFile,
nutstoreMaxBackups
} = useAppSelector((state) => state.nutstore) } = useAppSelector((state) => state.nutstore)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -143,6 +145,10 @@ const NutstoreSettings: FC = () => {
dispatch(setNutstoreSkipBackupFile(value)) dispatch(setNutstoreSkipBackupFile(value))
} }
const onMaxBackupsChange = (value: number) => {
dispatch(setNutstoreMaxBackups(value))
}
const handleClickPathChange = async () => { const handleClickPathChange = async () => {
if (!nutstoreToken) { if (!nutstoreToken) {
return return
@ -308,6 +314,25 @@ const NutstoreSettings: FC = () => {
</> </>
)} )}
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
<Selector
size={14}
value={nutstoreMaxBackups}
onChange={onMaxBackupsChange}
disabled={!nutstoreToken}
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> <SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={nutSkipBackupFile} onChange={onSkipBackupFilesChange} /> <Switch checked={nutSkipBackupFile} onChange={onSkipBackupFilesChange} />

View File

@ -63,6 +63,50 @@ let syncTimeout: NodeJS.Timeout | null = null
let isAutoBackupRunning = false let isAutoBackupRunning = false
let isManualBackupRunning = false let isManualBackupRunning = false
async function cleanupOldBackups(webdavConfig: WebDavConfig, maxBackups: number): Promise<void> {
if (maxBackups <= 0) {
logger.debug('[cleanupOldBackups] Skip cleanup: maxBackups <= 0')
return
}
try {
const files = await window.api.backup.listWebdavFiles(webdavConfig)
if (!files || !Array.isArray(files)) {
logger.warn('[cleanupOldBackups] Failed to list nutstore directory contents')
return
}
const backupFiles = files
.filter((file) => file.fileName.startsWith('cherry-studio') && file.fileName.endsWith('.zip'))
.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
if (backupFiles.length < maxBackups) {
logger.info(`[cleanupOldBackups] No cleanup needed: ${backupFiles.length}/${maxBackups} backups`)
return
}
const filesToDelete = backupFiles.slice(maxBackups - 1)
logger.info(`[cleanupOldBackups] Deleting ${filesToDelete.length} old backup files`)
let deletedCount = 0
for (const file of filesToDelete) {
try {
await window.api.backup.deleteWebdavFile(file.fileName, webdavConfig)
deletedCount++
} catch (error) {
logger.error(`[cleanupOldBackups] Failed to delete ${file.basename}:`, error as Error)
}
}
if (deletedCount > 0) {
logger.info(`[cleanupOldBackups] Successfully deleted ${deletedCount} old backups`)
}
} catch (error) {
logger.error('[cleanupOldBackups] Error during cleanup:', error as Error)
}
}
export async function backupToNutstore({ export async function backupToNutstore({
showMessage = false, showMessage = false,
customFileName = '' customFileName = ''
@ -101,7 +145,12 @@ export async function backupToNutstore({
const backupData = await getBackupData() const backupData = await getBackupData()
const skipBackupFile = store.getState().nutstore.nutstoreSkipBackupFile const skipBackupFile = store.getState().nutstore.nutstoreSkipBackupFile
const maxBackups = store.getState().nutstore.nutstoreMaxBackups
try { try {
// 先清理旧备份
await cleanupOldBackups(config, maxBackups)
const isSuccess = await window.api.backup.backupToWebdav(backupData, { const isSuccess = await window.api.backup.backupToWebdav(backupData, {
...config, ...config,
fileName: finalFileName, fileName: finalFileName,
@ -109,11 +158,7 @@ export async function backupToNutstore({
}) })
if (isSuccess) { if (isSuccess) {
store.dispatch( store.dispatch(setNutstoreSyncState({ lastSyncError: null }))
setNutstoreSyncState({
lastSyncError: null
})
)
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
} else { } else {
store.dispatch(setNutstoreSyncState({ lastSyncError: 'Backup failed' })) store.dispatch(setNutstoreSyncState({ lastSyncError: 'Backup failed' }))

View File

@ -2080,11 +2080,15 @@ const migrateConfig = {
return state return state
} }
}, },
'130': (state: RootState) => { '130': (state: RootState) => {
try { try {
if (state.settings && state.settings.openAI && !state.settings.openAI.verbosity) { if (state.settings && state.settings.openAI && !state.settings.openAI.verbosity) {
state.settings.openAI.verbosity = 'medium' state.settings.openAI.verbosity = 'medium'
} }
// 为 nutstore 添加备份数量限制的默认值
if (state.nutstore && state.nutstore.nutstoreMaxBackups === undefined) {
state.nutstore.nutstoreMaxBackups = 0
}
return state return state
} catch (error) { } catch (error) {
logger.error('migrate 130 error', error as Error) logger.error('migrate 130 error', error as Error)

View File

@ -11,6 +11,7 @@ export interface NutstoreState {
nutstoreSyncInterval: number nutstoreSyncInterval: number
nutstoreSyncState: NutstoreSyncState nutstoreSyncState: NutstoreSyncState
nutstoreSkipBackupFile: boolean nutstoreSkipBackupFile: boolean
nutstoreMaxBackups: number
} }
const initialState: NutstoreState = { const initialState: NutstoreState = {
@ -23,7 +24,8 @@ const initialState: NutstoreState = {
syncing: false, syncing: false,
lastSyncError: null lastSyncError: null
}, },
nutstoreSkipBackupFile: false nutstoreSkipBackupFile: false,
nutstoreMaxBackups: 0
} }
const nutstoreSlice = createSlice({ const nutstoreSlice = createSlice({
@ -47,6 +49,9 @@ const nutstoreSlice = createSlice({
}, },
setNutstoreSkipBackupFile: (state, action: PayloadAction<boolean>) => { setNutstoreSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.nutstoreSkipBackupFile = action.payload state.nutstoreSkipBackupFile = action.payload
},
setNutstoreMaxBackups: (state, action: PayloadAction<number>) => {
state.nutstoreMaxBackups = action.payload
} }
} }
}) })
@ -57,7 +62,8 @@ export const {
setNutstoreAutoSync, setNutstoreAutoSync,
setNutstoreSyncInterval, setNutstoreSyncInterval,
setNutstoreSyncState, setNutstoreSyncState,
setNutstoreSkipBackupFile setNutstoreSkipBackupFile,
setNutstoreMaxBackups
} = nutstoreSlice.actions } = nutstoreSlice.actions
export default nutstoreSlice.reducer export default nutstoreSlice.reducer