mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-22 22:36:06 +08:00
脚本管理和日志管理支持下载
This commit is contained in:
parent
118c92d9e5
commit
cf94ecfb11
|
@ -1,10 +1,15 @@
|
|||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import { getFileContentByName, readDirs, removeAnsi, rmPath } from '../config/util';
|
||||
import { join, resolve } from 'path';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import {
|
||||
getFileContentByName,
|
||||
readDirs,
|
||||
removeAnsi,
|
||||
rmPath,
|
||||
} from '../config/util';
|
||||
import LogService from '../services/log';
|
||||
const route = Router();
|
||||
const blacklist = ['.tmp'];
|
||||
|
||||
|
@ -29,17 +34,16 @@ export default (app: Router) => {
|
|||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const finalPath = resolve(
|
||||
config.logPath,
|
||||
const logService = Container.get(LogService);
|
||||
const finalPath = logService.checkFilePath(
|
||||
(req.query.path as string) || '',
|
||||
(req.query.file as string) || '',
|
||||
);
|
||||
|
||||
if (
|
||||
blacklist.includes(req.query.path as string) ||
|
||||
!finalPath.startsWith(config.logPath)
|
||||
) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
const content = await getFileContentByName(finalPath);
|
||||
res.send({ code: 200, data: removeAnsi(content) });
|
||||
|
@ -53,16 +57,16 @@ export default (app: Router) => {
|
|||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const finalPath = resolve(
|
||||
config.logPath,
|
||||
const logService = Container.get(LogService);
|
||||
const finalPath = logService.checkFilePath(
|
||||
(req.query.path as string) || '',
|
||||
(req.params.file as string) || '',
|
||||
(req.query.file as string) || '',
|
||||
);
|
||||
if (
|
||||
blacklist.includes(req.path) ||
|
||||
!finalPath.startsWith(config.logPath)
|
||||
) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
const content = await getFileContentByName(finalPath);
|
||||
res.send({ code: 200, data: content });
|
||||
|
@ -83,17 +87,56 @@ export default (app: Router) => {
|
|||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.logPath, path, filename);
|
||||
await rmPath(filePath);
|
||||
const logService = Container.get(LogService);
|
||||
const finalPath = logService.checkFilePath(filename, path);
|
||||
if (!finalPath || blacklist.includes(path)) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
await rmPath(finalPath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/download',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const logService = Container.get(LogService);
|
||||
const filePath = logService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
return res.download(filePath, filename, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { fileExist, readDirs, readDir, rmPath } from '../config/util';
|
||||
import { fileExist, readDirs, readDir, rmPath, IFile } from '../config/util';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
|
@ -27,7 +27,7 @@ export default (app: Router) => {
|
|||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let result = [];
|
||||
let result: IFile[] = [];
|
||||
const blacklist = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
|
@ -102,7 +102,6 @@ export default (app: Router) => {
|
|||
'/',
|
||||
upload.single('file'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, content, originFilename, directory } =
|
||||
req.body as {
|
||||
|
@ -124,8 +123,8 @@ export default (app: Router) => {
|
|||
}
|
||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||
return res.send({
|
||||
code: 430,
|
||||
message: '文件路径禁止访问',
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -175,14 +174,20 @@ export default (app: Router) => {
|
|||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body as {
|
||||
filename: string;
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const filePath = scriptService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
await writeFileWithLock(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
|
@ -197,18 +202,22 @@ export default (app: Router) => {
|
|||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const filePath = scriptService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
await rmPath(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
|
@ -222,24 +231,27 @@ export default (app: Router) => {
|
|||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(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);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const filePath = scriptService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
return res.download(filePath, filename, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
|
|
@ -237,7 +237,7 @@ enum FileType {
|
|||
'file',
|
||||
}
|
||||
|
||||
interface IFile {
|
||||
export interface IFile {
|
||||
title: string;
|
||||
key: string;
|
||||
type: 'directory' | 'file';
|
||||
|
|
14
back/services/log.ts
Normal file
14
back/services/log.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import path from 'path';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
|
||||
@Service()
|
||||
export default class LogService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public checkFilePath(filePath: string, fileName: string) {
|
||||
const finalPath = path.resolve(config.logPath, filePath, fileName);
|
||||
return finalPath.startsWith(config.logPath) ? finalPath : '';
|
||||
}
|
||||
}
|
|
@ -64,10 +64,15 @@ export default class ScriptService {
|
|||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
public checkFilePath(filePath: string, fileName: string) {
|
||||
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
||||
return finalPath.startsWith(config.scriptPath) ? finalPath : '';
|
||||
}
|
||||
|
||||
if (!finalPath.startsWith(config.scriptPath)) {
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
const finalPath = this.checkFilePath(filePath, fileName);
|
||||
|
||||
if (!finalPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,6 @@ class GrpcClient {
|
|||
},
|
||||
grpcOptions: {
|
||||
'grpc.enable_http_proxy': 0,
|
||||
'grpc.keepalive_time_ms': 120000,
|
||||
'grpc.keepalive_timeout_ms': 20000,
|
||||
'grpc.max_receive_message_length': 100 * 1024 * 1024,
|
||||
},
|
||||
defaultTimeout: 30000,
|
||||
};
|
||||
|
@ -59,23 +56,12 @@ class GrpcClient {
|
|||
grpc.credentials.createInsecure(),
|
||||
grpcOptions,
|
||||
);
|
||||
|
||||
this.#checkConnection();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize gRPC client:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#checkConnection() {
|
||||
this.#client.waitForReady(Date.now() + 5000, (error) => {
|
||||
if (error) {
|
||||
console.error('gRPC client connection failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#promisifyMethod(methodName) {
|
||||
const capitalizedMethod =
|
||||
methodName.charAt(0).toUpperCase() + methodName.slice(1);
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import intl from 'react-intl-universal';
|
||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { depthFirstSearch } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import { CloudDownloadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { useOutletContext } from '@umijs/max';
|
||||
import {
|
||||
TreeSelect,
|
||||
Tree,
|
||||
Input,
|
||||
Empty,
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { request } from '@/utils/http';
|
||||
import styles from './index.module.less';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { useOutletContext } from '@umijs/max';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { depthFirstSearch } from '@/utils';
|
||||
import { saveAs } from 'file-saver';
|
||||
import debounce from 'lodash/debounce';
|
||||
import uniq from 'lodash/uniq';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { Key, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import styles from './index.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
@ -67,6 +68,21 @@ const Log = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const downloadLog = () => {
|
||||
request
|
||||
.post<Blob>(
|
||||
`${config.apiPrefix}logs/download`,
|
||||
{
|
||||
filename: currentNode.title,
|
||||
path: currentNode.parent || '',
|
||||
},
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
.then((res) => {
|
||||
saveAs(res, currentNode.title);
|
||||
});
|
||||
};
|
||||
|
||||
const onSelect = (value: any, node: any) => {
|
||||
if (node.key === select || !value) {
|
||||
return;
|
||||
|
@ -225,10 +241,18 @@ const Log = () => {
|
|||
/>,
|
||||
]
|
||||
: [
|
||||
<Tooltip title={intl.get('下载')}>
|
||||
<Button
|
||||
disabled={!currentNode || currentNode.type === 'directory'}
|
||||
type="primary"
|
||||
onClick={downloadLog}
|
||||
icon={<CloudDownloadOutlined />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('删除')}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
onClick={deleteFile}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
|
|
|
@ -1,52 +1,48 @@
|
|||
import intl from 'react-intl-universal';
|
||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||
import {
|
||||
TreeSelect,
|
||||
Tree,
|
||||
Input,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Empty,
|
||||
MenuProps,
|
||||
} from 'antd';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { depthFirstSearch, findNode, getEditorMode } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { request } from '@/utils/http';
|
||||
import styles from './index.module.less';
|
||||
import EditModal from './editModal';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { canPreviewInMonaco } from '@/utils/monaco';
|
||||
import {
|
||||
CloudDownloadOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
FormOutlined,
|
||||
PlusOutlined,
|
||||
PlusSquareOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import EditScriptNameModal from './editNameModal';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { history, useOutletContext, useLocation } from '@umijs/max';
|
||||
import { parse } from 'query-string';
|
||||
import { depthFirstSearch, findNode, getEditorMode } from '@/utils';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import uniq from 'lodash/uniq';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import RenameModal from './renameModal';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { langs } from '@uiw/codemirror-extensions-langs';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { history, useOutletContext } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Input,
|
||||
MenuProps,
|
||||
message,
|
||||
Modal,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { saveAs } from 'file-saver';
|
||||
import debounce from 'lodash/debounce';
|
||||
import uniq from 'lodash/uniq';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { canPreviewInMonaco } from '@/utils/monaco';
|
||||
import { parse } from 'query-string';
|
||||
import { Key, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import intl from 'react-intl-universal';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import EditModal from './editModal';
|
||||
import EditScriptNameModal from './editNameModal';
|
||||
import styles from './index.module.less';
|
||||
import RenameModal from './renameModal';
|
||||
const { Text } = Typography;
|
||||
|
||||
const Script = () => {
|
||||
|
@ -100,6 +96,21 @@ const Script = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const downloadScript = () => {
|
||||
request
|
||||
.post<Blob>(
|
||||
`${config.apiPrefix}scripts/download`,
|
||||
{
|
||||
filename: currentNode.title,
|
||||
path: currentNode.parent || '',
|
||||
},
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
.then((res) => {
|
||||
saveAs(res, currentNode.title);
|
||||
});
|
||||
};
|
||||
|
||||
const initGetScript = (_data: any) => {
|
||||
const { p, s } = parse(history.location.search);
|
||||
if (s) {
|
||||
|
@ -482,21 +493,19 @@ const Script = () => {
|
|||
label: intl.get('编辑'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
disabled:
|
||||
!select ||
|
||||
(currentNode && !canPreviewInMonaco(currentNode?.title)),
|
||||
disabled: !currentNode || !canPreviewInMonaco(currentNode?.title),
|
||||
},
|
||||
{
|
||||
label: intl.get('重命名'),
|
||||
key: 'rename',
|
||||
icon: <IconFont type="ql-icon-rename" />,
|
||||
disabled: !select,
|
||||
disabled: !currentNode,
|
||||
},
|
||||
{
|
||||
label: intl.get('删除'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
disabled: !select,
|
||||
disabled: !currentNode,
|
||||
},
|
||||
],
|
||||
onClick: ({ key, domEvent }) => {
|
||||
|
@ -567,8 +576,7 @@ const Script = () => {
|
|||
<Tooltip title={intl.get('编辑')}>
|
||||
<Button
|
||||
disabled={
|
||||
!select ||
|
||||
(currentNode && !canPreviewInMonaco(currentNode?.title))
|
||||
!currentNode || !canPreviewInMonaco(currentNode?.title)
|
||||
}
|
||||
type="primary"
|
||||
onClick={editFile}
|
||||
|
@ -577,16 +585,24 @@ const Script = () => {
|
|||
</Tooltip>,
|
||||
<Tooltip title={intl.get('重命名')}>
|
||||
<Button
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
type="primary"
|
||||
onClick={renameFile}
|
||||
icon={<IconFont type="ql-icon-rename" />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('下载')}>
|
||||
<Button
|
||||
disabled={!currentNode || currentNode.type === 'directory'}
|
||||
type="primary"
|
||||
onClick={downloadScript}
|
||||
icon={<CloudDownloadOutlined />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('删除')}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
onClick={deleteFile}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user