mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 05:09:09 +08:00
feat(backup): add single-file overwrite options for backups
This commit is contained in:
parent
13093bb821
commit
ae392eb2ef
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user