diff --git a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx index c5ac8ccd45..13087359e3 100644 --- a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx @@ -17,6 +17,7 @@ import { import { useAppDispatch, useAppSelector } from '@renderer/store' import { setNutstoreAutoSync, + setNutstoreMaxBackups, setNutstorePath, setNutstoreSkipBackupFile, setNutstoreSyncInterval, @@ -41,7 +42,8 @@ const NutstoreSettings: FC = () => { nutstoreSyncInterval, nutstoreAutoSync, nutstoreSyncState, - nutstoreSkipBackupFile + nutstoreSkipBackupFile, + nutstoreMaxBackups } = useAppSelector((state) => state.nutstore) const dispatch = useAppDispatch() @@ -143,6 +145,10 @@ const NutstoreSettings: FC = () => { dispatch(setNutstoreSkipBackupFile(value)) } + const onMaxBackupsChange = (value: number) => { + dispatch(setNutstoreMaxBackups(value)) + } + const handleClickPathChange = async () => { if (!nutstoreToken) { return @@ -308,6 +314,25 @@ const NutstoreSettings: FC = () => { )} + + {t('settings.data.webdav.maxBackups')} + + + {t('settings.data.backup.skip_file_data_title')} diff --git a/src/renderer/src/services/NutstoreService.ts b/src/renderer/src/services/NutstoreService.ts index c613502646..40c27a2932 100644 --- a/src/renderer/src/services/NutstoreService.ts +++ b/src/renderer/src/services/NutstoreService.ts @@ -63,6 +63,50 @@ let syncTimeout: NodeJS.Timeout | null = null let isAutoBackupRunning = false let isManualBackupRunning = false +async function cleanupOldBackups(webdavConfig: WebDavConfig, maxBackups: number): Promise { + 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({ showMessage = false, customFileName = '' @@ -101,7 +145,12 @@ export async function backupToNutstore({ const backupData = await getBackupData() const skipBackupFile = store.getState().nutstore.nutstoreSkipBackupFile + const maxBackups = store.getState().nutstore.nutstoreMaxBackups + try { + // 先清理旧备份 + await cleanupOldBackups(config, maxBackups) + const isSuccess = await window.api.backup.backupToWebdav(backupData, { ...config, fileName: finalFileName, @@ -109,11 +158,7 @@ export async function backupToNutstore({ }) if (isSuccess) { - store.dispatch( - setNutstoreSyncState({ - lastSyncError: null - }) - ) + store.dispatch(setNutstoreSyncState({ lastSyncError: null })) showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) } else { store.dispatch(setNutstoreSyncState({ lastSyncError: 'Backup failed' })) diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index f5613a52a2..6e4da0f1de 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2080,11 +2080,15 @@ const migrateConfig = { return state } }, - '130': (state: RootState) => { + '130': (state: RootState) => { try { if (state.settings && state.settings.openAI && !state.settings.openAI.verbosity) { state.settings.openAI.verbosity = 'medium' } + // 为 nutstore 添加备份数量限制的默认值 + if (state.nutstore && state.nutstore.nutstoreMaxBackups === undefined) { + state.nutstore.nutstoreMaxBackups = 0 + } return state } catch (error) { logger.error('migrate 130 error', error as Error) diff --git a/src/renderer/src/store/nutstore.ts b/src/renderer/src/store/nutstore.ts index cd4721b6df..e2a05f021e 100644 --- a/src/renderer/src/store/nutstore.ts +++ b/src/renderer/src/store/nutstore.ts @@ -11,6 +11,7 @@ export interface NutstoreState { nutstoreSyncInterval: number nutstoreSyncState: NutstoreSyncState nutstoreSkipBackupFile: boolean + nutstoreMaxBackups: number } const initialState: NutstoreState = { @@ -23,7 +24,8 @@ const initialState: NutstoreState = { syncing: false, lastSyncError: null }, - nutstoreSkipBackupFile: false + nutstoreSkipBackupFile: false, + nutstoreMaxBackups: 0 } const nutstoreSlice = createSlice({ @@ -47,6 +49,9 @@ const nutstoreSlice = createSlice({ }, setNutstoreSkipBackupFile: (state, action: PayloadAction) => { state.nutstoreSkipBackupFile = action.payload + }, + setNutstoreMaxBackups: (state, action: PayloadAction) => { + state.nutstoreMaxBackups = action.payload } } }) @@ -57,7 +62,8 @@ export const { setNutstoreAutoSync, setNutstoreSyncInterval, setNutstoreSyncState, - setNutstoreSkipBackupFile + setNutstoreSkipBackupFile, + setNutstoreMaxBackups } = nutstoreSlice.actions export default nutstoreSlice.reducer