mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-09 09:29:36 +08:00
feat: enhance backup functionality with section selection and import mode options
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import request from './client';
|
||||
|
||||
export const backupApi = {
|
||||
export: async () => {
|
||||
const response = await request('/backup/export', {
|
||||
export: async (sections?: string[]) => {
|
||||
const params = new URLSearchParams();
|
||||
(sections || []).forEach((section) => params.append('sections', section));
|
||||
const query = params.toString();
|
||||
const response = await request(`/backup/export${query ? `?${query}` : ''}`, {
|
||||
method: 'GET',
|
||||
rawResponse: true,
|
||||
}) as Response;
|
||||
@@ -27,12 +30,13 @@ export const backupApi = {
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
import: async (file: File) => {
|
||||
import: async (file: File, mode: 'replace' | 'merge' = 'replace') => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('mode', mode);
|
||||
return request('/backup/import', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -556,10 +556,24 @@
|
||||
"Export": "Export",
|
||||
"Import": "Import",
|
||||
"Export all data (adapters, users, tasks, shares) into a JSON file.": "Export all data (adapters, users, tasks, shares) into a JSON file.",
|
||||
"Export selected data into a JSON file.": "Export selected data into a JSON file.",
|
||||
"Keep your backup file safe.": "Keep your backup file safe.",
|
||||
"Select backup sections": "Select backup sections",
|
||||
"User Accounts": "User Accounts",
|
||||
"Share Links": "Share Links",
|
||||
"Configurations": "Configurations",
|
||||
"AI Providers": "AI Providers",
|
||||
"AI Models": "AI Models",
|
||||
"AI Default Models": "AI Default Models",
|
||||
"Plugin Data": "Plugins",
|
||||
"Export Backup": "Export Backup",
|
||||
"Restore data from a previously exported JSON file.": "Restore data from a previously exported JSON file.",
|
||||
"Warning: This will clear and overwrite existing data.": "Warning: This will clear and overwrite existing data.",
|
||||
"Import mode": "Import mode",
|
||||
"Merge (upsert by ID)": "Merge (upsert by ID)",
|
||||
"Replace (clear before import)": "Replace (clear before import)",
|
||||
"Warning: This will clear data in the backup sections before importing.": "Warning: This will clear data in the backup sections before importing.",
|
||||
"Warning: This will merge data in the backup sections and overwrite existing records with the same ID.": "Warning: This will merge data in the backup sections and overwrite existing records with the same ID.",
|
||||
"Choose File and Restore": "Choose File and Restore",
|
||||
"No files yet here": "No files yet here",
|
||||
"This folder is empty": "This folder is empty",
|
||||
|
||||
@@ -547,10 +547,24 @@
|
||||
"Export": "导出",
|
||||
"Import": "恢复",
|
||||
"Export all data (adapters, users, tasks, shares) into a JSON file.": "点击按钮将所有数据(包括存储、用户、自动化任务和分享)导出为一个 JSON 文件。",
|
||||
"Export selected data into a JSON file.": "导出选中的数据为一个 JSON 文件。",
|
||||
"Keep your backup file safe.": "请妥善保管您的备份文件。",
|
||||
"Select backup sections": "选择备份内容",
|
||||
"User Accounts": "账号",
|
||||
"Share Links": "分享列表",
|
||||
"Configurations": "配置",
|
||||
"AI Providers": "AI 服务商",
|
||||
"AI Models": "AI 模型",
|
||||
"AI Default Models": "AI 默认模型",
|
||||
"Plugin Data": "插件",
|
||||
"Export Backup": "导出备份",
|
||||
"Restore data from a previously exported JSON file.": "从之前导出的JSON文件恢复数据。",
|
||||
"Warning: This will clear and overwrite existing data.": "警告:此操作将清除并覆盖现有数据。",
|
||||
"Import mode": "导入方式",
|
||||
"Merge (upsert by ID)": "增量+覆盖(按 ID)",
|
||||
"Replace (clear before import)": "清空后导入",
|
||||
"Warning: This will clear data in the backup sections before importing.": "警告:此操作会先清空备份中包含的分区数据,再导入。",
|
||||
"Warning: This will merge data in the backup sections and overwrite existing records with the same ID.": "警告:此操作会合并备份中包含的分区数据,并按 ID 覆盖已存在记录。",
|
||||
"Choose File and Restore": "选择文件并恢复",
|
||||
"No files yet here": "这里还没有任何文件",
|
||||
"This folder is empty": "此目录为空",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Button, Typography, Upload, message, Modal, Card } from 'antd';
|
||||
import { Button, Typography, Upload, message, Modal, Card, Checkbox, Space, Radio } from 'antd';
|
||||
import PageCard from '../../components/PageCard';
|
||||
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { backupApi } from '../../api/backup';
|
||||
@@ -7,14 +7,40 @@ import { useI18n } from '../../i18n';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
const BACKUP_SECTIONS = [
|
||||
{ key: 'user_accounts', labelKey: 'User Accounts' },
|
||||
{ key: 'storage_adapters', labelKey: 'Storage Adapters' },
|
||||
{ key: 'automation_tasks', labelKey: 'Automation Tasks' },
|
||||
{ key: 'share_links', labelKey: 'Share Links' },
|
||||
{ key: 'configurations', labelKey: 'Configurations' },
|
||||
{ key: 'ai_providers', labelKey: 'AI Providers' },
|
||||
{ key: 'ai_models', labelKey: 'AI Models' },
|
||||
{ key: 'ai_default_models', labelKey: 'AI Default Models' },
|
||||
{ key: 'plugins', labelKey: 'Plugin Data' },
|
||||
] as const;
|
||||
|
||||
type BackupSection = typeof BACKUP_SECTIONS[number]['key'];
|
||||
const ALL_SECTION_KEYS = BACKUP_SECTIONS.map((section) => section.key) as BackupSection[];
|
||||
|
||||
const BackupPage = memo(function BackupPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedSections, setSelectedSections] = useState<BackupSection[]>(ALL_SECTION_KEYS);
|
||||
const [importMode, setImportMode] = useState<'replace' | 'merge'>('replace');
|
||||
const { t } = useI18n();
|
||||
const importWarning = importMode === 'replace'
|
||||
? t('Warning: This will clear data in the backup sections before importing.')
|
||||
: t('Warning: This will merge data in the backup sections and overwrite existing records with the same ID.');
|
||||
const importWarningType = importMode === 'replace' ? 'danger' : 'warning';
|
||||
const exportOptions = BACKUP_SECTIONS.map((section) => ({
|
||||
label: t(section.labelKey),
|
||||
value: section.key,
|
||||
}));
|
||||
const canExport = selectedSections.length > 0;
|
||||
|
||||
const handleExport = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await backupApi.export();
|
||||
await backupApi.export(selectedSections);
|
||||
message.success(t('Export started, check your downloads.'));
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('Export failed'));
|
||||
@@ -29,7 +55,9 @@ const BackupPage = memo(function BackupPage() {
|
||||
content: (
|
||||
<Typography>
|
||||
<Paragraph>{t('Are you sure to import from this file?')}</Paragraph>
|
||||
<Paragraph strong>{t('Warning: This will overwrite all data including users (with passwords), settings, storages and tasks. Irreversible!')}</Paragraph>
|
||||
<Paragraph>
|
||||
<Text strong type={importWarningType}>{importWarning}</Text>
|
||||
</Paragraph>
|
||||
</Typography>
|
||||
),
|
||||
okText: t('Confirm Import'),
|
||||
@@ -38,7 +66,7 @@ const BackupPage = memo(function BackupPage() {
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await backupApi.import(file);
|
||||
const response = await backupApi.import(file, importMode);
|
||||
message.success(response.message || t('Import succeeded! The page will refresh.'));
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (e: any) {
|
||||
@@ -57,13 +85,22 @@ const BackupPage = memo(function BackupPage() {
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
<Card title={t('Export')} style={{ flex: 1 }}>
|
||||
<Paragraph>
|
||||
{t('Export all data (adapters, users, tasks, shares) into a JSON file.')}
|
||||
{t('Export selected data into a JSON file.')}
|
||||
<Text strong>{t('Keep your backup file safe.')}</Text>
|
||||
</Paragraph>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 12 }}>
|
||||
<Text>{t('Select backup sections')}</Text>
|
||||
<Checkbox.Group
|
||||
options={exportOptions}
|
||||
value={selectedSections}
|
||||
onChange={(values) => setSelectedSections(values as BackupSection[])}
|
||||
/>
|
||||
</Space>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
loading={loading}
|
||||
disabled={!canExport}
|
||||
>
|
||||
{t('Export Backup')}
|
||||
</Button>
|
||||
@@ -71,8 +108,22 @@ const BackupPage = memo(function BackupPage() {
|
||||
<Card title={t('Import')} style={{ flex: 1 }}>
|
||||
<Paragraph>
|
||||
{t('Restore data from a previously exported JSON file.')}
|
||||
<Text strong type="danger">{t('Warning: This will clear and overwrite existing data.')}</Text>
|
||||
</Paragraph>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 12 }}>
|
||||
<Text>{t('Import mode')}</Text>
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
value={importMode}
|
||||
onChange={(event) => setImportMode(event.target.value)}
|
||||
>
|
||||
<Radio.Button value="merge">{t('Merge (upsert by ID)')}</Radio.Button>
|
||||
<Radio.Button value="replace">{t('Replace (clear before import)')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Text type={importWarningType}>
|
||||
{importWarning}
|
||||
</Text>
|
||||
</Space>
|
||||
<Upload
|
||||
beforeUpload={handleImport}
|
||||
showUploadList={false}
|
||||
|
||||
Reference in New Issue
Block a user