脚本管理支持新增文件

This commit is contained in:
hanhh 2021-09-19 21:23:03 +08:00
parent 646443deb5
commit 0baf0d23ae
9 changed files with 234 additions and 54 deletions

View File

@ -23,7 +23,8 @@ export default (app: Router) => {
return !fs.lstatSync(config.scriptPath + x).isDirectory(); return !fs.lstatSync(config.scriptPath + x).isDirectory();
}) })
.map((x) => { .map((x) => {
return { title: x, value: x, key: x }; const statObj = fs.statSync(config.scriptPath + x);
return { title: x, value: x, key: x, mtime: statObj.mtimeMs };
}), }),
}); });
} catch (e) { } catch (e) {
@ -54,34 +55,46 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().required(), path: Joi.string().allow(''),
content: Joi.string().required(), content: Joi.string().allow(''),
originFilename: Joi.string().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path, content } = req.body as { let { filename, path, content, originFilename } = req.body as {
filename: string; filename: string;
path: string; path: string;
content: string; content: string;
originFilename: string;
}; };
if (!path) {
path = config.scriptPath;
}
if (!path.endsWith('/')) { if (!path.endsWith('/')) {
path += '/'; path += '/';
} }
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
code: 400, code: 430,
data: '文件路径错误,可保存目录/ql/scripts、/ql/config、/ql/jbot、/ql/bak', data: '文件路径禁止访问',
}); });
} }
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`; const filePath = `${path}${filename.replace(/\//g, '')}`;
const bakPath = '/ql/bak'; if (fs.existsSync(originFilePath)) {
if (fs.existsSync(filePath)) { if (!fs.existsSync(config.bakPath)) {
if (!fs.existsSync(bakPath)) { fs.mkdirSync(config.bakPath);
fs.mkdirSync(bakPath); }
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
} }
fs.copyFileSync(filePath, bakPath);
} }
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
@ -139,4 +152,35 @@ export default (app: Router) => {
} }
}, },
); );
route.post(
'/scripts/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename } = req.body as {
filename: string;
};
const filePath = `${config.scriptPath}${filename}`;
// const stats = fs.statSync(filePath);
// res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
// 'Content-Length': stats.size //文件大小
// });
// fs.createReadStream(filePath).pipe(res);
return res.download(filePath, filename, (err) => {
return next(err);
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };

View File

@ -5,7 +5,7 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const envFound = dotenv.config(); const envFound = dotenv.config();
const rootPath = path.resolve(__dirname, '../../'); const rootPath = process.cwd();
const envFile = path.join(rootPath, 'config/env.sh'); const envFile = path.join(rootPath, 'config/env.sh');
const confFile = path.join(rootPath, 'config/config.sh'); const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sample.sh'); const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
@ -15,6 +15,7 @@ const authConfigFile = path.join(rootPath, 'config/auth.json');
const extraFile = path.join(rootPath, 'config/extra.sh'); const extraFile = path.join(rootPath, 'config/extra.sh');
const configPath = path.join(rootPath, 'config/'); const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/'); const scriptPath = path.join(rootPath, 'scripts/');
const bakPath = path.join(rootPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const logPath = path.join(rootPath, 'log/'); const logPath = path.join(rootPath, 'log/');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
@ -46,6 +47,7 @@ export default {
api: { api: {
prefix: '/api', prefix: '/api',
}, },
rootPath,
configString, configString,
loginFaild, loginFaild,
authError, authError,
@ -72,5 +74,6 @@ export default {
'crontab.list', 'crontab.list',
'env.sh', 'env.sh',
], ],
writePathList: ['/ql/scripts/', '/ql/config/', '/ql/jbot/', '/ql/bak/'], writePathList: [configPath, scriptPath],
bakPath,
}; };

1
bak/bv.log Normal file
View File

@ -0,0 +1 @@
123123123

8
bak/bv3.log Normal file
View File

@ -0,0 +1,8 @@
123123123sdfasdfdjflsdjflad
所肩负的拉三季度福利静安寺两地分居阿临时冻结发了;阿克苏剪短发拉加快速度联发科;就的说法
阿斯顿发斯蒂芬是打发斯蒂芬阿斯顿发斯蒂芬
// 阿斯顿发双方都

View File

@ -41,7 +41,7 @@ function getFilterData(keyword: string, data: any) {
const Log = ({ headerStyle, isPhone, theme }: any) => { const Log = ({ headerStyle, isPhone, theme }: any) => {
const [title, setTitle] = useState('请选择日志文件'); const [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState(); const [select, setSelect] = useState<any>();
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]); const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -84,6 +84,9 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === select || !value) {
return;
}
setValue('加载中...'); setValue('加载中...');
setSelect(value); setSelect(value);
setTitle(node.parent || node.value); setTitle(node.parent || node.value);
@ -147,6 +150,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
height={height} height={height}
selectedKeys={[select]}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
></Tree> ></Tree>

View File

@ -41,7 +41,6 @@ const EditModal = ({
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [isNewFile, setIsNewFile] = useState<boolean>(false);
const [log, setLog] = useState<string>(''); const [log, setLog] = useState<string>('');
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
@ -54,7 +53,6 @@ const EditModal = ({
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setFileName(value); setFileName(value);
setLanguage(newMode); setLanguage(newMode);
setIsNewFile(false);
getDetail(node); getDetail(node);
}; };
@ -64,23 +62,14 @@ const EditModal = ({
}); });
}; };
const createFile = () => {
setFileName(`未命名${prefixMap[language]}`);
setIsNewFile(true);
setValue('');
};
const run = () => {}; const run = () => {};
useEffect(() => { useEffect(() => {
if (!currentFile) { if (currentFile) {
createFile();
} else {
setFileName(currentFile); setFileName(currentFile);
setValue(content as string); setValue(content as string);
} }
setIsNewFile(!currentFile); }, [currentFile, content]);
}, []);
return ( return (
<Drawer <Drawer
@ -131,13 +120,6 @@ const EditModal = ({
> >
</Button> </Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={createFile}
>
</Button>
<Button <Button
type="primary" type="primary"
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
@ -178,8 +160,12 @@ const EditModal = ({
handleCancel={() => { handleCancel={() => {
setSaveModalVisible(false); setSaveModalVisible(false);
}} }}
isNewFile={isNewFile} file={{
file={{ content: editorRef.current && editorRef.current.getValue().replace(/\r\n/g, '\n'), filename: fileName }} content:
editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName,
}}
/> />
<SettingModal <SettingModal
visible={settingModalVisible} visible={settingModalVisible}

View File

@ -0,0 +1,68 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const EditScriptNameModal = ({
handleCancel,
visible,
}: {
visible: boolean;
handleCancel: (file: { filename: string }) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
request
.post(`${config.apiPrefix}scripts`, {
data: { filename: values.filename, content: '' },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('保存文件成功');
handleCancel({ filename: values.filename });
} else {
message.error(data);
}
setLoading(false);
})
.finally(() => setLoading(false));
};
useEffect(() => {
form.resetFields();
}, [visible]);
return (
<Modal
title="新建文件"
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="filename"
rules={[{ required: true, message: '请输入文件名' }]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
</Form>
</Modal>
);
};
export default EditScriptNameModal;

View File

@ -7,6 +7,7 @@ import {
Modal, Modal,
message, message,
Typography, Typography,
Tooltip,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@ -16,6 +17,16 @@ import styles from './index.module.less';
import EditModal from './editModal'; import EditModal from './editModal';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
FormOutlined,
PlusOutlined,
PlusSquareOutlined,
SearchOutlined,
} from '@ant-design/icons';
import EditScriptNameModal from './editNameModal';
const { Text } = Typography; const { Text } = Typography;
@ -53,15 +64,17 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const getScripts = () => { const getScripts = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}scripts/files`) .get(`${config.apiPrefix}scripts/files`)
.then((data) => { .then((data) => {
setData(data.data); const sortData = data.data.sort((a: any, b: any) => b.mtime - a.mtime);
setFilterData(data.data); setData(sortData);
onSelect(data.data[0].value, data.data[0]); setFilterData(sortData);
onSelect(sortData[0].value, sortData[0]);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@ -102,6 +115,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
}, },
}); });
} else { } else {
setIsEditing(false);
onSelect(keys[0], e.node); onSelect(keys[0], e.node);
} }
}, },
@ -207,6 +221,42 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
}); });
}; };
const addFile = () => {
setIsAddFileModalVisible(true);
};
const addFileModalClose = (
{ filename }: { filename: string } = { filename: '' },
) => {
if (filename) {
const newData = [...data];
const _file = { title: filename, key: filename, value: filename };
newData.unshift(_file);
setData(newData);
onSelect(_file.value, _file);
}
setIsAddFileModalVisible(false);
};
const downloadFile = () => {
request
.post(`${config.apiPrefix}scripts/download`, {
data: {
filename: select,
},
})
.then((_data: any) => {
const blob = new Blob([_data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = select;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
});
};
useEffect(() => { useEffect(() => {
const word = searchValue || ''; const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data); const { tree } = getFilterData(word.toLocaleLowerCase(), data);
@ -255,12 +305,30 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
</Button>, </Button>,
] ]
: [ : [
<Button type="primary" onClick={editFile}> <Tooltip title="新建">
<Button
</Button>, type="primary"
<Button type="primary" onClick={deleteFile}> onClick={addFile}
icon={<PlusOutlined />}
</Button>, />
</Tooltip>,
<Tooltip title="编辑">
<Button
type="primary"
onClick={editFile}
icon={<EditOutlined />}
/>
</Tooltip>,
<Tooltip title="删除">
<Button
type="primary"
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
// <Tooltip title="下载">
// <Button type="primary" onClick={downloadFile} icon={<DownloadOutlined />} />
// </Tooltip>,
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
@ -338,6 +406,10 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
/> />
<EditScriptNameModal
visible={isAddFileModalVisible}
handleCancel={addFileModalClose}
/>
</div> </div>
</PageContainer> </PageContainer>
); );

View File

@ -7,19 +7,17 @@ const SaveModal = ({
file, file,
handleCancel, handleCancel,
visible, visible,
isNewFile,
}: { }: {
file?: any; file?: any;
visible: boolean; visible: boolean;
handleCancel: (cks?: any[]) => void; handleCancel: (cks?: any[]) => void;
isNewFile: boolean;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const payload = { ...file, ...values }; const payload = { ...file, ...values, originFilename: file.filename };
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: payload, data: payload,
@ -71,12 +69,8 @@ const SaveModal = ({
> >
<Input placeholder="请输入文件名" /> <Input placeholder="请输入文件名" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item name="path" label="保存目录">
name="path" <Input placeholder="请输入保存目录默认scripts目录" />
label="保存目录"
rules={[{ required: true, message: '请输入保存目录' }]}
>
<Input placeholder="请输入保存目录" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>