feat: implement maximum backups feature in WebDAV settings (#5060)

* feat: implement maximum backups feature in WebDAV settings

- Add maximum backups feature to WebDAV settings

Signed-off-by: ysicing <i@ysicing.me>

* refactor: refactor backup file management for device-specific handling

- Change the order of hostname and device type in the backup file name
- Add filtering for backup files to manage device-specific backups
- Update logic to delete the oldest backup files based on the specific device count

Signed-off-by: ysicing <i@ysicing.me>

---------

Signed-off-by: ysicing <i@ysicing.me>
This commit is contained in:
缘生 2025-04-19 10:22:03 +08:00 committed by GitHub
parent 3e1e814004
commit 9b21c334cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 98 additions and 12 deletions

View File

@ -919,7 +919,9 @@
"syncError": "Backup Error", "syncError": "Backup Error",
"syncStatus": "Backup Status", "syncStatus": "Backup Status",
"title": "WebDAV", "title": "WebDAV",
"user": "WebDAV User" "user": "WebDAV User",
"maxBackups": "Maximum Backups",
"maxBackups.unlimited": "Unlimited"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -919,7 +919,9 @@
"syncError": "バックアップエラー", "syncError": "バックアップエラー",
"syncStatus": "バックアップ状態", "syncStatus": "バックアップ状態",
"title": "WebDAV", "title": "WebDAV",
"user": "WebDAVユーザー" "user": "WebDAVユーザー",
"maxBackups": "最大バックアップ数",
"maxBackups.unlimited": "無制限"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -919,7 +919,9 @@
"syncError": "Ошибка резервного копирования", "syncError": "Ошибка резервного копирования",
"syncStatus": "Статус резервного копирования", "syncStatus": "Статус резервного копирования",
"title": "WebDAV", "title": "WebDAV",
"user": "Пользователь WebDAV" "user": "Пользователь WebDAV",
"maxBackups": "Максимальное количество резервных копий",
"maxBackups.unlimited": "Без ограничений"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -921,7 +921,9 @@
"syncError": "备份错误", "syncError": "备份错误",
"syncStatus": "备份状态", "syncStatus": "备份状态",
"title": "WebDAV", "title": "WebDAV",
"user": "WebDAV 用户名" "user": "WebDAV 用户名",
"maxBackups": "最大备份数",
"maxBackups.unlimited": "无限制"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -919,7 +919,9 @@
"syncError": "備份錯誤", "syncError": "備份錯誤",
"syncStatus": "備份狀態", "syncStatus": "備份狀態",
"title": "WebDAV", "title": "WebDAV",
"user": "WebDAV 使用者名稱" "user": "WebDAV 使用者名稱",
"maxBackups": "最大備份數量",
"maxBackups.unlimited": "無限制"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -814,7 +814,9 @@
"syncError": "Σφάλμα στην αντιγραφή ασφαλείας", "syncError": "Σφάλμα στην αντιγραφή ασφαλείας",
"syncStatus": "Κατάσταση αντιγραφής ασφαλείας", "syncStatus": "Κατάσταση αντιγραφής ασφαλείας",
"title": "WebDAV", "title": "WebDAV",
"user": "Όνομα χρήστη WebDAV" "user": "Όνομα χρήστη WebDAV",
"maxBackups": "Μέγιστο αριθμό αρχείων αντιγραφής ασφαλείας",
"maxBackups.unlimited": "Απεριόριστο"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -814,7 +814,9 @@
"syncError": "Error de copia de seguridad", "syncError": "Error de copia de seguridad",
"syncStatus": "Estado de copia de seguridad", "syncStatus": "Estado de copia de seguridad",
"title": "WebDAV", "title": "WebDAV",
"user": "Nombre de usuario WebDAV" "user": "Nombre de usuario WebDAV",
"maxBackups": "Número máximo de copias de seguridad",
"maxBackups.unlimited": "Sin límite"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -814,7 +814,9 @@
"syncError": "Erreur de sauvegarde", "syncError": "Erreur de sauvegarde",
"syncStatus": "Statut de la sauvegarde", "syncStatus": "Statut de la sauvegarde",
"title": "WebDAV", "title": "WebDAV",
"user": "Nom d'utilisateur WebDAV" "user": "Nom d'utilisateur WebDAV",
"maxBackups": "Nombre maximal de sauvegardes",
"maxBackups.unlimited": "Illimité"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -814,7 +814,9 @@
"syncError": "Erro de backup", "syncError": "Erro de backup",
"syncStatus": "Status de backup", "syncStatus": "Status de backup",
"title": "WebDAV", "title": "WebDAV",
"user": "Nome de usuário WebDAV" "user": "Nome de usuário WebDAV",
"maxBackups": "Número máximo de backups",
"maxBackups.unlimited": "Sem limite"
}, },
"yuque": { "yuque": {
"check": { "check": {

View File

@ -9,6 +9,7 @@ import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
setWebdavAutoSync, setWebdavAutoSync,
setWebdavHost as _setWebdavHost, setWebdavHost as _setWebdavHost,
setWebdavMaxBackups as _setWebdavMaxBackups,
setWebdavPass as _setWebdavPass, setWebdavPass as _setWebdavPass,
setWebdavPath as _setWebdavPath, setWebdavPath as _setWebdavPath,
setWebdavSyncInterval as _setWebdavSyncInterval, setWebdavSyncInterval as _setWebdavSyncInterval,
@ -27,7 +28,8 @@ const WebDavSettings: FC = () => {
webdavUser: webDAVUser, webdavUser: webDAVUser,
webdavPass: webDAVPass, webdavPass: webDAVPass,
webdavPath: webDAVPath, webdavPath: webDAVPath,
webdavSyncInterval: webDAVSyncInterval webdavSyncInterval: webDAVSyncInterval,
webdavMaxBackups: webDAVMaxBackups
} = useSettings() } = useSettings()
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost) const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
@ -37,6 +39,7 @@ const WebDavSettings: FC = () => {
const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval) const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
const [maxBackups, setMaxBackups] = useState<number>(webDAVMaxBackups)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { theme } = useTheme() const { theme } = useTheme()
@ -59,6 +62,11 @@ const WebDavSettings: FC = () => {
} }
} }
const onMaxBackupsChange = (value: number) => {
setMaxBackups(value)
dispatch(_setWebdavMaxBackups(value))
}
const renderSyncStatus = () => { const renderSyncStatus = () => {
if (!webdavHost) return null if (!webdavHost) return null
@ -173,6 +181,19 @@ const WebDavSettings: FC = () => {
<Select.Option value={1440}>{t('settings.data.webdav.hour_interval', { count: 24 })}</Select.Option> <Select.Option value={1440}>{t('settings.data.webdav.hour_interval', { count: 24 })}</Select.Option>
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
<Select value={maxBackups} onChange={onMaxBackupsChange} disabled={!webdavHost} style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.webdav.maxBackups.unlimited')}</Select.Option>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={3}>3</Select.Option>
<Select.Option value={5}>5</Select.Option>
<Select.Option value={10}>10</Select.Option>
<Select.Option value={20}>20</Select.Option>
<Select.Option value={50}>50</Select.Option>
</Select>
</SettingRow>
{webdavSync && syncInterval > 0 && ( {webdavSync && syncInterval > 0 && (
<> <>
<SettingDivider /> <SettingDivider />

View File

@ -82,7 +82,7 @@ export async function backupToWebdav({
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null })) store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings const { webdavHost, webdavUser, webdavPass, webdavPath, webdavMaxBackups } = store.getState().settings
let deviceType = 'unknown' let deviceType = 'unknown'
let hostname = 'unknown' let hostname = 'unknown'
try { try {
@ -92,7 +92,7 @@ export async function backupToWebdav({
Logger.error('[Backup] Failed to get device type or hostname:', error) Logger.error('[Backup] Failed to get device type or hostname:', error)
} }
const timestamp = dayjs().format('YYYYMMDDHHmmss') const timestamp = dayjs().format('YYYYMMDDHHmmss')
const backupFileName = customFileName || `cherry-studio.${timestamp}.${deviceType}.${hostname}.zip` const backupFileName = customFileName || `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip`
const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip` const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
const backupData = await getBackupData() const backupData = await getBackupData()
@ -114,6 +114,47 @@ export async function backupToWebdav({
if (showMessage && !autoBackupProcess) { if (showMessage && !autoBackupProcess) {
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
} }
// 清理旧备份文件
if (webdavMaxBackups > 0) {
try {
// 获取所有备份文件
const files = await window.api.backup.listWebdavFiles({
webdavHost,
webdavUser,
webdavPass,
webdavPath
})
// 筛选当前设备的备份文件
const currentDeviceFiles = files.filter((file) => {
// 检查文件名是否包含当前设备的标识信息
return file.fileName.includes(deviceType) && file.fileName.includes(hostname)
})
// 如果当前设备的备份文件数量超过最大保留数量,删除最旧的文件
if (currentDeviceFiles.length > webdavMaxBackups) {
// 文件已按修改时间降序排序,所以最旧的文件在末尾
const filesToDelete = currentDeviceFiles.slice(webdavMaxBackups)
for (const file of filesToDelete) {
try {
await window.api.backup.deleteWebdavFile(file.fileName, {
webdavHost,
webdavUser,
webdavPass,
webdavPath
})
Logger.log(`[Backup] Deleted old backup file: ${file.fileName}`)
} catch (error) {
Logger.error(`[Backup] Failed to delete old backup file: ${file.fileName}`, error)
}
}
}
} catch (error) {
Logger.error('[Backup] Failed to clean up old backup files:', error)
}
}
} else { } else {
// if auto backup process, throw error // if auto backup process, throw error
if (autoBackupProcess) { if (autoBackupProcess) {

View File

@ -72,6 +72,7 @@ export interface SettingsState {
webdavPath: string webdavPath: string
webdavAutoSync: boolean webdavAutoSync: boolean
webdavSyncInterval: number webdavSyncInterval: number
webdavMaxBackups: number
translateModelPrompt: string translateModelPrompt: string
autoTranslateWithSpace: boolean autoTranslateWithSpace: boolean
enableTopicNaming: boolean enableTopicNaming: boolean
@ -176,6 +177,7 @@ export const initialState: SettingsState = {
webdavPath: '/cherry-studio', webdavPath: '/cherry-studio',
webdavAutoSync: false, webdavAutoSync: false,
webdavSyncInterval: 0, webdavSyncInterval: 0,
webdavMaxBackups: 0,
translateModelPrompt: TRANSLATE_PROMPT, translateModelPrompt: TRANSLATE_PROMPT,
autoTranslateWithSpace: false, autoTranslateWithSpace: false,
enableTopicNaming: true, enableTopicNaming: true,
@ -334,6 +336,9 @@ const settingsSlice = createSlice({
setWebdavSyncInterval: (state, action: PayloadAction<number>) => { setWebdavSyncInterval: (state, action: PayloadAction<number>) => {
state.webdavSyncInterval = action.payload state.webdavSyncInterval = action.payload
}, },
setWebdavMaxBackups: (state, action: PayloadAction<number>) => {
state.webdavMaxBackups = action.payload
},
setCodeShowLineNumbers: (state, action: PayloadAction<boolean>) => { setCodeShowLineNumbers: (state, action: PayloadAction<boolean>) => {
state.codeShowLineNumbers = action.payload state.codeShowLineNumbers = action.payload
}, },
@ -526,6 +531,7 @@ export const {
setWebdavPath, setWebdavPath,
setWebdavAutoSync, setWebdavAutoSync,
setWebdavSyncInterval, setWebdavSyncInterval,
setWebdavMaxBackups,
setCodeShowLineNumbers, setCodeShowLineNumbers,
setCodeCollapsible, setCodeCollapsible,
setCodeWrappable, setCodeWrappable,