mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 14:31:35 +08:00
257 lines
7.0 KiB
TypeScript
257 lines
7.0 KiB
TypeScript
import { backupToS3 } from '@renderer/services/BackupService'
|
|
import { formatFileSize } from '@renderer/utils'
|
|
import { Input, Modal, Select, Spin } from 'antd'
|
|
import dayjs from 'dayjs'
|
|
import { useCallback, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
interface BackupFile {
|
|
fileName: string
|
|
modifiedTime: string
|
|
size: number
|
|
}
|
|
|
|
export function useS3BackupModal() {
|
|
const [customFileName, setCustomFileName] = useState('')
|
|
const [isModalVisible, setIsModalVisible] = useState(false)
|
|
const [backuping, setBackuping] = useState(false)
|
|
|
|
const handleBackup = async () => {
|
|
setBackuping(true)
|
|
try {
|
|
await backupToS3({ customFileName, showMessage: true })
|
|
} finally {
|
|
setBackuping(false)
|
|
setIsModalVisible(false)
|
|
}
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
setIsModalVisible(false)
|
|
}
|
|
|
|
const showBackupModal = useCallback(async () => {
|
|
// 获取默认文件名
|
|
const deviceType = await window.api.system.getDeviceType()
|
|
const hostname = await window.api.system.getHostname()
|
|
const timestamp = dayjs().format('YYYYMMDDHHmmss')
|
|
const defaultFileName = `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip`
|
|
setCustomFileName(defaultFileName)
|
|
setIsModalVisible(true)
|
|
}, [])
|
|
|
|
return {
|
|
isModalVisible,
|
|
handleBackup,
|
|
handleCancel,
|
|
backuping,
|
|
customFileName,
|
|
setCustomFileName,
|
|
showBackupModal
|
|
}
|
|
}
|
|
|
|
type S3BackupModalProps = {
|
|
isModalVisible: boolean
|
|
handleBackup: () => Promise<void>
|
|
handleCancel: () => void
|
|
backuping: boolean
|
|
customFileName: string
|
|
setCustomFileName: (value: string) => void
|
|
}
|
|
|
|
export function S3BackupModal({
|
|
isModalVisible,
|
|
handleBackup,
|
|
handleCancel,
|
|
backuping,
|
|
customFileName,
|
|
setCustomFileName
|
|
}: S3BackupModalProps) {
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<Modal
|
|
title={t('settings.data.s3.backup.modal.title')}
|
|
open={isModalVisible}
|
|
onOk={handleBackup}
|
|
onCancel={handleCancel}
|
|
okButtonProps={{ loading: backuping }}
|
|
transitionName="animation-move-down"
|
|
centered>
|
|
<Input
|
|
value={customFileName}
|
|
onChange={(e) => setCustomFileName(e.target.value)}
|
|
placeholder={t('settings.data.s3.backup.modal.filename.placeholder')}
|
|
/>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
interface UseS3RestoreModalProps {
|
|
endpoint: string | undefined
|
|
region: string | undefined
|
|
bucket: string | undefined
|
|
accessKeyId: string | undefined
|
|
secretAccessKey: string | undefined
|
|
root?: string | undefined
|
|
}
|
|
|
|
export function useS3RestoreModal({
|
|
endpoint,
|
|
region,
|
|
bucket,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
root
|
|
}: UseS3RestoreModalProps) {
|
|
const [isRestoreModalVisible, setIsRestoreModalVisible] = useState(false)
|
|
const [restoring, setRestoring] = useState(false)
|
|
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
|
const [loadingFiles, setLoadingFiles] = useState(false)
|
|
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([])
|
|
const { t } = useTranslation()
|
|
|
|
const showRestoreModal = useCallback(async () => {
|
|
if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) {
|
|
window.toast.error(t('settings.data.s3.manager.config.incomplete'))
|
|
return
|
|
}
|
|
|
|
setIsRestoreModalVisible(true)
|
|
setLoadingFiles(true)
|
|
try {
|
|
const files = await window.api.backup.listS3Files({
|
|
endpoint,
|
|
region,
|
|
bucket,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
root,
|
|
autoSync: false,
|
|
syncInterval: 0,
|
|
maxBackups: 0,
|
|
skipBackupFile: false
|
|
})
|
|
setBackupFiles(files)
|
|
} catch (error: any) {
|
|
window.toast.error(t('settings.data.s3.manager.files.fetch.error', { message: error.message }))
|
|
} finally {
|
|
setLoadingFiles(false)
|
|
}
|
|
}, [endpoint, region, bucket, accessKeyId, secretAccessKey, root, t])
|
|
|
|
const handleRestore = useCallback(async () => {
|
|
if (!selectedFile || !endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) {
|
|
window.toast.error(
|
|
!selectedFile ? t('settings.data.s3.restore.file.required') : t('settings.data.s3.restore.config.incomplete')
|
|
)
|
|
return
|
|
}
|
|
|
|
window.modal.confirm({
|
|
title: t('settings.data.s3.restore.confirm.title'),
|
|
content: t('settings.data.s3.restore.confirm.content', { fileName: selectedFile }),
|
|
okText: t('settings.data.s3.restore.confirm.ok'),
|
|
cancelText: t('settings.data.s3.restore.confirm.cancel'),
|
|
centered: true,
|
|
onOk: async () => {
|
|
setRestoring(true)
|
|
try {
|
|
await window.api.backup.restoreFromS3({
|
|
endpoint,
|
|
region,
|
|
bucket,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
root,
|
|
fileName: selectedFile,
|
|
autoSync: false,
|
|
syncInterval: 0,
|
|
maxBackups: 0,
|
|
skipBackupFile: false
|
|
})
|
|
window.toast.success(t('message.restore.success'))
|
|
setIsRestoreModalVisible(false)
|
|
} catch (error: any) {
|
|
window.toast.error(t('settings.data.s3.restore.error', { message: error.message }))
|
|
} finally {
|
|
setRestoring(false)
|
|
}
|
|
}
|
|
})
|
|
}, [selectedFile, endpoint, region, bucket, accessKeyId, secretAccessKey, root, t])
|
|
|
|
const handleCancel = () => {
|
|
setIsRestoreModalVisible(false)
|
|
}
|
|
|
|
return {
|
|
isRestoreModalVisible,
|
|
handleRestore,
|
|
handleCancel,
|
|
restoring,
|
|
selectedFile,
|
|
setSelectedFile,
|
|
loadingFiles,
|
|
backupFiles,
|
|
showRestoreModal
|
|
}
|
|
}
|
|
|
|
type S3RestoreModalProps = ReturnType<typeof useS3RestoreModal>
|
|
|
|
export function S3RestoreModal({
|
|
isRestoreModalVisible,
|
|
handleRestore,
|
|
handleCancel,
|
|
restoring,
|
|
selectedFile,
|
|
setSelectedFile,
|
|
loadingFiles,
|
|
backupFiles
|
|
}: S3RestoreModalProps) {
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<Modal
|
|
title={t('settings.data.s3.restore.modal.title')}
|
|
open={isRestoreModalVisible}
|
|
onOk={handleRestore}
|
|
onCancel={handleCancel}
|
|
okButtonProps={{ loading: restoring }}
|
|
width={600}
|
|
transitionName="animation-move-down"
|
|
centered>
|
|
<div style={{ position: 'relative' }}>
|
|
<Select
|
|
style={{ width: '100%' }}
|
|
placeholder={t('settings.data.s3.restore.modal.select.placeholder')}
|
|
value={selectedFile}
|
|
onChange={setSelectedFile}
|
|
options={backupFiles.map(formatFileOption)}
|
|
loading={loadingFiles}
|
|
showSearch
|
|
filterOption={(input, option) =>
|
|
typeof option?.label === 'string' ? option.label.toLowerCase().includes(input.toLowerCase()) : false
|
|
}
|
|
/>
|
|
{loadingFiles && (
|
|
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
|
|
<Spin />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
function formatFileOption(file: BackupFile) {
|
|
const date = dayjs(file.modifiedTime).format('YYYY-MM-DD HH:mm:ss')
|
|
const size = formatFileSize(file.size)
|
|
return {
|
|
label: `${file.fileName} (${date}, ${size})`,
|
|
value: file.fileName
|
|
}
|
|
}
|