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 {
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 = () => {
</>
)}
<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>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={nutSkipBackupFile} onChange={onSkipBackupFilesChange} />

View File

@ -63,6 +63,50 @@ let syncTimeout: NodeJS.Timeout | null = null
let isAutoBackupRunning = 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({
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' }))

View File

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

View File

@ -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<boolean>) => {
state.nutstoreSkipBackupFile = action.payload
},
setNutstoreMaxBackups: (state, action: PayloadAction<number>) => {
state.nutstoreMaxBackups = action.payload
}
}
})
@ -57,7 +62,8 @@ export const {
setNutstoreAutoSync,
setNutstoreSyncInterval,
setNutstoreSyncState,
setNutstoreSkipBackupFile
setNutstoreSkipBackupFile,
setNutstoreMaxBackups
} = nutstoreSlice.actions
export default nutstoreSlice.reducer