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