脚本管理支持新增文件

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();
})
.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) {
@ -54,34 +55,46 @@ export default (app: Router) => {
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().required(),
content: Joi.string().required(),
path: Joi.string().allow(''),
content: Joi.string().allow(''),
originFilename: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path, content } = req.body as {
let { filename, path, content, originFilename } = req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
};
if (!path) {
path = config.scriptPath;
}
if (!path.endsWith('/')) {
path += '/';
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
code: 400,
data: '文件路径错误,可保存目录/ql/scripts、/ql/config、/ql/jbot、/ql/bak',
code: 430,
data: '文件路径禁止访问',
});
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`;
const bakPath = '/ql/bak';
if (fs.existsSync(filePath)) {
if (!fs.existsSync(bakPath)) {
fs.mkdirSync(bakPath);
if (fs.existsSync(originFilePath)) {
if (!fs.existsSync(config.bakPath)) {
fs.mkdirSync(config.bakPath);
}
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
}
fs.copyFileSync(filePath, bakPath);
}
fs.writeFileSync(filePath, content);
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';
const envFound = dotenv.config();
const rootPath = path.resolve(__dirname, '../../');
const rootPath = process.cwd();
const envFile = path.join(rootPath, 'config/env.sh');
const confFile = path.join(rootPath, 'config/config.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 configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const bakPath = path.join(rootPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/');
const logPath = path.join(rootPath, 'log/');
const authError = '错误的用户名密码,请重试';
@ -46,6 +47,7 @@ export default {
api: {
prefix: '/api',
},
rootPath,
configString,
loginFaild,
authError,
@ -72,5 +74,6 @@ export default {
'crontab.list',
'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 [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState();
const [select, setSelect] = useState<any>();
const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
@ -84,6 +84,9 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
};
const onSelect = (value: any, node: any) => {
if (node.key === select || !value) {
return;
}
setValue('加载中...');
setSelect(value);
setTitle(node.parent || node.value);
@ -147,6 +150,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
treeData={filterData}
showIcon={true}
height={height}
selectedKeys={[select]}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
></Tree>

View File

@ -41,7 +41,6 @@ const EditModal = ({
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false);
const [isNewFile, setIsNewFile] = useState<boolean>(false);
const [log, setLog] = useState<string>('');
const { theme } = useTheme();
const editorRef = useRef<any>(null);
@ -54,7 +53,6 @@ const EditModal = ({
const newMode = LangMap[value.slice(-3)] || '';
setFileName(value);
setLanguage(newMode);
setIsNewFile(false);
getDetail(node);
};
@ -64,23 +62,14 @@ const EditModal = ({
});
};
const createFile = () => {
setFileName(`未命名${prefixMap[language]}`);
setIsNewFile(true);
setValue('');
};
const run = () => {};
useEffect(() => {
if (!currentFile) {
createFile();
} else {
if (currentFile) {
setFileName(currentFile);
setValue(content as string);
}
setIsNewFile(!currentFile);
}, []);
}, [currentFile, content]);
return (
<Drawer
@ -131,13 +120,6 @@ const EditModal = ({
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={createFile}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
@ -178,8 +160,12 @@ const EditModal = ({
handleCancel={() => {
setSaveModalVisible(false);
}}
isNewFile={isNewFile}
file={{ content: editorRef.current && editorRef.current.getValue().replace(/\r\n/g, '\n'), filename: fileName }}
file={{
content:
editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName,
}}
/>
<SettingModal
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,
message,
Typography,
Tooltip,
} from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@ -16,6 +17,16 @@ import styles from './index.module.less';
import EditModal from './editModal';
import { Controlled as CodeMirror } from 'react-codemirror2';
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;
@ -53,15 +64,17 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const [searchValue, setSearchValue] = useState('');
const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const getScripts = () => {
setLoading(true);
request
.get(`${config.apiPrefix}scripts/files`)
.then((data) => {
setData(data.data);
setFilterData(data.data);
onSelect(data.data[0].value, data.data[0]);
const sortData = data.data.sort((a: any, b: any) => b.mtime - a.mtime);
setData(sortData);
setFilterData(sortData);
onSelect(sortData[0].value, sortData[0]);
})
.finally(() => setLoading(false));
};
@ -102,6 +115,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
},
});
} else {
setIsEditing(false);
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(() => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
@ -255,12 +305,30 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
</Button>,
]
: [
<Button type="primary" onClick={editFile}>
</Button>,
<Button type="primary" onClick={deleteFile}>
</Button>,
<Tooltip title="新建">
<Button
type="primary"
onClick={addFile}
icon={<PlusOutlined />}
/>
</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
type="primary"
onClick={() => {
@ -338,6 +406,10 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
setIsLogModalVisible(false);
}}
/>
<EditScriptNameModal
visible={isAddFileModalVisible}
handleCancel={addFileModalClose}
/>
</div>
</PageContainer>
);

View File

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