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:
parent
3e1e814004
commit
9b21c334cc
@ -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": {
|
||||||
|
|||||||
@ -919,7 +919,9 @@
|
|||||||
"syncError": "バックアップエラー",
|
"syncError": "バックアップエラー",
|
||||||
"syncStatus": "バックアップ状態",
|
"syncStatus": "バックアップ状態",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "WebDAVユーザー"
|
"user": "WebDAVユーザー",
|
||||||
|
"maxBackups": "最大バックアップ数",
|
||||||
|
"maxBackups.unlimited": "無制限"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -919,7 +919,9 @@
|
|||||||
"syncError": "Ошибка резервного копирования",
|
"syncError": "Ошибка резервного копирования",
|
||||||
"syncStatus": "Статус резервного копирования",
|
"syncStatus": "Статус резервного копирования",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "Пользователь WebDAV"
|
"user": "Пользователь WebDAV",
|
||||||
|
"maxBackups": "Максимальное количество резервных копий",
|
||||||
|
"maxBackups.unlimited": "Без ограничений"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -921,7 +921,9 @@
|
|||||||
"syncError": "备份错误",
|
"syncError": "备份错误",
|
||||||
"syncStatus": "备份状态",
|
"syncStatus": "备份状态",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "WebDAV 用户名"
|
"user": "WebDAV 用户名",
|
||||||
|
"maxBackups": "最大备份数",
|
||||||
|
"maxBackups.unlimited": "无限制"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -919,7 +919,9 @@
|
|||||||
"syncError": "備份錯誤",
|
"syncError": "備份錯誤",
|
||||||
"syncStatus": "備份狀態",
|
"syncStatus": "備份狀態",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "WebDAV 使用者名稱"
|
"user": "WebDAV 使用者名稱",
|
||||||
|
"maxBackups": "最大備份數量",
|
||||||
|
"maxBackups.unlimited": "無限制"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -814,7 +814,9 @@
|
|||||||
"syncError": "Σφάλμα στην αντιγραφή ασφαλείας",
|
"syncError": "Σφάλμα στην αντιγραφή ασφαλείας",
|
||||||
"syncStatus": "Κατάσταση αντιγραφής ασφαλείας",
|
"syncStatus": "Κατάσταση αντιγραφής ασφαλείας",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "Όνομα χρήστη WebDAV"
|
"user": "Όνομα χρήστη WebDAV",
|
||||||
|
"maxBackups": "Μέγιστο αριθμό αρχείων αντιγραφής ασφαλείας",
|
||||||
|
"maxBackups.unlimited": "Απεριόριστο"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user