mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 13:59:28 +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 { 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} />
|
||||||
|
|||||||
@ -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' }))
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user