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

View File

@ -168,7 +168,9 @@ export async function backupToWebdav({
webdavPath, webdavPath,
webdavMaxBackups, webdavMaxBackups,
webdavSkipBackupFile, webdavSkipBackupFile,
webdavDisableStream webdavDisableStream,
webdavSingleFileOverwrite,
webdavSingleFileName
} = store.getState().settings } = store.getState().settings
let deviceType = 'unknown' let deviceType = 'unknown'
let hostname = 'unknown' let hostname = 'unknown'
@ -179,7 +181,12 @@ export async function backupToWebdav({
logger.error('Failed to get device type or hostname:', error as Error) logger.error('Failed to get device type or hostname:', error as Error)
} }
const timestamp = dayjs().format('YYYYMMDDHHmmss') 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 finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
const backupData = await getBackupData() const backupData = await getBackupData()
@ -212,8 +219,8 @@ export async function backupToWebdav({
}) })
showMessage && window.toast.success(i18n.t('message.backup.success')) showMessage && window.toast.success(i18n.t('message.backup.success'))
// 清理旧备份文件 // 覆盖式单文件备份启用时(且=1不进行清理
if (webdavMaxBackups > 0) { if (webdavMaxBackups > 0 && !(autoBackupProcess && webdavMaxBackups === 1 && webdavSingleFileOverwrite)) {
try { try {
// 获取所有备份文件 // 获取所有备份文件
const files = await window.api.backup.listWebdavFiles({ 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) logger.error('Failed to get device type or hostname:', error as Error)
} }
const timestamp = dayjs().format('YYYYMMDDHHmmss') 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 finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
const backupData = await getBackupData() const backupData = await getBackupData()

View File

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

View File

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