From cf94ecfb11f63c87ccbeb13a9b64fbfd52c28394 Mon Sep 17 00:00:00 2001 From: whyour Date: Thu, 13 Mar 2025 00:22:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86=E6=94=AF=E6=8C=81=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/api/log.ts | 91 ++++++++++++++++++++++-------- back/api/script.ts | 58 +++++++++++-------- back/config/util.ts | 2 +- back/services/log.ts | 14 +++++ back/services/script.ts | 9 ++- shell/preload/client.js | 14 ----- src/pages/log/index.tsx | 62 +++++++++++++------- src/pages/script/index.tsx | 112 +++++++++++++++++++++---------------- 8 files changed, 231 insertions(+), 131 deletions(-) create mode 100644 back/services/log.ts diff --git a/back/api/log.ts b/back/api/log.ts index 0d2068fb..a8d952b1 100644 --- a/back/api/log.ts +++ b/back/api/log.ts @@ -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); + } + }, + ); }; diff --git a/back/api/script.ts b/back/api/script.ts index 721db871..b2662609 100644 --- a/back/api/script.ts +++ b/back/api/script.ts @@ -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) => { - return next(err); + if (err) { + return next(err); + } }); } catch (e) { return next(e); diff --git a/back/config/util.ts b/back/config/util.ts index 47e3c923..7b822403 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -237,7 +237,7 @@ enum FileType { 'file', } -interface IFile { +export interface IFile { title: string; key: string; type: 'directory' | 'file'; diff --git a/back/services/log.ts b/back/services/log.ts new file mode 100644 index 00000000..ab3f16e4 --- /dev/null +++ b/back/services/log.ts @@ -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 : ''; + } +} diff --git a/back/services/script.ts b/back/services/script.ts index d7551084..52921c6c 100644 --- a/back/services/script.ts +++ b/back/services/script.ts @@ -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 ''; } diff --git a/shell/preload/client.js b/shell/preload/client.js index bd154b71..605aa9f4 100644 --- a/shell/preload/client.js +++ b/shell/preload/client.js @@ -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); diff --git a/src/pages/log/index.tsx b/src/pages/log/index.tsx index cab04766..549b0ea3 100644 --- a/src/pages/log/index.tsx +++ b/src/pages/log/index.tsx @@ -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( + `${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 = () => { />, ] : [ + +