feat(backup): add single-file overwrite options for backups

This commit is contained in:
GeorgeDong32 2025-09-25 12:45:27 +08:00 committed by GeorgeDong32
parent 13093bb821
commit ae392eb2ef
4 changed files with 64 additions and 7 deletions

View File

@ -30,7 +30,9 @@ const S3Settings: FC = () => {
root: s3RootInit = '',
syncInterval: s3SyncIntervalInit = 0,
maxBackups: s3MaxBackupsInit = 5,
skipBackupFile: s3SkipBackupFileInit = false
skipBackupFile: s3SkipBackupFileInit = false,
singleFileOverwrite: s3SingleFileOverwriteInit = false,
singleFileName: s3SingleFileNameInit = ''
} = s3
const [endpoint, setEndpoint] = useState<string | undefined>(s3EndpointInit)
@ -40,6 +42,8 @@ const S3Settings: FC = () => {
const [secretAccessKey, setSecretAccessKey] = useState<string | undefined>(s3SecretAccessKeyInit)
const [root, setRoot] = useState<string | undefined>(s3RootInit)
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(s3SkipBackupFileInit)
const [singleFileOverwrite, setSingleFileOverwrite] = useState<boolean>(!!s3SingleFileOverwriteInit)
const [singleFileName, setSingleFileName] = useState<string | undefined>(s3SingleFileNameInit)
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(s3SyncIntervalInit)
@ -81,6 +85,15 @@ const S3Settings: FC = () => {
dispatch(setS3Partial({ skipBackupFile: value }))
}
const onSingleFileOverwriteChange = (value: boolean) => {
setSingleFileOverwrite(value)
dispatch(setS3Partial({ singleFileOverwrite: value }))
}
const onSingleFileNameBlur = () => {
dispatch(setS3Partial({ singleFileName: singleFileName || '' }))
}
const renderSyncStatus = () => {
if (!endpoint) return null

View File

@ -168,7 +168,9 @@ export async function backupToWebdav({
webdavPath,
webdavMaxBackups,
webdavSkipBackupFile,
webdavDisableStream
webdavDisableStream,
webdavSingleFileOverwrite,
webdavSingleFileName
} = store.getState().settings
let deviceType = 'unknown'
let hostname = 'unknown'
@ -179,7 +181,12 @@ export async function backupToWebdav({
logger.error('Failed to get device type or hostname:', error as Error)
}
const timestamp = dayjs().format('YYYYMMDDHHmmss')
const backupFileName = customFileName || `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip`
let backupFileName = customFileName || `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip`
// 覆盖式单文件备份(仅在自动备份流程且保留份数=1时生效
if (autoBackupProcess && webdavMaxBackups === 1 && webdavSingleFileOverwrite) {
const base = (webdavSingleFileName || `cherry-studio.${hostname}.${deviceType}`).trim()
backupFileName = base.endsWith('.zip') ? base : `${base}.zip`
}
const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
const backupData = await getBackupData()
@ -212,8 +219,8 @@ export async function backupToWebdav({
})
showMessage && window.toast.success(i18n.t('message.backup.success'))
// 清理旧备份文件
if (webdavMaxBackups > 0) {
// 覆盖式单文件备份启用时(且=1不进行清理
if (webdavMaxBackups > 0 && !(autoBackupProcess && webdavMaxBackups === 1 && webdavSingleFileOverwrite)) {
try {
// 获取所有备份文件
const files = await window.api.backup.listWebdavFiles({
@ -353,7 +360,12 @@ export async function backupToS3({
logger.error('Failed to get device type or hostname:', error as Error)
}
const timestamp = dayjs().format('YYYYMMDDHHmmss')
const backupFileName = customFileName || `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip`
let backupFileName = customFileName || `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip`
// 覆盖式单文件备份(仅在自动备份流程且保留份数=1时生效
if (autoBackupProcess && s3Config.maxBackups === 1 && s3Config.singleFileOverwrite) {
const base = (s3Config.singleFileName || `cherry-studio.${hostname}.${deviceType}`).trim()
backupFileName = base.endsWith('.zip') ? base : `${base}.zip`
}
const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
const backupData = await getBackupData()

View File

@ -119,6 +119,9 @@ export interface SettingsState {
webdavMaxBackups: number
webdavSkipBackupFile: boolean
webdavDisableStream: boolean
// 覆盖式单文件备份WebDAV
webdavSingleFileOverwrite?: boolean
webdavSingleFileName?: string
translateModelPrompt: string
autoTranslateWithSpace: boolean
showTranslateConfirm: boolean
@ -210,6 +213,9 @@ export interface SettingsState {
localBackupSyncInterval: number
localBackupMaxBackups: number
localBackupSkipBackupFile: boolean
// 覆盖式单文件备份Local
localSingleFileOverwrite?: boolean
localSingleFileName?: string
defaultPaintingProvider: PaintingProvider
s3: S3Config
// Developer mode
@ -306,6 +312,8 @@ export const initialState: SettingsState = {
webdavMaxBackups: 0,
webdavSkipBackupFile: false,
webdavDisableStream: false,
webdavSingleFileOverwrite: false,
webdavSingleFileName: '',
translateModelPrompt: TRANSLATE_PROMPT,
autoTranslateWithSpace: false,
showTranslateConfirm: true,
@ -389,6 +397,8 @@ export const initialState: SettingsState = {
localBackupSyncInterval: 0,
localBackupMaxBackups: 0,
localBackupSkipBackupFile: false,
localSingleFileOverwrite: false,
localSingleFileName: '',
defaultPaintingProvider: 'zhipu',
s3: {
endpoint: '',
@ -400,7 +410,9 @@ export const initialState: SettingsState = {
autoSync: false,
syncInterval: 0,
maxBackups: 0,
skipBackupFile: false
skipBackupFile: false,
singleFileOverwrite: false,
singleFileName: ''
},
// Developer mode
@ -556,6 +568,12 @@ const settingsSlice = createSlice({
setWebdavDisableStream: (state, action: PayloadAction<boolean>) => {
state.webdavDisableStream = action.payload
},
setWebdavSingleFileOverwrite: (state, action: PayloadAction<boolean>) => {
state.webdavSingleFileOverwrite = action.payload
},
setWebdavSingleFileName: (state, action: PayloadAction<string>) => {
state.webdavSingleFileName = action.payload
},
setCodeExecution: (state, action: PayloadAction<{ enabled?: boolean; timeoutMinutes?: number }>) => {
if (action.payload.enabled !== undefined) {
state.codeExecution.enabled = action.payload.enabled
@ -816,6 +834,12 @@ const settingsSlice = createSlice({
setLocalBackupSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.localBackupSkipBackupFile = action.payload
},
setLocalSingleFileOverwrite: (state, action: PayloadAction<boolean>) => {
state.localSingleFileOverwrite = action.payload
},
setLocalSingleFileName: (state, action: PayloadAction<string>) => {
state.localSingleFileName = action.payload
},
setDefaultPaintingProvider: (state, action: PayloadAction<PaintingProvider>) => {
state.defaultPaintingProvider = action.payload
},
@ -903,6 +927,8 @@ export const {
setWebdavMaxBackups,
setWebdavSkipBackupFile,
setWebdavDisableStream,
setWebdavSingleFileOverwrite,
setWebdavSingleFileName,
setCodeExecution,
setCodeEditor,
setCodeViewer,
@ -974,6 +1000,8 @@ export const {
setLocalBackupSyncInterval,
setLocalBackupMaxBackups,
setLocalBackupSkipBackupFile,
setLocalSingleFileOverwrite,
setLocalSingleFileName,
setDefaultPaintingProvider,
setS3,
setS3Partial,

View File

@ -873,6 +873,10 @@ export type S3Config = {
autoSync: boolean
syncInterval: number
maxBackups: number
/** 当自动备份且保留份数=1时是否启用覆盖式单文件备份 */
singleFileOverwrite?: boolean
/** 覆盖式单文件备份的自定义文件名(可选,默认使用不带时间戳的设备名+主机名) */
singleFileName?: string
}
export type { Message } from './newMessage'