mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
feat: add max backups for NutStore (#9020)
This commit is contained in:
parent
27c9ceab9f
commit
6b8ba9d273
@ -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} />
|
||||
|
||||
@ -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' }))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user