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

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(
'/data/export',
celebrate({
body: Joi.object({
type: Joi.array().items(Joi.string()).optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.exportData(res);
await systemService.exportData(res, req.body.type);
} catch (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 sshdPath = path.join(dataPath, 'ssh.d/');
const systemLogPath = path.join(dataPath, 'syslog/');
const dependenceCachePath = path.join(dataPath, 'dep_cache/');
const envFile = path.join(preloadPath, 'env.sh');
const jsEnvFile = path.join(preloadPath, 'env.js');
@ -174,4 +175,5 @@ export default {
sqliteFile,
sshdPath,
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 {
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(
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile} data/`,
`cd ${config.dataPath} && cd ../ && tar -zcvf ${
config.dataTgzFile
} ${dataPaths.join(' ')}`,
);
res.download(config.dataTgzFile);
} catch (error: any) {
@ -503,4 +510,15 @@ export default class SystemService {
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",
"确认离开": "Confirm 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 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 { request } from '@/utils/http';
import './index.less';
@ -25,6 +25,7 @@ const Dependence = () => {
const [form] = Form.useForm();
const [log, setLog] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [cleanType, setCleanType] = useState<string>('node');
const getSystemConfig = () => {
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(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('updateNodeMirror', handleMessage);
@ -222,6 +241,38 @@ const Dependence = () => {
</Button>
</Input.Group>
</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>
<pre
style={{

View File

@ -10,6 +10,7 @@ import {
Upload,
Modal,
Select,
Checkbox,
} from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config';
@ -31,6 +32,19 @@ const dataMap = {
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 = ({
systemInfo,
reloadTheme,
@ -45,6 +59,8 @@ const Other = ({
const [exportLoading, setExportLoading] = useState(false);
const showUploadProgress = useProgress(intl.get('上传'));
const showDownloadProgress = useProgress(intl.get('下载'));
const [visible, setVisible] = useState(false);
const [selectedModules, setSelectedModules] = useState<string[]>(['base']);
const {
enable: enableDarkMode,
@ -110,7 +126,7 @@ const Other = ({
request
.put<Blob>(
`${config.apiPrefix}system/data/export`,
{},
{ type: selectedModules },
{
responseType: 'blob',
timeout: 86400000,
@ -127,7 +143,10 @@ const Other = ({
.catch((error: any) => {
console.log(error);
})
.finally(() => setExportLoading(false));
.finally(() => {
setExportLoading(false);
setVisible(false);
});
};
const showReloadModal = () => {
@ -178,160 +197,205 @@ const Other = ({
}, []);
return (
<Form layout="vertical" form={form}>
<Form.Item
label={intl.get('主题')}
name="theme"
initialValue={defaultTheme}
>
<Radio.Group
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
<>
<Form layout="vertical" form={form}>
<Form.Item
label={intl.get('主题')}
name="theme"
initialValue={defaultTheme}
>
<Radio.Button
value="light"
style={{ width: 70, textAlign: 'center' }}
<Radio.Group
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
>
{intl.get('亮色')}
</Radio.Button>
<Radio.Button value="dark" style={{ width: 66, textAlign: 'center' }}>
{intl.get('暗色')}
</Radio.Button>
<Radio.Button
value="auto"
style={{ width: 129, textAlign: 'center' }}
>
{intl.get('跟随系统')}
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
label={intl.get('日志删除频率')}
name="frequency"
tooltip={intl.get('每x天自动删除x天以前的日志')}
>
<Input.Group compact>
<InputNumber
addonBefore={intl.get('每')}
addonAfter={intl.get('天')}
style={{ width: 180 }}
min={0}
value={systemConfig?.logRemoveFrequency}
onChange={(value) => {
setSystemConfig({ ...systemConfig, logRemoveFrequency: value });
}}
/>
<Button
type="primary"
onClick={() => {
updateSystemConfig('log-remove-frequency');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item label={intl.get('定时任务并发数')} name="frequency">
<Input.Group compact>
<InputNumber
style={{ width: 180 }}
min={1}
value={systemConfig?.cronConcurrency}
onChange={(value) => {
setSystemConfig({ ...systemConfig, cronConcurrency: value });
}}
/>
<Button
type="primary"
onClick={() => {
updateSystemConfig('cron-concurrency');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item label={intl.get('时区')} name="timezone">
<Input.Group compact>
<Radio.Button
value="light"
style={{ width: 70, textAlign: 'center' }}
>
{intl.get('亮色')}
</Radio.Button>
<Radio.Button
value="dark"
style={{ width: 66, textAlign: 'center' }}
>
{intl.get('暗色')}
</Radio.Button>
<Radio.Button
value="auto"
style={{ width: 129, textAlign: 'center' }}
>
{intl.get('跟随系统')}
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
label={intl.get('日志删除频率')}
name="frequency"
tooltip={intl.get('每x天自动删除x天以前的日志')}
>
<Input.Group compact>
<InputNumber
addonBefore={intl.get('每')}
addonAfter={intl.get('天')}
style={{ width: 180 }}
min={0}
value={systemConfig?.logRemoveFrequency}
onChange={(value) => {
setSystemConfig({ ...systemConfig, logRemoveFrequency: value });
}}
/>
<Button
type="primary"
onClick={() => {
updateSystemConfig('log-remove-frequency');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item label={intl.get('定时任务并发数')} name="frequency">
<Input.Group compact>
<InputNumber
style={{ width: 180 }}
min={1}
value={systemConfig?.cronConcurrency}
onChange={(value) => {
setSystemConfig({ ...systemConfig, cronConcurrency: value });
}}
/>
<Button
type="primary"
onClick={() => {
updateSystemConfig('cron-concurrency');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item label={intl.get('时区')} name="timezone">
<Input.Group compact>
<Select
value={systemConfig?.timezone}
style={{ width: 180 }}
onChange={(value) => {
setSystemConfig({ ...systemConfig, timezone: value });
}}
options={TIMEZONES.map((timezone) => ({
value: timezone,
label: timezone,
}))}
showSearch
filterOption={(input, option) =>
option?.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
/>
<Button
type="primary"
onClick={() => {
updateSystemConfig('timezone');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item label={intl.get('语言')} name="lang">
<Select
value={systemConfig?.timezone}
style={{ width: 180 }}
onChange={(value) => {
setSystemConfig({ ...systemConfig, timezone: value });
}}
options={TIMEZONES.map((timezone) => ({
value: timezone,
label: timezone,
}))}
showSearch
filterOption={(input, option) =>
option?.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
defaultValue={localStorage.getItem('lang') || ''}
style={{ width: 264 }}
onChange={handleLangChange}
options={[
{ value: '', label: intl.get('跟随系统') },
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: 'English' },
]}
/>
</Form.Item>
<Form.Item label={intl.get('数据备份还原')} name="frequency">
<Button
type="primary"
onClick={() => {
updateSystemConfig('timezone');
setSelectedModules(['base']);
setVisible(true);
}}
style={{ width: 84 }}
loading={exportLoading}
>
{intl.get('确认')}
{exportLoading ? intl.get('生成数据中...') : intl.get('备份')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item label={intl.get('语言')} name="lang">
<Select
defaultValue={localStorage.getItem('lang') || ''}
style={{ width: 264 }}
onChange={handleLangChange}
options={[
{ value: '', label: intl.get('跟随系统') },
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: 'English' },
]}
/>
</Form.Item>
<Form.Item label={intl.get('数据备份还原')} name="frequency">
<Button type="primary" onClick={exportData} loading={exportLoading}>
{exportLoading ? intl.get('生成数据中...') : intl.get('备份')}
</Button>
<Upload
method="put"
showUploadList={false}
maxCount={1}
action={`${config.apiPrefix}system/data/import`}
onChange={({ file, event }) => {
if (event?.percent) {
showUploadProgress(
Math.min(parseFloat(event?.percent.toFixed(1)), 99),
);
}
if (file.status === 'done') {
showUploadProgress(100);
showReloadModal();
}
if (file.status === 'error') {
message.error('上传失败');
}
<Upload
method="put"
showUploadList={false}
maxCount={1}
action={`${config.apiPrefix}system/data/import`}
onChange={({ file, event }) => {
if (event?.percent) {
showUploadProgress(
Math.min(parseFloat(event?.percent.toFixed(1)), 99),
);
}
if (file.status === 'done') {
showUploadProgress(100);
showReloadModal();
}
if (file.status === 'error') {
message.error('上传失败');
}
}}
name="data"
headers={{
Authorization: `Bearer ${localStorage.getItem(config.authKey)}`,
}}
>
<Button icon={<UploadOutlined />} style={{ marginLeft: 8 }}>
{intl.get('还原数据')}
</Button>
</Upload>
</Form.Item>
<Form.Item label={intl.get('检查更新')} name="update">
<CheckUpdate systemInfo={systemInfo} />
</Form.Item>
</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[]);
}}
name="data"
headers={{
Authorization: `Bearer ${localStorage.getItem(config.authKey)}`,
style={{
width: '100%',
display: 'flex',
flexWrap: 'wrap',
gap: '8px 16px',
}}
>
<Button icon={<UploadOutlined />} style={{ marginLeft: 8 }}>
{intl.get('还原数据')}
</Button>
</Upload>
</Form.Item>
<Form.Item label={intl.get('检查更新')} name="update">
<CheckUpdate systemInfo={systemInfo} />
</Form.Item>
</Form>
{exportModules.map((module) => (
<Checkbox
key={module.value}
value={module.value}
disabled={module.disabled}
style={{ marginLeft: 0 }}
>
{module.label}
</Checkbox>
))}
</Checkbox.Group>
</Modal>
</>
);
};