备份数据支持选择模块,支持清除依赖缓存

This commit is contained in:
whyour 2025-06-22 14:25:19 +08:00
parent c9bd053fbd
commit ef9e38f167
7 changed files with 329 additions and 149 deletions

View File

@ -316,10 +316,15 @@ export default (app: Router) => {
route.put( route.put(
'/data/export', '/data/export',
celebrate({
body: Joi.object({
type: Joi.array().items(Joi.string()).optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
await systemService.exportData(res); await systemService.exportData(res, req.body.type);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@ -416,4 +421,22 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/config/dependence-clean',
celebrate({
body: Joi.object({
type: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.cleanDependence(req.body.type);
res.send(result);
} catch (e) {
return next(e);
}
},
);
}; };

View File

@ -86,6 +86,7 @@ const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/'); const sshdPath = path.join(dataPath, 'ssh.d/');
const systemLogPath = path.join(dataPath, 'syslog/'); const systemLogPath = path.join(dataPath, 'syslog/');
const dependenceCachePath = path.join(dataPath, 'dep_cache/');
const envFile = path.join(preloadPath, 'env.sh'); const envFile = path.join(preloadPath, 'env.sh');
const jsEnvFile = path.join(preloadPath, 'env.js'); const jsEnvFile = path.join(preloadPath, 'env.js');
@ -174,4 +175,5 @@ export default {
sqliteFile, sqliteFile,
sshdPath, sshdPath,
systemLogPath, systemLogPath,
dependenceCachePath,
}; };

View File

@ -415,10 +415,17 @@ export default class SystemService {
} }
} }
public async exportData(res: Response) { public async exportData(res: Response, type?: string[]) {
try { try {
let dataDirs = ['db', 'upload'];
if (type && type.length) {
dataDirs = dataDirs.concat(type.filter((x) => x !== 'base'));
}
const dataPaths = dataDirs.map((dir) => `data/${dir}`);
await promiseExec( await promiseExec(
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile} data/`, `cd ${config.dataPath} && cd ../ && tar -zcvf ${
config.dataTgzFile
} ${dataPaths.join(' ')}`,
); );
res.download(config.dataTgzFile); res.download(config.dataTgzFile);
} catch (error: any) { } catch (error: any) {
@ -503,4 +510,15 @@ export default class SystemService {
return { code: 400, message: '设置时区失败' }; return { code: 400, message: '设置时区失败' };
} }
} }
public async cleanDependence(type: 'node' | 'python3') {
if (!type || !['node', 'python3'].includes(type)) {
return { code: 400, message: '参数错误' };
}
try {
const finalPath = path.join(config.dependenceCachePath, type);
await fs.promises.rm(finalPath, { recursive: true });
} catch (error) {}
return { code: 200 };
}
} }

View File

@ -510,5 +510,16 @@
"强制打开可能会导致编辑器显示异常": "Force opening may cause display issues in the editor", "强制打开可能会导致编辑器显示异常": "Force opening may cause display issues in the editor",
"确认离开": "Confirm Leave", "确认离开": "Confirm Leave",
"当前文件未保存,确认离开吗": "Current file is not saved, are you sure to leave?", "当前文件未保存,确认离开吗": "Current file is not saved, are you sure to leave?",
"收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "Receiving email address, multiple semicolon separated, sent to the sending email address by default" "收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "Receiving email address, multiple semicolon separated, sent to the sending email address by default",
"选择备份模块": "Select backup module",
"开始备份": "Start backup",
"基础数据": "Basic data",
"脚本文件": "Script files",
"日志文件": "Log files",
"依赖缓存": "Dependency cache",
"远程脚本缓存": "Remote script cache",
"远程仓库缓存": "Remote repository cache",
"SSH 文件缓存": "SSH file cache",
"清除依赖缓存": "Clean dependency cache",
"清除成功": "Clean successful"
} }

View File

@ -510,5 +510,16 @@
"强制打开可能会导致编辑器显示异常": "强制打开可能会导致编辑器显示异常", "强制打开可能会导致编辑器显示异常": "强制打开可能会导致编辑器显示异常",
"确认离开": "确认离开", "确认离开": "确认离开",
"当前文件未保存,确认离开吗": "当前文件未保存,确认离开吗", "当前文件未保存,确认离开吗": "当前文件未保存,确认离开吗",
"收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址" "收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址",
"选择备份模块": "选择备份模块",
"开始备份": "开始备份",
"基础数据": "基础数据",
"脚本文件": "脚本文件",
"日志文件": "日志文件",
"依赖缓存": "依赖缓存",
"远程脚本缓存": "远程脚本缓存",
"远程仓库缓存": "远程仓库缓存",
"SSH 文件缓存": "SSH 文件缓存",
"清除依赖缓存": "清除依赖缓存",
"清除成功": "清除成功"
} }

View File

@ -1,6 +1,6 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Button, InputNumber, Form, message, Input, Alert } from 'antd'; import { Button, InputNumber, Form, message, Input, Alert, Select } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
@ -25,6 +25,7 @@ const Dependence = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [log, setLog] = useState<string>(''); const [log, setLog] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [cleanType, setCleanType] = useState<string>('node');
const getSystemConfig = () => { const getSystemConfig = () => {
request request
@ -84,6 +85,24 @@ const Dependence = () => {
} }
}; };
const cleanDependenceCache = (type: string) => {
setLoading(true);
setLog('');
request
.put(`${config.apiPrefix}system/config/dependence-clean`, {
type,
})
.then(({ code, data }) => {
if (code === 200) {
message.success(intl.get('清除成功'));
}
})
.catch((error: any) => {
console.log(error);
})
.finally(() => setLoading(false));
};
useEffect(() => { useEffect(() => {
const ws = WebSocketManager.getInstance(); const ws = WebSocketManager.getInstance();
ws.subscribe('updateNodeMirror', handleMessage); ws.subscribe('updateNodeMirror', handleMessage);
@ -222,6 +241,38 @@ const Dependence = () => {
</Button> </Button>
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item
label={intl.get('清除依赖缓存')}
name="clean"
tooltip={{
title: intl.get('清除依赖缓存'),
placement: 'topLeft',
}}
>
<Input.Group compact>
<Select
defaultValue={'node'}
style={{ width: 100 }}
onChange={(value) => {
setCleanType(value);
}}
options={[
{ label: 'node', value: 'node' },
{ label: 'python3', value: 'python3' },
]}
/>
<Button
type="primary"
loading={loading}
onClick={() => {
cleanDependenceCache(cleanType);
}}
style={{ width: 100 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
</Form> </Form>
<pre <pre
style={{ style={{

View File

@ -10,6 +10,7 @@ import {
Upload, Upload,
Modal, Modal,
Select, Select,
Checkbox,
} from 'antd'; } from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader'; import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config'; import config from '@/utils/config';
@ -31,6 +32,19 @@ const dataMap = {
timezone: 'timezone', timezone: 'timezone',
}; };
const exportModules = [
{ value: 'base', label: intl.get('基础数据'), disabled: true },
{ value: 'config', label: intl.get('配置文件') },
{ value: 'scripts', label: intl.get('脚本文件') },
{ value: 'log', label: intl.get('日志文件') },
{ value: 'deps', label: intl.get('依赖文件') },
{ value: 'syslog', label: intl.get('系统日志') },
{ value: 'dep_cache', label: intl.get('依赖缓存') },
{ value: 'raw', label: intl.get('远程脚本缓存') },
{ value: 'repo', label: intl.get('远程仓库缓存') },
{ value: 'ssh.d', label: intl.get('SSH 文件缓存') },
];
const Other = ({ const Other = ({
systemInfo, systemInfo,
reloadTheme, reloadTheme,
@ -45,6 +59,8 @@ const Other = ({
const [exportLoading, setExportLoading] = useState(false); const [exportLoading, setExportLoading] = useState(false);
const showUploadProgress = useProgress(intl.get('上传')); const showUploadProgress = useProgress(intl.get('上传'));
const showDownloadProgress = useProgress(intl.get('下载')); const showDownloadProgress = useProgress(intl.get('下载'));
const [visible, setVisible] = useState(false);
const [selectedModules, setSelectedModules] = useState<string[]>(['base']);
const { const {
enable: enableDarkMode, enable: enableDarkMode,
@ -110,7 +126,7 @@ const Other = ({
request request
.put<Blob>( .put<Blob>(
`${config.apiPrefix}system/data/export`, `${config.apiPrefix}system/data/export`,
{}, { type: selectedModules },
{ {
responseType: 'blob', responseType: 'blob',
timeout: 86400000, timeout: 86400000,
@ -127,7 +143,10 @@ const Other = ({
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
}) })
.finally(() => setExportLoading(false)); .finally(() => {
setExportLoading(false);
setVisible(false);
});
}; };
const showReloadModal = () => { const showReloadModal = () => {
@ -178,6 +197,7 @@ const Other = ({
}, []); }, []);
return ( return (
<>
<Form layout="vertical" form={form}> <Form layout="vertical" form={form}>
<Form.Item <Form.Item
label={intl.get('主题')} label={intl.get('主题')}
@ -196,7 +216,10 @@ const Other = ({
> >
{intl.get('亮色')} {intl.get('亮色')}
</Radio.Button> </Radio.Button>
<Radio.Button value="dark" style={{ width: 66, textAlign: 'center' }}> <Radio.Button
value="dark"
style={{ width: 66, textAlign: 'center' }}
>
{intl.get('暗色')} {intl.get('暗色')}
</Radio.Button> </Radio.Button>
<Radio.Button <Radio.Button
@ -296,7 +319,14 @@ const Other = ({
/> />
</Form.Item> </Form.Item>
<Form.Item label={intl.get('数据备份还原')} name="frequency"> <Form.Item label={intl.get('数据备份还原')} name="frequency">
<Button type="primary" onClick={exportData} loading={exportLoading}> <Button
type="primary"
onClick={() => {
setSelectedModules(['base']);
setVisible(true);
}}
loading={exportLoading}
>
{exportLoading ? intl.get('生成数据中...') : intl.get('备份')} {exportLoading ? intl.get('生成数据中...') : intl.get('备份')}
</Button> </Button>
<Upload <Upload
@ -332,6 +362,40 @@ const Other = ({
<CheckUpdate systemInfo={systemInfo} /> <CheckUpdate systemInfo={systemInfo} />
</Form.Item> </Form.Item>
</Form> </Form>
<Modal
title={intl.get('选择备份模块')}
open={visible}
onOk={exportData}
onCancel={() => setVisible(false)}
okText={intl.get('开始备份')}
cancelText={intl.get('取消')}
okButtonProps={{ loading: exportLoading }} // 绑定加载状态到按钮
>
<Checkbox.Group
value={selectedModules}
onChange={(v) => {
setSelectedModules(v as string[]);
}}
style={{
width: '100%',
display: 'flex',
flexWrap: 'wrap',
gap: '8px 16px',
}}
>
{exportModules.map((module) => (
<Checkbox
key={module.value}
value={module.value}
disabled={module.disabled}
style={{ marginLeft: 0 }}
>
{module.label}
</Checkbox>
))}
</Checkbox.Group>
</Modal>
</>
); );
}; };