完善文件管理

This commit is contained in:
dushixiang 2021-02-03 19:53:13 +08:00
parent 9b9dcf9b56
commit 1444891d96
7 changed files with 270 additions and 162 deletions

View File

@ -111,7 +111,6 @@ func SetupRoutes() *echo.Echo {
sessions.GET("/:id/download", SessionDownloadEndpoint) sessions.GET("/:id/download", SessionDownloadEndpoint)
sessions.GET("/:id/ls", SessionLsEndpoint) sessions.GET("/:id/ls", SessionLsEndpoint)
sessions.POST("/:id/mkdir", SessionMkDirEndpoint) sessions.POST("/:id/mkdir", SessionMkDirEndpoint)
sessions.DELETE("/:id/rmdir", SessionRmDirEndpoint)
sessions.DELETE("/:id/rm", SessionRmEndpoint) sessions.DELETE("/:id/rm", SessionRmEndpoint)
sessions.DELETE("/:id", SessionDeleteEndpoint) sessions.DELETE("/:id", SessionDeleteEndpoint)
sessions.GET("/:id/recording", SessionRecordingEndpoint) sessions.GET("/:id/recording", SessionRecordingEndpoint)

View File

@ -476,63 +476,47 @@ func SessionMkDirEndpoint(c echo.Context) error {
return nil return nil
} }
func SessionRmDirEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
fileInfos, err := tun.Subject.SftpClient.ReadDir(remoteDir)
if err != nil {
return err
}
for i := range fileInfos {
if err := tun.Subject.SftpClient.Remove(path.Join(remoteDir, fileInfos[i].Name())); err != nil {
return err
}
}
if err := tun.Subject.SftpClient.RemoveDirectory(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.RemoveAll(path.Join(drivePath, remoteDir)); err != nil {
return err
}
return Success(c, nil)
}
return nil
}
func SessionRmEndpoint(c echo.Context) error { func SessionRmEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId) session, err := model.FindSessionById(sessionId)
if err != nil { if err != nil {
return err return err
} }
remoteFile := c.QueryParam("file") key := c.QueryParam("key")
if "ssh" == session.Protocol { if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId) tun, ok := global.Store.Get(sessionId)
if !ok { if !ok {
return errors.New("获取sftp客户端失败") return errors.New("获取sftp客户端失败")
} }
if err := tun.Subject.SftpClient.Remove(remoteFile); err != nil {
sftpClient := tun.Subject.SftpClient
stat, err := sftpClient.Stat(key)
if err != nil {
return err return err
} }
if stat.IsDir() {
fileInfos, err := tun.Subject.SftpClient.ReadDir(key)
if err != nil {
return err
}
for i := range fileInfos {
if err := tun.Subject.SftpClient.Remove(path.Join(key, fileInfos[i].Name())); err != nil {
return err
}
}
if err := tun.Subject.SftpClient.RemoveDirectory(key); err != nil {
return err
}
} else {
if err := tun.Subject.SftpClient.Remove(key); err != nil {
return err
}
}
return Success(c, nil) return Success(c, nil)
} else if "rdp" == session.Protocol { } else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath() drivePath, err := model.GetDrivePath()
@ -540,11 +524,13 @@ func SessionRmEndpoint(c echo.Context) error {
return err return err
} }
if err := os.Remove(path.Join(drivePath, remoteFile)); err != nil { if err := os.RemoveAll(path.Join(drivePath, key)); err != nil {
return err return err
} }
return Success(c, nil) return Success(c, nil)
} }
return nil return nil
} }

View File

@ -1,21 +1,15 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js'; import Guacamole from 'guacamole-common-js';
import {Affix, Button, Col, Drawer, Dropdown, Form, Input, Menu, message, Modal, Row, Space, Tooltip} from 'antd' import {Affix, Button, Col, Drawer, Dropdown, Form, Input, Menu, message, Modal, Row} from 'antd'
import qs from "qs"; import qs from "qs";
import request from "../../common/request"; import request from "../../common/request";
import {wsServer} from "../../common/constants"; import {wsServer} from "../../common/constants";
import { import {
AppstoreTwoTone, AppstoreTwoTone,
CloudDownloadOutlined,
CloudUploadOutlined,
CopyTwoTone, CopyTwoTone,
DeleteOutlined,
DesktopOutlined, DesktopOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
ExpandOutlined, ExpandOutlined
FolderAddOutlined,
LoadingOutlined,
ReloadOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import {exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils"; import {exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils";
import './Access.css' import './Access.css'
@ -48,9 +42,6 @@ class Access extends Component {
uploadHeaders: {}, uploadHeaders: {},
keyboard: {}, keyboard: {},
protocol: '', protocol: '',
treeData: [],
confirmVisible: false,
confirmLoading: false, confirmLoading: false,
uploadVisible: false, uploadVisible: false,
uploadLoading: false, uploadLoading: false,
@ -315,7 +306,7 @@ class Access extends Component {
}; };
onKeyDown = (keysym) => { onKeyDown = (keysym) => {
if (true === this.state.clipboardVisible || true === this.state.confirmVisible) { if (this.state.clipboardVisible || this.state.fileSystemVisible) {
return true; return true;
} }
this.state.client.sendKeyEvent(1, keysym); this.state.client.sendKeyEvent(1, keysym);
@ -598,9 +589,9 @@ class Access extends Component {
<Drawer <Drawer
title={'会话详情'} title={'会话详情'}
placement="right" placement="right"
width={window.innerWidth * 0.5} width={window.innerWidth * 0.8}
closable={true} closable={true}
maskClosable={false} // maskClosable={false}
onClose={() => { onClose={() => {
this.setState({ this.setState({
fileSystemVisible: false fileSystemVisible: false

View File

@ -5,14 +5,64 @@
-ms-user-select: none; -ms-user-select: none;
} }
.selectedRow { @keyframes fadeIn {
background-color: #69c0ff; 0% {
transform: translateY(-25%);
}
50%{
transform: translateY(4%);
}
65%{
transform: translateY(-2%);
}
80%{
transform: translateY(2%);
}
95%{
transform: translateY(-1%);
}
100% {
transform: translateY(0%);
}
} }
.selectedRow > .ant-table-column-sort { .popup {
background-color: #69c0ff; animation-name: fadeIn;
animation-duration: 0.4s;
background-clip: padding-box;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.75);
left: 0;
list-style-type: none;
margin: 0;
outline: none;
padding: 0;
position: fixed;
text-align: left;
top: 0;
overflow: hidden;
-webkit-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.75);
} }
.ant-table .ant-table-tbody > tr:hover:not(.ant-table-expanded-row .selectedRow) > td { .popup li {
background: #bae7ff; clear: both;
/*color: rgba(0, 0, 0, 0.65);*/
cursor: pointer;
font-size: 14px;
font-weight: normal;
line-height: 22px;
margin: 0;
padding: 5px 12px;
transition: all .3s;
white-space: nowrap;
-webkit-transition: all .3s;
}
.popup li:hover {
background-color: #e6f7ff;
}
.popup li > i {
margin-right: 8px;
} }

View File

@ -1,21 +1,22 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {Button, Card, Form, Input, message, Modal, Row, Space, Table, Tooltip} from "antd"; import {Button, Card, Col, Form, Input, message, Modal, Row, Space, Table, Tooltip} from "antd";
import { import {
CloudDownloadOutlined, CloudDownloadOutlined,
CloudUploadOutlined, CloudUploadOutlined,
DeleteOutlined, DeleteOutlined,
FileExcelTwoTone, ExclamationCircleOutlined,
FileImageTwoTone, FileExcelOutlined,
FileMarkdownTwoTone, FileImageOutlined,
FilePdfTwoTone, FileMarkdownOutlined,
FileTextTwoTone, FileOutlined,
FileTwoTone, FilePdfOutlined,
FileWordTwoTone, FileTextOutlined,
FileZipTwoTone, FileWordOutlined,
FileZipOutlined,
FolderAddOutlined, FolderAddOutlined,
FolderTwoTone, FolderTwoTone,
LinkOutlined,
ReloadOutlined, ReloadOutlined,
ThunderboltTwoTone,
UploadOutlined UploadOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import qs from "qs"; import qs from "qs";
@ -29,6 +30,7 @@ const formItemLayout = {
labelCol: {span: 6}, labelCol: {span: 6},
wrapperCol: {span: 14}, wrapperCol: {span: 14},
}; };
const {confirm} = Modal;
class FileSystem extends Component { class FileSystem extends Component {
@ -40,7 +42,10 @@ class FileSystem extends Component {
files: [], files: [],
loading: false, loading: false,
selectedRowKeys: [], selectedRowKeys: [],
selectedRow: {} selectedRow: {},
dropdown: {
visible: false
},
} }
componentDidMount() { componentDidMount() {
@ -76,12 +81,6 @@ class FileSystem extends Component {
}) })
} }
mkdir = () => {
this.setState({
confirmVisible: true
})
}
upload = () => { upload = () => {
this.setState({ this.setState({
uploadVisible: true uploadVisible: true
@ -93,29 +92,40 @@ class FileSystem extends Component {
} }
rmdir = async () => { rmdir = async () => {
if (!this.state.selectedRow.key) { let selectedRowKeys = this.state.selectedRowKeys;
message.warning('请选择一个文件或目录'); if (selectedRowKeys === undefined || selectedRowKeys.length === 0) {
return; message.warning('请至少选择一个文件或目录');
} }
let result;
if (this.state.selectedRow.isLeaf) { let title = '';
result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectedRow.key}`); if (selectedRowKeys.length === 1) {
let file = selectedRowKeys[0].substring(selectedRowKeys[0].lastIndexOf('/') + 1, selectedRowKeys[0].length);
title = <p>您确认要删除"{file}"</p>;
} else { } else {
result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectedRow.key}`); title = `您确认要删除所选的${selectedRowKeys.length}项目吗?`;
} }
if (result.code !== 1) { confirm({
message.error(result.message); title: title,
} else { icon: <ExclamationCircleOutlined/>,
message.success('删除成功'); content: '所选项目将立即被删除。',
let path = this.state.selectedRow.key; onOk: async () => {
let parentPath = path.substring(0, path.lastIndexOf('/')); for (let i = 0; i < selectedRowKeys.length; i++) {
let items = await this.getTreeNodes(parentPath); let rowKey = selectedRowKeys[i];
this.setState({ if (rowKey === '..') {
treeData: this.updateTreeData(this.state.treeData, parentPath, items), continue;
selectedRow: {} }
let result = await request.delete(`/sessions/${this.state.sessionId}/rm?key=${rowKey}`);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
await this.loadFiles(this.state.currentDirectory);
},
onCancel() {
},
}); });
} }
}
refresh = async () => { refresh = async () => {
this.loadFiles(this.state.currentDirectory); this.loadFiles(this.state.currentDirectory);
@ -169,6 +179,34 @@ class FileSystem extends Component {
} }
} }
getNodeTreeRightClickMenu = () => {
const {pageX, pageY, visible} = {...this.state.dropdown};
if (visible) {
console.log(pageX, pageY)
const tmpStyle = {
left: `${pageX}px`,
top: `${pageY}px`,
};
let disableDownload = true;
if (this.state.selectedRowKeys.length === 1
&& !this.state.selectedRow['isDir']
&& !this.state.selectedRow['isLink']) {
disableDownload = false;
}
return (
<ul className="popup" style={tmpStyle}>
<li><Button type={'text'} size={'small'} icon={<CloudDownloadOutlined/>} onClick={this.download}
disabled={disableDownload}>下载</Button></li>
<li><Button type={'text'} size={'small'} icon={<DeleteOutlined/>} onClick={this.rmdir}>删除</Button>
</li>
</ul>
);
}
return undefined;
};
render() { render() {
const columns = [ const columns = [
@ -182,17 +220,17 @@ class FileSystem extends Component {
icon = <FolderTwoTone/>; icon = <FolderTwoTone/>;
} else { } else {
if (item['isLink']) { if (item['isLink']) {
icon = <LinkOutlined/>; icon = <ThunderboltTwoTone/>;
} else { } else {
const fileExtension = item['name'].split('.').pop().toLowerCase(); const fileExtension = item['name'].split('.').pop().toLowerCase();
switch (fileExtension) { switch (fileExtension) {
case "doc": case "doc":
case "docx": case "docx":
icon = <FileWordTwoTone/>; icon = <FileWordOutlined/>;
break; break;
case "xls": case "xls":
case "xlsx": case "xlsx":
icon = <FileExcelTwoTone/>; icon = <FileExcelOutlined/>;
break; break;
case "bmp": case "bmp":
case "jpg": case "jpg":
@ -207,24 +245,25 @@ class FileSystem extends Component {
case "psd": case "psd":
case "ai": case "ai":
case "webp": case "webp":
icon = <FileImageTwoTone/>; icon = <FileImageOutlined/>;
break; break;
case "md": case "md":
icon = <FileMarkdownTwoTone/>; icon = <FileMarkdownOutlined/>;
break; break;
case "pdf": case "pdf":
icon = <FilePdfTwoTone/>; icon = <FilePdfOutlined/>;
break; break;
case "txt": case "txt":
icon = <FileTextTwoTone/>; icon = <FileTextOutlined/>;
break; break;
case "zip": case "zip":
case "gz": case "gz":
case "tar": case "tar":
icon = <FileZipTwoTone/>; case "tgz":
icon = <FileZipOutlined/>;
break; break;
default: default:
icon = <FileTwoTone/>; icon = <FileOutlined/>;
break; break;
} }
} }
@ -293,30 +332,28 @@ class FileSystem extends Component {
]; ];
const title = ( const title = (
<Row> <Row justify="space-around" align="middle" gutter={24}>
<Col span={16} key={1}>
{this.state.currentDirectory}
</Col>
<Col span={8} key={2} style={{textAlign: 'right'}}>
<Space> <Space>
远程文件管理
&nbsp;
&nbsp;
<Tooltip title="创建文件夹"> <Tooltip title="创建文件夹">
<Button type="primary" size="small" icon={<FolderAddOutlined/>} <Button type="primary" size="small" icon={<FolderAddOutlined/>}
onClick={this.mkdir} ghost/> onClick={() => {
this.setState({
mkdirVisible: true
})
}} ghost/>
</Tooltip> </Tooltip>
<Tooltip title="上传"> <Tooltip title="上传">
<Button type="primary" size="small" icon={<CloudUploadOutlined/>} <Button type="primary" size="small" icon={<CloudUploadOutlined/>}
onClick={this.upload} ghost/> onClick={() => {
</Tooltip> this.setState({
uploadVisible: true
<Tooltip title="下载"> })
<Button type="primary" size="small" icon={<CloudDownloadOutlined/>} }} ghost/>
disabled={isEmpty(this.state.selectedRow['key']) || this.state.selectedRow['isDir'] || this.state.selectedRow['isLink']}
onClick={this.download} ghost/>
</Tooltip>
<Tooltip title="删除文件">
<Button type="dashed" size="small" icon={<DeleteOutlined/>} disabled={isEmpty(this.state.selectedRow['key'])} onClick={this.rmdir}
danger/>
</Tooltip> </Tooltip>
<Tooltip title="刷新"> <Tooltip title="刷新">
@ -324,22 +361,41 @@ class FileSystem extends Component {
ghost/> ghost/>
</Tooltip> </Tooltip>
</Space> </Space>
</Col>
</Row> </Row>
); );
const {loading, selectedRowKeys} = this.state;
const rowSelection = {
selectedRowKeys,
onChange: (selectedRowKeys) => {
selectedRowKeys = selectedRowKeys.filter(rowKey => rowKey !== '..');
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return ( return (
<div> <div>
<Card title={title} bordered={true} size="small"> <Card title={title} bordered={true} size="small">
<Table columns={columns} <Table columns={columns}
rowSelection={rowSelection}
dataSource={this.state.files} dataSource={this.state.files}
size={'small'} size={'small'}
pagination={false} pagination={false}
loading={this.state.loading} loading={this.state.loading}
onRow={record => { onRow={record => {
return { return {
onClick: event => { onClick: event => {
if (record['key'] === '..') {
return;
}
this.setState({ this.setState({
selectedRow: record selectedRow: record,
selectedRowKeys: [record['key']]
}); });
}, // 点击行 }, // 点击行
onDoubleClick: event => { onDoubleClick: event => {
@ -358,12 +414,44 @@ class FileSystem extends Component {
} }
}, },
onContextMenu: event => { onContextMenu: event => {
event.preventDefault();
if (record['key'] === '..') {
return;
}
let selectedRowKeys = this.state.selectedRowKeys;
if (selectedRowKeys.length === 0) {
selectedRowKeys = [record['key']]
}
this.setState({
selectedRow: record,
selectedRowKeys: selectedRowKeys,
dropdown: {
visible: true,
pageX: event.pageX,
pageY: event.pageY,
}
});
if (!this.state.dropdown.visible) {
const that = this;
document.addEventListener(`click`, function onClickOutside() {
that.setState({dropdown: {visible: false}});
document.removeEventListener(`click`, onClickOutside);
document.querySelector('.ant-drawer-body').style.height = 'unset';
document.querySelector('.ant-drawer-body').style['overflow-y'] = 'auto';
});
document.querySelector('.ant-drawer-body').style.height = '100vh';
document.querySelector('.ant-drawer-body').style['overflow-y'] = 'hidden';
}
}, },
onMouseEnter: event => { onMouseEnter: event => {
}, // 鼠标移入行 }, // 鼠标移入行
onMouseLeave: event => { onMouseLeave: event => {
}, },
}; };
}} }}
@ -378,7 +466,9 @@ class FileSystem extends Component {
title="上传文件" title="上传文件"
visible={this.state.uploadVisible} visible={this.state.uploadVisible}
onOk={() => { onOk={() => {
this.setState({
uploadVisible: false
})
}} }}
confirmLoading={this.state.uploadLoading} confirmLoading={this.state.uploadLoading}
onCancel={() => { onCancel={() => {
@ -388,7 +478,7 @@ class FileSystem extends Component {
}} }}
> >
<Upload <Upload
action={server + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.selectedRow.key}> action={server + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.currentDirectory}>
<Button icon={<UploadOutlined/>}>上传文件</Button> <Button icon={<UploadOutlined/>}>上传文件</Button>
</Upload> </Upload>
</Modal> </Modal>
@ -396,12 +486,12 @@ class FileSystem extends Component {
<Modal <Modal
title="创建文件夹" title="创建文件夹"
visible={this.state.confirmVisible} visible={this.state.mkdirVisible}
onOk={() => { onOk={() => {
this.formRef.current this.mkdirFormRef.current
.validateFields() .validateFields()
.then(values => { .then(values => {
this.formRef.current.resetFields(); this.mkdirFormRef.current.resetFields();
this.handleOk(values); this.handleOk(values);
}) })
.catch(info => { .catch(info => {
@ -411,19 +501,19 @@ class FileSystem extends Component {
confirmLoading={this.state.confirmLoading} confirmLoading={this.state.confirmLoading}
onCancel={() => { onCancel={() => {
this.setState({ this.setState({
confirmVisible: false mkdirVisible: false
}) })
}} }}
> >
<Form ref={this.formRef} {...formItemLayout}> <Form ref={this.mkdirFormRef}>
<Form.Item label="文件夹名称" name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}> <Form.Item name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}>
<Input autoComplete="off" placeholder="请输入文件夹名称"/> <Input autoComplete="off" placeholder="请输入文件夹名称"/>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
{this.getNodeTreeRightClickMenu()}
</div> </div>
); );
} }

View File

@ -437,14 +437,6 @@ class Asset extends Component {
</Tooltip> </Tooltip>
); );
} }
}, {
title: '网络',
dataIndex: 'ip',
key: 'ip',
render: (text, record) => {
return record['ip'] + ':' + record['port'];
}
}, { }, {
title: '连接协议', title: '连接协议',
dataIndex: 'protocol', dataIndex: 'protocol',

View File

@ -220,5 +220,5 @@ export function renderSize(value) {
let index = Math.floor(Math.log(srcSize) / Math.log(1024)); let index = Math.floor(Math.log(srcSize) / Math.log(1024));
let size = srcSize / Math.pow(1024, index); let size = srcSize / Math.pow(1024, index);
size = size.toFixed(2); size = size.toFixed(2);
return size + unitArr[index]; return size + ' ' + unitArr[index];
} }