diff --git a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx index 9ea3dbceac..ccf8b41ae9 100644 --- a/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/S3Settings.tsx @@ -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(s3EndpointInit) @@ -40,6 +42,8 @@ const S3Settings: FC = () => { const [secretAccessKey, setSecretAccessKey] = useState(s3SecretAccessKeyInit) const [root, setRoot] = useState(s3RootInit) const [skipBackupFile, setSkipBackupFile] = useState(s3SkipBackupFileInit) + const [singleFileOverwrite, setSingleFileOverwrite] = useState(!!s3SingleFileOverwriteInit) + const [singleFileName, setSingleFileName] = useState(s3SingleFileNameInit) const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [syncInterval, setSyncInterval] = useState(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 diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index c168bd489b..57f2f8e8d0 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -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() diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 273653aa0c..93ab203d14 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -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) => { state.webdavDisableStream = action.payload }, + setWebdavSingleFileOverwrite: (state, action: PayloadAction) => { + state.webdavSingleFileOverwrite = action.payload + }, + setWebdavSingleFileName: (state, action: PayloadAction) => { + 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) => { state.localBackupSkipBackupFile = action.payload }, + setLocalSingleFileOverwrite: (state, action: PayloadAction) => { + state.localSingleFileOverwrite = action.payload + }, + setLocalSingleFileName: (state, action: PayloadAction) => { + state.localSingleFileName = action.payload + }, setDefaultPaintingProvider: (state, action: PayloadAction) => { 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, diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 1b1d69c3cb..6ae0579d13 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -873,6 +873,10 @@ export type S3Config = { autoSync: boolean syncInterval: number maxBackups: number + /** 当自动备份且保留份数=1时,是否启用覆盖式单文件备份 */ + singleFileOverwrite?: boolean + /** 覆盖式单文件备份的自定义文件名(可选,默认使用不带时间戳的设备名+主机名) */ + singleFileName?: string } export type { Message } from './newMessage'