优化接入功能&重构接入文件操作
This commit is contained in:
		| @ -11,6 +11,7 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"next-terminal/pkg/global" | ||||
| 	"next-terminal/pkg/guacd" | ||||
| 	"next-terminal/pkg/model" | ||||
| 	"next-terminal/pkg/utils" | ||||
| 	"os" | ||||
| @ -18,7 +19,6 @@ import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func SessionPagingEndpoint(c echo.Context) error { | ||||
| @ -40,10 +40,10 @@ func SessionPagingEndpoint(c echo.Context) error { | ||||
| 		if status == model.Disconnected && len(items[i].Recording) > 0 { | ||||
|  | ||||
| 			var recording string | ||||
| 			if items[i].Protocol == "rdp" || items[i].Protocol == "vnc" { | ||||
| 				recording = items[i].Recording + "/recording" | ||||
| 			} else { | ||||
| 			if items[i].Mode == model.Naive { | ||||
| 				recording = items[i].Recording | ||||
| 			} else { | ||||
| 				recording = items[i].Recording + "/recording" | ||||
| 			} | ||||
|  | ||||
| 			if utils.FileExists(recording) { | ||||
| @ -100,7 +100,7 @@ func SessionDisconnectEndpoint(c echo.Context) error { | ||||
|  | ||||
| 	split := strings.Split(sessionIds, ",") | ||||
| 	for i := range split { | ||||
| 		CloseSessionById(split[i], ForcedDisconnect, "强制断开") | ||||
| 		CloseSessionById(split[i], ForcedDisconnect, "forced disconnect") | ||||
| 	} | ||||
| 	return Success(c, nil) | ||||
| } | ||||
| @ -154,15 +154,11 @@ func CloseWebSocket(ws *websocket.Conn, c int, t string) { | ||||
| 	if ws == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	ws.SetCloseHandler(func(code int, text string) error { | ||||
| 		var message []byte | ||||
| 		if code != websocket.CloseNoStatusReceived { | ||||
| 			message = websocket.FormatCloseMessage(c, t) | ||||
| 		} | ||||
| 		_ = ws.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second)) | ||||
| 		return nil | ||||
| 	}) | ||||
| 	defer ws.Close() | ||||
| 	err := guacd.NewInstruction("error", "", strconv.Itoa(c)) | ||||
| 	_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String())) | ||||
| 	disconnect := guacd.NewInstruction("disconnect") | ||||
| 	_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String())) | ||||
| 	//defer ws.Close() | ||||
| } | ||||
|  | ||||
| func SessionResizeEndpoint(c echo.Context) error { | ||||
| @ -186,6 +182,14 @@ func SessionResizeEndpoint(c echo.Context) error { | ||||
|  | ||||
| func SessionCreateEndpoint(c echo.Context) error { | ||||
| 	assetId := c.QueryParam("assetId") | ||||
| 	mode := c.QueryParam("mode") | ||||
|  | ||||
| 	if mode == model.Naive { | ||||
| 		mode = model.Naive | ||||
| 	} else { | ||||
| 		mode = model.Guacd | ||||
| 	} | ||||
|  | ||||
| 	user, _ := GetCurrentAccount(c) | ||||
|  | ||||
| 	if model.TypeUser == user.Type { | ||||
| @ -218,6 +222,7 @@ func SessionCreateEndpoint(c echo.Context) error { | ||||
| 		Status:     model.NoConnect, | ||||
| 		Creator:    user.ID, | ||||
| 		ClientIP:   c.RealIP(), | ||||
| 		Mode:       mode, | ||||
| 	} | ||||
|  | ||||
| 	if asset.AccountType == "credential" { | ||||
| @ -356,6 +361,8 @@ type File struct { | ||||
| 	IsDir   bool           `json:"isDir"` | ||||
| 	Mode    string         `json:"mode"` | ||||
| 	IsLink  bool           `json:"isLink"` | ||||
| 	ModTime utils.JsonTime `json:"modTime"` | ||||
| 	Size    int64          `json:"size"` | ||||
| } | ||||
|  | ||||
| func SessionLsEndpoint(c echo.Context) error { | ||||
| @ -387,12 +394,20 @@ func SessionLsEndpoint(c echo.Context) error { | ||||
|  | ||||
| 		var files = make([]File, 0) | ||||
| 		for i := range fileInfos { | ||||
|  | ||||
| 			// 忽略因此文件 | ||||
| 			if strings.HasPrefix(fileInfos[i].Name(), ".") { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			file := File{ | ||||
| 				Name:    fileInfos[i].Name(), | ||||
| 				Path:    path.Join(remoteDir, fileInfos[i].Name()), | ||||
| 				IsDir:   fileInfos[i].IsDir(), | ||||
| 				Mode:    fileInfos[i].Mode().String(), | ||||
| 				IsLink:  fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink, | ||||
| 				ModTime: utils.NewJsonTime(fileInfos[i].ModTime()), | ||||
| 				Size:    fileInfos[i].Size(), | ||||
| 			} | ||||
|  | ||||
| 			files = append(files, file) | ||||
| @ -417,6 +432,8 @@ func SessionLsEndpoint(c echo.Context) error { | ||||
| 				IsDir:   fileInfos[i].IsDir(), | ||||
| 				Mode:    fileInfos[i].Mode().String(), | ||||
| 				IsLink:  fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink, | ||||
| 				ModTime: utils.NewJsonTime(fileInfos[i].ModTime()), | ||||
| 				Size:    fileInfos[i].Size(), | ||||
| 			} | ||||
|  | ||||
| 			files = append(files, file) | ||||
| @ -539,10 +556,10 @@ func SessionRecordingEndpoint(c echo.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	var recording string | ||||
| 	if session.Protocol == "rdp" || session.Protocol == "vnc" { | ||||
| 		recording = session.Recording + "/recording" | ||||
| 	} else { | ||||
| 	if session.Mode == model.Naive { | ||||
| 		recording = session.Recording | ||||
| 	} else { | ||||
| 		recording = session.Recording + "/recording" | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording)) | ||||
|  | ||||
| @ -15,10 +15,9 @@ import ( | ||||
| const ( | ||||
| 	TunnelClosed     int = -1 | ||||
| 	Normal           int = 0 | ||||
| 	NotFoundSession    int = 2000 | ||||
| 	NewTunnelError     int = 2001 | ||||
| 	NewSftpClientError int = 2002 | ||||
| 	ForcedDisconnect   int = 2003 | ||||
| 	NotFoundSession  int = 800 | ||||
| 	NewTunnelError   int = 801 | ||||
| 	ForcedDisconnect int = 802 | ||||
| ) | ||||
|  | ||||
| func TunEndpoint(c echo.Context) error { | ||||
| @ -145,7 +144,7 @@ func TunEndpoint(c echo.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	tun := global.Tun{ | ||||
| 		Protocol:  configuration.Protocol, | ||||
| 		Protocol:  session.Protocol, | ||||
| 		Tunnel:    tunnel, | ||||
| 		WebSocket: ws, | ||||
| 	} | ||||
|  | ||||
| @ -20,10 +20,15 @@ func (r *Tun) Close() { | ||||
| 	if r.Protocol == "rdp" || r.Protocol == "vnc" { | ||||
| 		_ = r.Tunnel.Close() | ||||
| 	} else { | ||||
| 		if r.SshClient != nil { | ||||
| 			_ = r.SshClient.Close() | ||||
| 		} | ||||
|  | ||||
| 		if r.SftpClient != nil { | ||||
| 			_ = r.SftpClient.Close() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Observable struct { | ||||
| 	Subject   *Tun | ||||
|  | ||||
| @ -13,6 +13,11 @@ const ( | ||||
| 	Disconnected = "disconnected" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	Guacd = "guacd" | ||||
| 	Naive = "naive" | ||||
| ) | ||||
|  | ||||
| type Session struct { | ||||
| 	ID               string         `gorm:"primary_key" json:"id"` | ||||
| 	Protocol         string         `json:"protocol"` | ||||
| @ -34,6 +39,7 @@ type Session struct { | ||||
| 	Message          string         `json:"message"` | ||||
| 	ConnectedTime    utils.JsonTime `json:"connectedTime"` | ||||
| 	DisconnectedTime utils.JsonTime `json:"disconnectedTime"` | ||||
| 	Mode             string         `json:"mode"` | ||||
| } | ||||
|  | ||||
| func (r *Session) TableName() string { | ||||
| @ -60,6 +66,7 @@ type SessionVo struct { | ||||
| 	CreatorName      string         `json:"creatorName"` | ||||
| 	Code             int            `json:"code"` | ||||
| 	Message          string         `json:"message"` | ||||
| 	Mode             string         `json:"mode"` | ||||
| } | ||||
|  | ||||
| func FindPageSession(pageIndex, pageSize int, status, userId, clientIp, assetId, protocol string) (results []SessionVo, total int64, err error) { | ||||
| @ -69,7 +76,7 @@ func FindPageSession(pageIndex, pageSize int, status, userId, clientIp, assetId, | ||||
|  | ||||
| 	params = append(params, status) | ||||
|  | ||||
| 	itemSql := "SELECT s.id, s.protocol,s.recording, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time,s.code, s.message, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? " | ||||
| 	itemSql := "SELECT s.id,s.mode, s.protocol,s.recording, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time,s.code, s.message, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? " | ||||
| 	countSql := "select count(*) from sessions as s where s.status = ? " | ||||
|  | ||||
| 	if len(userId) > 0 { | ||||
|  | ||||
| @ -40,7 +40,7 @@ import {isEmpty, NT_PACKAGE} from "./utils/utils"; | ||||
| import {isAdmin} from "./service/permission"; | ||||
| import UserGroup from "./components/user/UserGroup"; | ||||
| import LoginLog from "./components/session/LoginLog"; | ||||
| import AccessSSH from "./components/access/AccessSSH"; | ||||
| import AccessNaive from "./components/access/AccessNaive"; | ||||
|  | ||||
| const {Footer, Sider} = Layout; | ||||
|  | ||||
| @ -114,7 +114,7 @@ class App extends Component { | ||||
|  | ||||
|             <Switch> | ||||
|                 <Route path="/access" component={Access}/> | ||||
|                 <Route path="/access-ssh" component={AccessSSH}/> | ||||
|                 <Route path="/access-naive" component={AccessNaive}/> | ||||
|                 <Route path="/login"><Login updateUser={this.updateUser}/></Route> | ||||
|  | ||||
|                 <Route path="/"> | ||||
|  | ||||
| @ -1,50 +1,28 @@ | ||||
| import React, {Component} from 'react'; | ||||
| import Guacamole from 'guacamole-common-js'; | ||||
| import { | ||||
|     Affix, | ||||
|     Alert, | ||||
|     Button, | ||||
|     Card, | ||||
|     Col, | ||||
|     Drawer, | ||||
|     Dropdown, | ||||
|     Form, | ||||
|     Input, | ||||
|     Menu, | ||||
|     message, | ||||
|     Modal, | ||||
|     Row, | ||||
|     Space, | ||||
|     Spin, | ||||
|     Tooltip, | ||||
|     Tree | ||||
| } from 'antd' | ||||
| import {Affix, Button, Col, Drawer, Dropdown, Form, Input, Menu, message, Modal, Row, Space, Tooltip} from 'antd' | ||||
| import qs from "qs"; | ||||
| import request from "../../common/request"; | ||||
| import {server, wsServer} from "../../common/constants"; | ||||
| import {wsServer} from "../../common/constants"; | ||||
| import { | ||||
|     AppstoreTwoTone, | ||||
|     CloudDownloadOutlined, | ||||
|     CloudUploadOutlined, | ||||
|     CopyOutlined, | ||||
|     CopyTwoTone, | ||||
|     DeleteOutlined, | ||||
|     DesktopOutlined, | ||||
|     ExclamationCircleOutlined, | ||||
|     ExpandOutlined, | ||||
|     FileZipOutlined, | ||||
|     FolderAddOutlined, | ||||
|     LoadingOutlined, | ||||
|     ReloadOutlined, | ||||
|     UploadOutlined | ||||
|     ReloadOutlined | ||||
| } from '@ant-design/icons'; | ||||
| import Upload from "antd/es/upload"; | ||||
| import {download, exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils"; | ||||
| import {exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils"; | ||||
| import './Access.css' | ||||
| import Draggable from 'react-draggable'; | ||||
| import FileSystem from "./FileSystem"; | ||||
|  | ||||
| const {TextArea} = Input; | ||||
| const {DirectoryTree} = Tree; | ||||
| const {SubMenu} = Menu; | ||||
|  | ||||
| const STATE_IDLE = 0; | ||||
| const STATE_CONNECTING = 1; | ||||
| @ -53,16 +31,10 @@ const STATE_CONNECTED = 3; | ||||
| const STATE_DISCONNECTING = 4; | ||||
| const STATE_DISCONNECTED = 5; | ||||
|  | ||||
| const antIcon = <LoadingOutlined/>; | ||||
|  | ||||
| const formItemLayout = { | ||||
|     labelCol: {span: 6}, | ||||
|     wrapperCol: {span: 14}, | ||||
| }; | ||||
|  | ||||
| class Access extends Component { | ||||
|  | ||||
|     formRef = React.createRef() | ||||
|     formRef = React.createRef(); | ||||
|     clipboardFormRef = React.createRef(); | ||||
|  | ||||
|     state = { | ||||
|         sessionId: '', | ||||
| @ -73,19 +45,12 @@ class Access extends Component { | ||||
|         containerOverflow: 'hidden', | ||||
|         containerWidth: 0, | ||||
|         containerHeight: 0, | ||||
|         fileSystemVisible: false, | ||||
|         fileSystem: { | ||||
|             loading: false, | ||||
|             object: null, | ||||
|             currentDirectory: '/', | ||||
|             files: [], | ||||
|         }, | ||||
|         uploadAction: '', | ||||
|         uploadHeaders: {}, | ||||
|         keyboard: {}, | ||||
|         protocol: '', | ||||
|         treeData: [], | ||||
|         selectNode: {}, | ||||
|  | ||||
|         confirmVisible: false, | ||||
|         confirmLoading: false, | ||||
|         uploadVisible: false, | ||||
| @ -97,10 +62,11 @@ class Access extends Component { | ||||
|  | ||||
|     async componentDidMount() { | ||||
|  | ||||
|         let params = new URLSearchParams(this.props.location.search); | ||||
|         let assetsId = params.get('assetsId'); | ||||
|         let protocol = params.get('protocol'); | ||||
|         let sessionId = await this.createSession(assetsId); | ||||
|         let urlParams = new URLSearchParams(this.props.location.search); | ||||
|         let assetId = urlParams.get('assetId'); | ||||
|         document.title = urlParams.get('assetName'); | ||||
|         let protocol = urlParams.get('protocol'); | ||||
|         let sessionId = await this.createSession(assetId); | ||||
|         if (isEmpty(sessionId)) { | ||||
|             return; | ||||
|         } | ||||
| @ -150,11 +116,11 @@ class Access extends Component { | ||||
|         }) | ||||
|         if (this.state.protocol === 'ssh') { | ||||
|             if (data.data && data.data.length > 0) { | ||||
|                 // message.success('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。'); | ||||
|                 message.info('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。'); | ||||
|             } | ||||
|         } else { | ||||
|             if (data.data && data.data.length > 0) { | ||||
|                 // message.success('您输入的内容已复制到远程服务器上'); | ||||
|                 message.info('您输入的内容已复制到远程服务器上'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -162,7 +128,7 @@ class Access extends Component { | ||||
|  | ||||
|     onTunnelStateChange = (state) => { | ||||
|         if (state === Guacamole.Tunnel.State.CLOSED) { | ||||
|             this.showMessage('连接已关闭'); | ||||
|             console.log('web socket 已关闭'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
| @ -277,8 +243,14 @@ class Access extends Component { | ||||
|             case 783: | ||||
|                 this.showMessage('错误的请求类型'); | ||||
|                 break; | ||||
|             case 797: | ||||
|                 this.showMessage('客户端连接数量过多'); | ||||
|             case 800: | ||||
|                 this.showMessage('会话不存在'); | ||||
|                 break; | ||||
|             case 801: | ||||
|                 this.showMessage('创建隧道失败'); | ||||
|                 break; | ||||
|             case 802: | ||||
|                 this.showMessage('管理员强制断开了此会话'); | ||||
|                 break; | ||||
|             default: | ||||
|                 this.showMessage('未知错误。'); | ||||
| @ -320,7 +292,7 @@ class Access extends Component { | ||||
|             // Set clipboard contents once stream is finished | ||||
|             reader.onend = async () => { | ||||
|  | ||||
|                 // message.success('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。'); | ||||
|                 message.info('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。'); | ||||
|                 this.setState({ | ||||
|                     clipboardText: data | ||||
|                 }); | ||||
| @ -343,17 +315,6 @@ class Access extends Component { | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     uploadChange = (info) => { | ||||
|         if (info.file.status !== 'uploading') { | ||||
|  | ||||
|         } | ||||
|         if (info.file.status === 'done') { | ||||
|             message.success(`${info.file.name} 文件上传成功。`, 3); | ||||
|         } else if (info.file.status === 'error') { | ||||
|             message.error(`${info.file.name} 文件上传失败。`, 10); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onKeyDown = (keysym) => { | ||||
|         if (true === this.state.clipboardVisible || true === this.state.confirmVisible) { | ||||
|             return true; | ||||
| @ -368,20 +329,6 @@ class Access extends Component { | ||||
|         this.state.client.sendKeyEvent(0, keysym); | ||||
|     }; | ||||
|  | ||||
|     showFileSystem = () => { | ||||
|         this.setState({ | ||||
|             fileSystemVisible: true, | ||||
|         }); | ||||
|  | ||||
|         this.loadDirData('/'); | ||||
|     }; | ||||
|  | ||||
|     hideFileSystem = () => { | ||||
|         this.setState({ | ||||
|             fileSystemVisible: false, | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     fullScreen = () => { | ||||
|         let fs = this.state.fullScreen; | ||||
|         if (fs) { | ||||
| @ -400,43 +347,12 @@ class Access extends Component { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     showClipboard = () => { | ||||
|         this.setState({ | ||||
|             clipboardVisible: true | ||||
|         }, () => { | ||||
|             let element = document.getElementById('clipboard'); | ||||
|             if (element) { | ||||
|                 element.value = this.state.clipboardText; | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     hideClipboard = () => { | ||||
|         this.setState({ | ||||
|             clipboardVisible: false | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     updateClipboardFormTextarea = () => { | ||||
|         let clipboardText = document.getElementById('clipboard').value; | ||||
|  | ||||
|         this.setState({ | ||||
|             clipboardText: clipboardText | ||||
|         }); | ||||
|  | ||||
|         this.sendClipboard({ | ||||
|             'data': clipboardText, | ||||
|             'type': 'text/plain' | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     async createSession(assetsId) { | ||||
|         let result = await request.post(`/sessions?assetId=${assetsId}`); | ||||
|         let result = await request.post(`/sessions?assetId=${assetsId}&mode=guacd`); | ||||
|         if (result['code'] !== 1) { | ||||
|             this.showMessage(result['message']); | ||||
|             return null; | ||||
|         } | ||||
|         document.title = result['data']['name']; | ||||
|         return result['data']['id']; | ||||
|     } | ||||
|  | ||||
| @ -516,23 +432,6 @@ class Access extends Component { | ||||
|         keyboard.onkeydown = this.onKeyDown; | ||||
|         keyboard.onkeyup = this.onKeyUp; | ||||
|  | ||||
|         let stateChecker = setInterval(async () => { | ||||
|             let result = await request.get(`/sessions/${sessionId}/status`); | ||||
|             if (result['code'] !== 1) { | ||||
|                 clearInterval(stateChecker); | ||||
|             } else { | ||||
|                 let session = result['data']; | ||||
|                 if (session['status'] === 'connected') { | ||||
|                     clearInterval(stateChecker); | ||||
|                     return | ||||
|                 } | ||||
|  | ||||
|                 if (session['status'] === 'disconnected') { | ||||
|                     this.showMessage(session['message']); | ||||
|                     clearInterval(stateChecker); | ||||
|                 } | ||||
|             } | ||||
|         }, 1000) | ||||
|         this.setState({ | ||||
|             client: client, | ||||
|             containerWidth: width, | ||||
| @ -620,185 +519,6 @@ class Access extends Component { | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     onSelect = (keys, event) => { | ||||
|         this.setState({ | ||||
|             selectNode: { | ||||
|                 key: keys[0], | ||||
|                 isLeaf: event.node.isLeaf | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     handleOk = async (values) => { | ||||
|         let params = { | ||||
|             'dir': this.state.selectNode.key + '/' + values['dir'] | ||||
|         } | ||||
|         let paramStr = qs.stringify(params); | ||||
|  | ||||
|         this.setState({ | ||||
|             confirmLoading: true | ||||
|         }) | ||||
|         let result = await request.post(`/sessions/${this.state.sessionId}/mkdir?${paramStr}`); | ||||
|         if (result.code === 1) { | ||||
|             message.success('创建成功'); | ||||
|             let parentPath = this.state.selectNode.key; | ||||
|             let items = await this.getTreeNodes(parentPath); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, parentPath, items), | ||||
|                 selectNode: {} | ||||
|             }); | ||||
|         } else { | ||||
|             message.error(result.message); | ||||
|         } | ||||
|  | ||||
|         this.setState({ | ||||
|             confirmLoading: false, | ||||
|             confirmVisible: false | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     handleConfirmCancel = () => { | ||||
|         this.setState({ | ||||
|             confirmVisible: false | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     handleUploadCancel = () => { | ||||
|         this.setState({ | ||||
|             uploadVisible: false | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     mkdir = () => { | ||||
|         if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { | ||||
|             message.warning('请选择一个目录'); | ||||
|             return; | ||||
|         } | ||||
|         this.setState({ | ||||
|             confirmVisible: true | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     upload = () => { | ||||
|         if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { | ||||
|             message.warning('请选择一个目录进行上传'); | ||||
|             return; | ||||
|         } | ||||
|         this.setState({ | ||||
|             uploadVisible: true | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     download = () => { | ||||
|         if (!this.state.selectNode.key || !this.state.selectNode.isLeaf) { | ||||
|             message.warning('当前只支持下载文件'); | ||||
|             return; | ||||
|         } | ||||
|         download(`${server}/sessions/${this.state.sessionId}/download?file=${this.state.selectNode.key}`); | ||||
|     } | ||||
|  | ||||
|     rmdir = async () => { | ||||
|         if (!this.state.selectNode.key) { | ||||
|             message.warning('请选择一个文件或目录'); | ||||
|             return; | ||||
|         } | ||||
|         let result; | ||||
|         if (this.state.selectNode.isLeaf) { | ||||
|             result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectNode.key}`); | ||||
|         } else { | ||||
|             result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectNode.key}`); | ||||
|         } | ||||
|         if (result.code !== 1) { | ||||
|             message.error(result.message); | ||||
|         } else { | ||||
|             message.success('删除成功'); | ||||
|             let path = this.state.selectNode.key; | ||||
|             let parentPath = path.substring(0, path.lastIndexOf('/')); | ||||
|             let items = await this.getTreeNodes(parentPath); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, parentPath, items), | ||||
|                 selectNode: {} | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     refresh = async () => { | ||||
|         if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { | ||||
|             await this.loadDirData('/'); | ||||
|         } else { | ||||
|             let key = this.state.selectNode.key; | ||||
|             let items = await this.getTreeNodes(key); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, key, items), | ||||
|             }); | ||||
|         } | ||||
|         message.success('刷新目录成功'); | ||||
|     } | ||||
|  | ||||
|     onRightClick = ({event, node}) => { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     loadDirData = async (key) => { | ||||
|         let items = await this.getTreeNodes(key); | ||||
|         this.setState({ | ||||
|             treeData: items, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getTreeNodes = async (key) => { | ||||
|         const url = server + '/sessions/' + this.state.sessionId + '/ls?dir=' + key; | ||||
|  | ||||
|         let result = await request.get(url); | ||||
|  | ||||
|         if (result.code !== 1) { | ||||
|             message.error(result['message']); | ||||
|             message.error(result['message']); | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         let data = result.data; | ||||
|  | ||||
|         data = data.sort(((a, b) => a.name.localeCompare(b.name))); | ||||
|  | ||||
|         return data.map(item => { | ||||
|             return { | ||||
|                 title: item['name'], | ||||
|                 key: item['path'], | ||||
|                 isLeaf: !item['isDir'] && !item['isLink'], | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     onLoadData = ({key, children}) => { | ||||
|  | ||||
|         return new Promise(async (resolve) => { | ||||
|             if (children) { | ||||
|                 resolve(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let items = await this.getTreeNodes(key); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, key, items), | ||||
|             }); | ||||
|  | ||||
|             resolve(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     updateTreeData = (list, key, children) => { | ||||
|         return list.map((node) => { | ||||
|             if (node.key === key) { | ||||
|                 return {...node, children}; | ||||
|             } else if (node.children) { | ||||
|                 return {...node, children: this.updateTreeData(node.children, key, children)}; | ||||
|             } | ||||
|  | ||||
|             return node; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     sendCombinationKey = (keys) => { | ||||
|         for (let i = 0; i < keys.length; i++) { | ||||
|             this.state.client.sendKeyEvent(1, keys[i]); | ||||
| @ -810,52 +530,8 @@ class Access extends Component { | ||||
|  | ||||
|     render() { | ||||
|  | ||||
|         const title = ( | ||||
|             <Row> | ||||
|                 <Space> | ||||
|                     远程文件管理 | ||||
|                       | ||||
|                       | ||||
|                     <Tooltip title="创建文件夹"> | ||||
|                         <Button type="primary" size="small" icon={<FolderAddOutlined/>} | ||||
|                                 onClick={this.mkdir} ghost/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="上传"> | ||||
|                         <Button type="primary" size="small" icon={<CloudUploadOutlined/>} | ||||
|                                 onClick={this.upload} ghost/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="下载"> | ||||
|                         <Button type="primary" size="small" icon={<CloudDownloadOutlined/>} | ||||
|                                 onClick={this.download} ghost/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="删除文件"> | ||||
|                         <Button type="dashed" size="small" icon={<DeleteOutlined/>} onClick={this.rmdir} | ||||
|                                 danger/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="刷新"> | ||||
|                         <Button type="primary" size="small" icon={<ReloadOutlined/>} onClick={this.refresh} | ||||
|                                 ghost/> | ||||
|                     </Tooltip> | ||||
|                 </Space> | ||||
|             </Row> | ||||
|         ); | ||||
|  | ||||
|         const menu = ( | ||||
|             <Menu> | ||||
|                 <Menu.Item key="1" icon={<CopyOutlined/>} onClick={this.showClipboard}> | ||||
|                     剪贴板 | ||||
|                 </Menu.Item> | ||||
|                 <Menu.Item key="2" icon={<FileZipOutlined/>} onClick={this.showFileSystem}> | ||||
|                     文件管理 | ||||
|                 </Menu.Item> | ||||
|                 <Menu.Item key="3" icon={<ExpandOutlined/>} onClick={this.fullScreen}> | ||||
|                     {this.state.fullScreenBtnText} | ||||
|                 </Menu.Item> | ||||
|                 <SubMenu title="发送快捷键" icon={<DesktopOutlined/>}> | ||||
|                 <Menu.Item | ||||
|                     onClick={() => this.sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item> | ||||
|                 <Menu.Item | ||||
| @ -870,7 +546,6 @@ class Access extends Component { | ||||
|                     onClick={() => this.sendCombinationKey(['65515', '120'])}>Windows+X</Menu.Item> | ||||
|                 <Menu.Item | ||||
|                     onClick={() => this.sendCombinationKey(['65515'])}>Windows</Menu.Item> | ||||
|                 </SubMenu> | ||||
|             </Menu> | ||||
|         ); | ||||
|  | ||||
| @ -885,100 +560,106 @@ class Access extends Component { | ||||
|                     <div id="display"/> | ||||
|                 </div> | ||||
|  | ||||
|                 <Modal | ||||
|                     title="创建文件夹" | ||||
|                     visible={this.state.confirmVisible} | ||||
|                     onOk={() => { | ||||
|                         this.formRef.current | ||||
|                             .validateFields() | ||||
|                             .then(values => { | ||||
|                                 this.formRef.current.resetFields(); | ||||
|                                 this.handleOk(values); | ||||
|                             }) | ||||
|                             .catch(info => { | ||||
|  | ||||
|                             }); | ||||
|                     }} | ||||
|                     confirmLoading={this.state.confirmLoading} | ||||
|                     onCancel={this.handleConfirmCancel} | ||||
|                 > | ||||
|                     <Form ref={this.formRef} {...formItemLayout}> | ||||
|  | ||||
|                         <Form.Item label="文件夹名称" name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}> | ||||
|                             <Input autoComplete="off" placeholder="请输入文件夹名称"/> | ||||
|                         </Form.Item> | ||||
|                     </Form> | ||||
|                 </Modal> | ||||
|  | ||||
|                 <Modal | ||||
|                     title="上传文件" | ||||
|                     visible={this.state.uploadVisible} | ||||
|                     onOk={() => { | ||||
|  | ||||
|                     }} | ||||
|                     confirmLoading={this.state.uploadLoading} | ||||
|                     onCancel={this.handleUploadCancel} | ||||
|                 > | ||||
|                     <Upload | ||||
|                         action={server + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.selectNode.key}> | ||||
|                         <Button icon={<UploadOutlined/>}>上传文件</Button> | ||||
|                     </Upload> | ||||
|                 </Modal> | ||||
|  | ||||
|  | ||||
|                 <Draggable> | ||||
|                     <Affix style={{position: 'absolute', top: 50, right: 100}}> | ||||
|                         <Button icon={<ExpandOutlined/>} onClick={() => { | ||||
|                             this.fullScreen(); | ||||
|                         }}/> | ||||
|                     </Affix> | ||||
|                 </Draggable> | ||||
|  | ||||
|                 <Draggable> | ||||
|                     <Affix style={{position: 'absolute', top: 50, right: 150}}> | ||||
|                         <Button icon={<CopyTwoTone/>} onClick={() => { | ||||
|                             this.setState({ | ||||
|                                 clipboardVisible: true | ||||
|                             }); | ||||
|                         }}/> | ||||
|                     </Affix> | ||||
|                 </Draggable> | ||||
|  | ||||
|                 <Draggable> | ||||
|                     <Affix style={{position: 'absolute', top: 100, right: 100}}> | ||||
|                         <Button icon={<AppstoreTwoTone/>} onClick={() => { | ||||
|                             this.setState({ | ||||
|                                 fileSystemVisible: true, | ||||
|                             }); | ||||
|                         }}/> | ||||
|                     </Affix> | ||||
|                 </Draggable> | ||||
|  | ||||
|                 <Draggable> | ||||
|                     <Affix style={{position: 'absolute', top: 100, right: 150}}> | ||||
|                         <Dropdown overlay={menu} trigger={['click']} placement="bottomLeft"> | ||||
|                             <Button icon={<AppstoreTwoTone/>}/> | ||||
|                             <Button icon={<DesktopOutlined/>}/> | ||||
|                         </Dropdown> | ||||
|                     </Affix> | ||||
|                 </Draggable> | ||||
|  | ||||
|                 <Drawer | ||||
|                     title={title} | ||||
|                     title={'会话详情'} | ||||
|                     placement="right" | ||||
|                     width={window.innerWidth * 0.3} | ||||
|                     width={window.innerWidth * 0.5} | ||||
|                     closable={true} | ||||
|                     maskClosable={false} | ||||
|                     onClose={this.hideFileSystem} | ||||
|                     onClose={()=>{ | ||||
|                         this.setState({ | ||||
|                             fileSystemVisible: false | ||||
|                         }); | ||||
|                     }} | ||||
|                     visible={this.state.fileSystemVisible} | ||||
|                 > | ||||
|  | ||||
|  | ||||
|                     <Row style={{marginTop: 10}}> | ||||
|                         <Col span={24}> | ||||
|                             <Card title={this.state.fileSystem.currentDirectory} bordered={true} size="small"> | ||||
|                                 <Spin indicator={antIcon} spinning={this.state.fileSystem.loading}> | ||||
|  | ||||
|                                     <DirectoryTree | ||||
|                                         // multiple | ||||
|                                         onSelect={this.onSelect} | ||||
|                                         loadData={this.onLoadData} | ||||
|                                         treeData={this.state.treeData} | ||||
|                                         onRightClick={this.onRightClick} | ||||
|                                     /> | ||||
|  | ||||
|                                 </Spin> | ||||
|                             </Card> | ||||
|                             <FileSystem sessionId={this.state.sessionId}/> | ||||
|                         </Col> | ||||
|                     </Row> | ||||
|                 </Drawer> | ||||
|  | ||||
|                 <Drawer | ||||
|                 { | ||||
|                     this.state.clipboardVisible ? | ||||
|                         <Modal | ||||
|                             title="剪贴板" | ||||
|                     placement="right" | ||||
|                     width={window.innerWidth * 0.3} | ||||
|                     onClose={this.hideClipboard} | ||||
|                             maskClosable={false} | ||||
|                             visible={this.state.clipboardVisible} | ||||
|                             onOk={() => { | ||||
|                                 this.clipboardFormRef.current | ||||
|                                     .validateFields() | ||||
|                                     .then(values => { | ||||
|                                         let clipboardText = values['clipboard']; | ||||
|  | ||||
|                                         this.sendClipboard({ | ||||
|                                             'data': clipboardText, | ||||
|                                             'type': 'text/plain' | ||||
|                                         }); | ||||
|  | ||||
|                                         this.setState({ | ||||
|                                             clipboardText: clipboardText, | ||||
|                                             clipboardVisible: false | ||||
|                                         }); | ||||
|                                     }) | ||||
|                                     .catch(info => { | ||||
|  | ||||
|                                     }); | ||||
|                             }} | ||||
|                             confirmLoading={this.state.confirmLoading} | ||||
|                             onCancel={() => { | ||||
|                                 this.setState({ | ||||
|                                     clipboardVisible: false | ||||
|                                 }) | ||||
|                             }} | ||||
|                         > | ||||
|                             <Form ref={this.clipboardFormRef} initialValues={{'clipboard': this.state.clipboardText}}> | ||||
|                                 <Form.Item name='clipboard' rules={[{required: false}]}> | ||||
|                                     <TextArea id='clipboard' rows={10}/> | ||||
|                                 </Form.Item> | ||||
|                             </Form> | ||||
|                         </Modal> | ||||
|                         : undefined | ||||
|                 } | ||||
|  | ||||
|                     <Alert message="复制/剪切的文本将出现在这里。对下面文本内容所作的修改将会影响远程电脑上的剪贴板。" type="info" showIcon closable/> | ||||
|  | ||||
|                     <div style={{marginTop: 10, marginBottom: 10}}> | ||||
|                         <TextArea id='clipboard' rows={10} onBlur={this.updateClipboardFormTextarea}/> | ||||
|                     </div> | ||||
|  | ||||
|                 </Drawer> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -3,14 +3,13 @@ import "xterm/css/xterm.css" | ||||
| import {Terminal} from "xterm"; | ||||
| import qs from "qs"; | ||||
| import {wsServer} from "../../common/constants"; | ||||
| import "./Console.css" | ||||
| import {getToken, isEmpty} from "../../utils/utils"; | ||||
| import {FitAddon} from 'xterm-addon-fit'; | ||||
| import "./Access.css" | ||||
| import request from "../../common/request"; | ||||
| import {message} from "antd"; | ||||
| 
 | ||||
| class AccessSSH extends Component { | ||||
| class AccessNaive extends Component { | ||||
| 
 | ||||
|     state = { | ||||
|         width: window.innerWidth, | ||||
| @ -24,6 +23,7 @@ class AccessSSH extends Component { | ||||
| 
 | ||||
|         let urlParams = new URLSearchParams(this.props.location.search); | ||||
|         let assetId = urlParams.get('assetId'); | ||||
|         document.title = urlParams.get('assetName'); | ||||
| 
 | ||||
|         let sessionId = await this.createSession(assetId); | ||||
|         if (isEmpty(sessionId)) { | ||||
| @ -144,12 +144,11 @@ class AccessSSH extends Component { | ||||
|     } | ||||
| 
 | ||||
|     async createSession(assetsId) { | ||||
|         let result = await request.post(`/sessions?assetId=${assetsId}`); | ||||
|         let result = await request.post(`/sessions?assetId=${assetsId}&mode=naive`); | ||||
|         if (result['code'] !== 1) { | ||||
|             this.showMessage(result['message']); | ||||
|             return null; | ||||
|         } | ||||
|         document.title = result['data']['name']; | ||||
|         return result['data']['id']; | ||||
|     } | ||||
| 
 | ||||
| @ -203,4 +202,4 @@ class AccessSSH extends Component { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default AccessSSH; | ||||
| export default AccessNaive; | ||||
							
								
								
									
										474
									
								
								web/src/components/access/FileSystem.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								web/src/components/access/FileSystem.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,474 @@ | ||||
| import React, {Component} from 'react'; | ||||
| import {Button, Card, Form, Input, message, Modal, Row, Space, Table, Tooltip, Tree} from "antd"; | ||||
| import { | ||||
|     CloudDownloadOutlined, | ||||
|     CloudUploadOutlined, | ||||
|     DeleteOutlined, | ||||
|     FileExcelTwoTone, | ||||
|     FileImageTwoTone, | ||||
|     FileMarkdownTwoTone, | ||||
|     FilePdfTwoTone, | ||||
|     FileTextTwoTone, | ||||
|     FileTwoTone, | ||||
|     FileWordTwoTone, | ||||
|     FileZipTwoTone, | ||||
|     FolderAddOutlined, | ||||
|     FolderTwoTone, | ||||
|     LinkOutlined, | ||||
|     LoadingOutlined, | ||||
|     ReloadOutlined, | ||||
|     UploadOutlined | ||||
| } from "@ant-design/icons"; | ||||
| import qs from "qs"; | ||||
| import request from "../../common/request"; | ||||
| import {server} from "../../common/constants"; | ||||
| import Upload from "antd/es/upload"; | ||||
| import {download, getToken, renderSize} from "../../utils/utils"; | ||||
|  | ||||
| const antIcon = <LoadingOutlined/>; | ||||
|  | ||||
| const formItemLayout = { | ||||
|     labelCol: {span: 6}, | ||||
|     wrapperCol: {span: 14}, | ||||
| }; | ||||
|  | ||||
| const {DirectoryTree} = Tree; | ||||
|  | ||||
| class FileSystem extends Component { | ||||
|  | ||||
|     state = { | ||||
|         sessionId: undefined, | ||||
|         currentDirectory: '/', | ||||
|         files: [], | ||||
|         loading: false, | ||||
|         selectNode: {}, | ||||
|         selectedRowKeys: [] | ||||
|     } | ||||
|  | ||||
|     componentDidMount() { | ||||
|         let sessionId = this.props.sessionId; | ||||
|         this.setState({ | ||||
|             sessionId: sessionId | ||||
|         }, () => { | ||||
|             this.loadFiles('/'); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     onSelect = (keys, event) => { | ||||
|         this.setState({ | ||||
|             selectNode: { | ||||
|                 key: keys[0], | ||||
|                 isLeaf: event.node.isLeaf | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     handleOk = async (values) => { | ||||
|         let params = { | ||||
|             'dir': this.state.selectNode.key + '/' + values['dir'] | ||||
|         } | ||||
|         let paramStr = qs.stringify(params); | ||||
|  | ||||
|         this.setState({ | ||||
|             confirmLoading: true | ||||
|         }) | ||||
|         let result = await request.post(`/sessions/${this.state.sessionId}/mkdir?${paramStr}`); | ||||
|         if (result.code === 1) { | ||||
|             message.success('创建成功'); | ||||
|             let parentPath = this.state.selectNode.key; | ||||
|             let items = await this.getTreeNodes(parentPath); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, parentPath, items), | ||||
|                 selectNode: {} | ||||
|             }); | ||||
|         } else { | ||||
|             message.error(result.message); | ||||
|         } | ||||
|  | ||||
|         this.setState({ | ||||
|             confirmLoading: false, | ||||
|             confirmVisible: false | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     handleConfirmCancel = () => { | ||||
|         this.setState({ | ||||
|             confirmVisible: false | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     handleUploadCancel = () => { | ||||
|         this.setState({ | ||||
|             uploadVisible: false | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     mkdir = () => { | ||||
|         if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { | ||||
|             message.warning('请选择一个目录'); | ||||
|             return; | ||||
|         } | ||||
|         this.setState({ | ||||
|             confirmVisible: true | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     upload = () => { | ||||
|         if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { | ||||
|             message.warning('请选择一个目录进行上传'); | ||||
|             return; | ||||
|         } | ||||
|         this.setState({ | ||||
|             uploadVisible: true | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     download = () => { | ||||
|         if (!this.state.selectNode.key || !this.state.selectNode.isLeaf) { | ||||
|             message.warning('当前只支持下载文件'); | ||||
|             return; | ||||
|         } | ||||
|         download(`${server}/sessions/${this.state.sessionId}/download?file=${this.state.selectNode.key}`); | ||||
|     } | ||||
|  | ||||
|     rmdir = async () => { | ||||
|         if (!this.state.selectNode.key) { | ||||
|             message.warning('请选择一个文件或目录'); | ||||
|             return; | ||||
|         } | ||||
|         let result; | ||||
|         if (this.state.selectNode.isLeaf) { | ||||
|             result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectNode.key}`); | ||||
|         } else { | ||||
|             result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectNode.key}`); | ||||
|         } | ||||
|         if (result.code !== 1) { | ||||
|             message.error(result.message); | ||||
|         } else { | ||||
|             message.success('删除成功'); | ||||
|             let path = this.state.selectNode.key; | ||||
|             let parentPath = path.substring(0, path.lastIndexOf('/')); | ||||
|             let items = await this.getTreeNodes(parentPath); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, parentPath, items), | ||||
|                 selectNode: {} | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     refresh = async () => { | ||||
|         if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { | ||||
|             await this.loadDirData('/'); | ||||
|         } else { | ||||
|             let key = this.state.selectNode.key; | ||||
|             let items = await this.getTreeNodes(key); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, key, items), | ||||
|             }); | ||||
|         } | ||||
|         message.success('刷新目录成功'); | ||||
|     } | ||||
|  | ||||
|     loadFiles = async (key) => { | ||||
|         this.setState({ | ||||
|             loading: true | ||||
|         }) | ||||
|         try { | ||||
|             let result = await request.get(`${server}/sessions/${this.state.sessionId}/ls?dir=${key}`); | ||||
|             if (result['code'] !== 1) { | ||||
|                 message.error(result['message']); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let data = result['data']; | ||||
|  | ||||
|             const items = data.map(item => { | ||||
|                 return {'key': item['path'], ...item} | ||||
|             }) | ||||
|  | ||||
|             this.setState({ | ||||
|                 files: items | ||||
|             }) | ||||
|         } finally { | ||||
|             this.setState({ | ||||
|                 loading: false | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     loadDirData = async (key) => { | ||||
|         let items = await this.getTreeNodes(key); | ||||
|         this.setState({ | ||||
|             treeData: items, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getTreeNodes = async (key) => { | ||||
|         const url = server + '/sessions/' + this.state.sessionId + '/ls?dir=' + key; | ||||
|  | ||||
|         let result = await request.get(url); | ||||
|  | ||||
|         if (result.code !== 1) { | ||||
|             message.error(result['message']); | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         let data = result.data; | ||||
|  | ||||
|         return data.map(item => { | ||||
|             return { | ||||
|                 title: item['name'], | ||||
|                 key: item['path'], | ||||
|                 isLeaf: !item['isDir'] && !item['isLink'], | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     onLoadData = ({key, children}) => { | ||||
|  | ||||
|         return new Promise(async (resolve) => { | ||||
|             if (children) { | ||||
|                 resolve(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let items = await this.getTreeNodes(key); | ||||
|             this.setState({ | ||||
|                 treeData: this.updateTreeData(this.state.treeData, key, items), | ||||
|             }); | ||||
|  | ||||
|             resolve(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     updateTreeData = (list, key, children) => { | ||||
|         return list.map((node) => { | ||||
|             if (node.key === key) { | ||||
|                 return {...node, children}; | ||||
|             } else if (node.children) { | ||||
|                 return {...node, children: this.updateTreeData(node.children, key, children)}; | ||||
|             } | ||||
|  | ||||
|             return node; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     uploadChange = (info) => { | ||||
|         if (info.file.status !== 'uploading') { | ||||
|  | ||||
|         } | ||||
|         if (info.file.status === 'done') { | ||||
|             message.success(`${info.file.name} 文件上传成功。`, 3); | ||||
|         } else if (info.file.status === 'error') { | ||||
|             message.error(`${info.file.name} 文件上传失败。`, 10); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|  | ||||
|         const columns = [ | ||||
|             { | ||||
|                 title: '名称', | ||||
|                 dataIndex: 'name', | ||||
|                 key: 'name', | ||||
|                 render: (value, item) => { | ||||
|                     let icon; | ||||
|                     if (item['isDir']) { | ||||
|                         icon = <FolderTwoTone/>; | ||||
|                     } else { | ||||
|                         if (item['isLink']) { | ||||
|                             icon = <LinkOutlined/>; | ||||
|                         } else { | ||||
|                             const fileExtension = item['name'].split('.').pop().toLowerCase(); | ||||
|                             switch (fileExtension) { | ||||
|                                 case "doc": | ||||
|                                 case "docx": | ||||
|                                     icon = <FileWordTwoTone/>; | ||||
|                                     break; | ||||
|                                 case "xls": | ||||
|                                 case "xlsx": | ||||
|                                     icon = <FileExcelTwoTone/>; | ||||
|                                     break; | ||||
|                                 case "bmp": | ||||
|                                 case "jpg": | ||||
|                                 case "jpeg": | ||||
|                                 case "png": | ||||
|                                 case "tif": | ||||
|                                 case "gif": | ||||
|                                 case "pcx": | ||||
|                                 case "tga": | ||||
|                                 case "exif": | ||||
|                                 case "svg": | ||||
|                                 case "psd": | ||||
|                                 case "ai": | ||||
|                                 case "webp": | ||||
|                                     icon = <FileImageTwoTone/>; | ||||
|                                     break; | ||||
|                                 case "md": | ||||
|                                     icon = <FileMarkdownTwoTone/>; | ||||
|                                     break; | ||||
|                                 case "pdf": | ||||
|                                     icon = <FilePdfTwoTone/>; | ||||
|                                     break; | ||||
|                                 case "txt": | ||||
|                                     icon = <FileTextTwoTone/>; | ||||
|                                     break; | ||||
|                                 case "zip": | ||||
|                                 case "gz": | ||||
|                                 case "tar": | ||||
|                                     icon = <FileZipTwoTone/>; | ||||
|                                     break; | ||||
|                                 default: | ||||
|                                     icon = <FileTwoTone/>; | ||||
|                                     break; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return <>{icon}  {item['name']}</>; | ||||
|                 }, | ||||
|                 sorter: (a, b) => a.name - b.name, | ||||
|                 sortDirections: ['descend', 'ascend'], | ||||
|             }, | ||||
|             { | ||||
|                 title: '大小', | ||||
|                 dataIndex: 'size', | ||||
|                 key: 'size', | ||||
|                 render: (value, item) => { | ||||
|                     if (!item['isDir'] && !item['isLink']) { | ||||
|                         return renderSize(value) | ||||
|                     } | ||||
|                     return '-'; | ||||
|                 }, | ||||
|                 sorter: (a, b) => a.size - b.size, | ||||
|             }, | ||||
|             { | ||||
|                 title: '修改日期', | ||||
|                 dataIndex: 'modTime', | ||||
|                 key: 'modTime', | ||||
|                 sorter: (a, b) => a.modTime - b.modTime, | ||||
|                 sortDirections: ['descend', 'ascend'], | ||||
|             } | ||||
|         ]; | ||||
|  | ||||
|         const {loading, selectedRowKeys} = this.state; | ||||
|         const rowSelection = { | ||||
|             selectedRowKeys, | ||||
|             onChange: () => { | ||||
|  | ||||
|             }, | ||||
|         }; | ||||
|         const hasSelected = selectedRowKeys.length > 0; | ||||
|  | ||||
|         const title = ( | ||||
|             <Row> | ||||
|                 <Space> | ||||
|                     远程文件管理 | ||||
|                       | ||||
|                       | ||||
|                     <Tooltip title="创建文件夹"> | ||||
|                         <Button type="primary" size="small" icon={<FolderAddOutlined/>} | ||||
|                                 onClick={this.mkdir} ghost/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="上传"> | ||||
|                         <Button type="primary" size="small" icon={<CloudUploadOutlined/>} | ||||
|                                 onClick={this.upload} ghost/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="下载"> | ||||
|                         <Button type="primary" size="small" icon={<CloudDownloadOutlined/>} | ||||
|                                 onClick={this.download} ghost/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="删除文件"> | ||||
|                         <Button type="dashed" size="small" icon={<DeleteOutlined/>} onClick={this.rmdir} | ||||
|                                 danger/> | ||||
|                     </Tooltip> | ||||
|  | ||||
|                     <Tooltip title="刷新"> | ||||
|                         <Button type="primary" size="small" icon={<ReloadOutlined/>} onClick={this.refresh} | ||||
|                                 ghost/> | ||||
|                     </Tooltip> | ||||
|                 </Space> | ||||
|             </Row> | ||||
|         ); | ||||
|  | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Card title={title} bordered={true} size="small"> | ||||
|                     <Table columns={columns} | ||||
|                            rowSelection={rowSelection} | ||||
|                            dataSource={this.state.files} | ||||
|                            size={'small'} | ||||
|                            pagination={false} | ||||
|                            loading={this.state.loading} | ||||
|                            onRow={record => { | ||||
|                                return { | ||||
|                                    onClick: event => { | ||||
|  | ||||
|                                    }, // 点击行 | ||||
|                                    onDoubleClick: event => { | ||||
|                                        this.loadFiles(record['path']); | ||||
|                                    }, | ||||
|                                    onContextMenu: event => { | ||||
|                                    }, | ||||
|                                    onMouseEnter: event => { | ||||
|                                    }, // 鼠标移入行 | ||||
|                                    onMouseLeave: event => { | ||||
|                                    }, | ||||
|                                }; | ||||
|                            }} | ||||
|                     /> | ||||
|                 </Card> | ||||
|  | ||||
|                 <Modal | ||||
|                     title="上传文件" | ||||
|                     visible={this.state.uploadVisible} | ||||
|                     onOk={() => { | ||||
|  | ||||
|                     }} | ||||
|                     confirmLoading={this.state.uploadLoading} | ||||
|                     onCancel={this.handleUploadCancel} | ||||
|                 > | ||||
|                     <Upload | ||||
|                         action={server + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.selectNode.key}> | ||||
|                         <Button icon={<UploadOutlined/>}>上传文件</Button> | ||||
|                     </Upload> | ||||
|                 </Modal> | ||||
|  | ||||
|  | ||||
|                 <Modal | ||||
|                     title="创建文件夹" | ||||
|                     visible={this.state.confirmVisible} | ||||
|                     onOk={() => { | ||||
|                         this.formRef.current | ||||
|                             .validateFields() | ||||
|                             .then(values => { | ||||
|                                 this.formRef.current.resetFields(); | ||||
|                                 this.handleOk(values); | ||||
|                             }) | ||||
|                             .catch(info => { | ||||
|  | ||||
|                             }); | ||||
|                     }} | ||||
|                     confirmLoading={this.state.confirmLoading} | ||||
|                     onCancel={this.handleConfirmCancel} | ||||
|                 > | ||||
|                     <Form ref={this.formRef} {...formItemLayout}> | ||||
|  | ||||
|                         <Form.Item label="文件夹名称" name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}> | ||||
|                             <Input autoComplete="off" placeholder="请输入文件夹名称"/> | ||||
|                         </Form.Item> | ||||
|                     </Form> | ||||
|                 </Modal> | ||||
|  | ||||
|  | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default FileSystem; | ||||
| @ -13,7 +13,7 @@ const STATE_CONNECTED = 3; | ||||
| const STATE_DISCONNECTING = 4; | ||||
| const STATE_DISCONNECTED = 5; | ||||
|  | ||||
| class Access extends Component { | ||||
| class Monitor extends Component { | ||||
|  | ||||
|     formRef = React.createRef() | ||||
|  | ||||
| @ -103,76 +103,6 @@ class Access extends Component { | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     onError = (status) => { | ||||
|  | ||||
|         console.log('通道异常。', status); | ||||
|  | ||||
|         switch (status.code) { | ||||
|             case 256: | ||||
|                 this.showMessage('未支持的访问'); | ||||
|                 break; | ||||
|             case 512: | ||||
|                 this.showMessage('远程服务异常'); | ||||
|                 break; | ||||
|             case 513: | ||||
|                 this.showMessage('服务器忙碌'); | ||||
|                 break; | ||||
|             case 514: | ||||
|                 this.showMessage('服务器连接超时'); | ||||
|                 break; | ||||
|             case 515: | ||||
|                 this.showMessage('远程服务异常'); | ||||
|                 break; | ||||
|             case 516: | ||||
|                 this.showMessage('资源未找到'); | ||||
|                 break; | ||||
|             case 517: | ||||
|                 this.showMessage('资源冲突'); | ||||
|                 break; | ||||
|             case 518: | ||||
|                 this.showMessage('资源已关闭'); | ||||
|                 break; | ||||
|             case 519: | ||||
|                 this.showMessage('远程服务未找到'); | ||||
|                 break; | ||||
|             case 520: | ||||
|                 this.showMessage('远程服务不可用'); | ||||
|                 break; | ||||
|             case 521: | ||||
|                 this.showMessage('会话冲突'); | ||||
|                 break; | ||||
|             case 522: | ||||
|                 this.showMessage('会话连接超时'); | ||||
|                 break; | ||||
|             case 523: | ||||
|                 this.showMessage('会话已关闭'); | ||||
|                 break; | ||||
|             case 768: | ||||
|                 this.showMessage('网络不可达'); | ||||
|                 break; | ||||
|             case 769: | ||||
|                 this.showMessage('服务器密码验证失败'); | ||||
|                 break; | ||||
|             case 771: | ||||
|                 this.showMessage('客户端被禁止'); | ||||
|                 break; | ||||
|             case 776: | ||||
|                 this.showMessage('客户端连接超时'); | ||||
|                 break; | ||||
|             case 781: | ||||
|                 this.showMessage('客户端异常'); | ||||
|                 break; | ||||
|             case 783: | ||||
|                 this.showMessage('错误的请求类型'); | ||||
|                 break; | ||||
|             case 797: | ||||
|                 this.showMessage('客户端连接数量过多'); | ||||
|                 break; | ||||
|             default: | ||||
|                 this.showMessage('未知错误。'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     showMessage(message) { | ||||
|         Modal.error({ | ||||
|             title: '提示', | ||||
| @ -189,7 +119,6 @@ class Access extends Component { | ||||
|  | ||||
|         // 处理客户端的状态变化事件 | ||||
|         client.onstatechange = this.onClientStateChange; | ||||
|         client.onerror = this.onError; | ||||
|         const display = document.getElementById("display"); | ||||
|  | ||||
|         // Add client to display div | ||||
| @ -245,4 +174,4 @@ class Access extends Component { | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default Access; | ||||
| export default Monitor; | ||||
|  | ||||
| @ -313,17 +313,22 @@ class Asset extends Component { | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     access = async (id, protocol) => { | ||||
|     access = async (record) => { | ||||
|         const id = record['id']; | ||||
|         const protocol = record['protocol']; | ||||
|         const name = record['name']; | ||||
|  | ||||
|         message.loading({content: '正在检测资产是否在线...', key: id}); | ||||
|         let result = await request.post(`/assets/${id}/tcping`); | ||||
|         if (result.code === 1) { | ||||
|             if (result.data === true) { | ||||
|                 message.success({content: '检测完成,您访问的资产在线,即将打开窗口进行访问。', key: id, duration: 3}); | ||||
|                 if(protocol === 'ssh'){ | ||||
|                     window.open(`#/access-ssh?assetId=${id}`); | ||||
|                 }else { | ||||
|                     window.open(`#/access?assetsId=${id}&protocol=${protocol}`); | ||||
|                 } | ||||
|                 window.open(`#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`); | ||||
|                 // if (protocol === 'ssh') { | ||||
|                 //     window.open(`#/access-naive?assetId=${id}&assetName=${name}`); | ||||
|                 // } else { | ||||
|                 //     window.open(`#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`); | ||||
|                 // } | ||||
|             } else { | ||||
|                 message.warn('您访问的资产未在线,请确认网络状态。', 10); | ||||
|             } | ||||
| @ -530,7 +535,7 @@ class Asset extends Component { | ||||
|                     return ( | ||||
|                         <div> | ||||
|                             <Button type="link" size='small' | ||||
|                                     onClick={() => this.access(record.id, record.protocol)}>接入</Button> | ||||
|                                     onClick={() => this.access(record)}>接入</Button> | ||||
|  | ||||
|                             <Dropdown overlay={menu}> | ||||
|                                 <Button type="link" size='small'> | ||||
|  | ||||
| @ -489,10 +489,9 @@ class OfflineSession extends Component { | ||||
|                                 maskClosable={false} | ||||
|                             > | ||||
|                                 { | ||||
|                                     this.state.selectedRow['protocol'] === 'rdp' || this.state.selectedRow['protocol'] === 'vnc' ? | ||||
|                                         <Playback sessionId={this.state.selectedRow['id']}/> | ||||
|                                         : | ||||
|                                     this.state.selectedRow['mode'] === 'naive' ? | ||||
|                                         <iframe | ||||
|                                             title='recording' | ||||
|                                             style={{ | ||||
|                                                 width: '100%', | ||||
|                                                 // height: this.state.iFrameHeight, | ||||
| @ -510,6 +509,7 @@ class OfflineSession extends Component { | ||||
|                                             height={window.innerHeight * 0.8} | ||||
|                                             frameBorder="0" | ||||
|                                         /> | ||||
|                                         : <Playback sessionId={this.state.selectedRow['id']}/> | ||||
|                                 } | ||||
|  | ||||
|                             </Modal> : undefined | ||||
|  | ||||
| @ -210,3 +210,15 @@ export function exitFull() { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function renderSize(value) { | ||||
|     if (null == value || value === '' || value === 0) { | ||||
|         return "0 Bytes"; | ||||
|     } | ||||
|     const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; | ||||
|     let srcSize = parseFloat(value); | ||||
|     let index = Math.floor(Math.log(srcSize) / Math.log(1024)); | ||||
|     let size = srcSize / Math.pow(1024, index); | ||||
|     size = size.toFixed(2); | ||||
|     return size + unitArr[index]; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user