完善文件管理

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/ls", SessionLsEndpoint)
sessions.POST("/:id/mkdir", SessionMkDirEndpoint)
sessions.DELETE("/:id/rmdir", SessionRmDirEndpoint)
sessions.DELETE("/:id/rm", SessionRmEndpoint)
sessions.DELETE("/:id", SessionDeleteEndpoint)
sessions.GET("/:id/recording", SessionRecordingEndpoint)

View File

@ -476,63 +476,47 @@ func SessionMkDirEndpoint(c echo.Context) error {
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 {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteFile := c.QueryParam("file")
key := c.QueryParam("key")
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
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
}
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)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
@ -540,11 +524,13 @@ func SessionRmEndpoint(c echo.Context) error {
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 Success(c, nil)
}
return nil
}

View File

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

View File

@ -5,14 +5,64 @@
-ms-user-select: none;
}
.selectedRow {
background-color: #69c0ff;
@keyframes fadeIn {
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 {
background-color: #69c0ff;
.popup {
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 {
background: #bae7ff;
.popup li {
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 {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 {
CloudDownloadOutlined,
CloudUploadOutlined,
DeleteOutlined,
FileExcelTwoTone,
FileImageTwoTone,
FileMarkdownTwoTone,
FilePdfTwoTone,
FileTextTwoTone,
FileTwoTone,
FileWordTwoTone,
FileZipTwoTone,
ExclamationCircleOutlined,
FileExcelOutlined,
FileImageOutlined,
FileMarkdownOutlined,
FileOutlined,
FilePdfOutlined,
FileTextOutlined,
FileWordOutlined,
FileZipOutlined,
FolderAddOutlined,
FolderTwoTone,
LinkOutlined,
ReloadOutlined,
ThunderboltTwoTone,
UploadOutlined
} from "@ant-design/icons";
import qs from "qs";
@ -29,6 +30,7 @@ const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const {confirm} = Modal;
class FileSystem extends Component {
@ -40,7 +42,10 @@ class FileSystem extends Component {
files: [],
loading: false,
selectedRowKeys: [],
selectedRow: {}
selectedRow: {},
dropdown: {
visible: false
},
}
componentDidMount() {
@ -76,12 +81,6 @@ class FileSystem extends Component {
})
}
mkdir = () => {
this.setState({
confirmVisible: true
})
}
upload = () => {
this.setState({
uploadVisible: true
@ -93,29 +92,40 @@ class FileSystem extends Component {
}
rmdir = async () => {
if (!this.state.selectedRow.key) {
message.warning('请选择一个文件或目录');
return;
let selectedRowKeys = this.state.selectedRowKeys;
if (selectedRowKeys === undefined || selectedRowKeys.length === 0) {
message.warning('请至少选择一个文件或目录');
}
let result;
if (this.state.selectedRow.isLeaf) {
result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectedRow.key}`);
let title = '';
if (selectedRowKeys.length === 1) {
let file = selectedRowKeys[0].substring(selectedRowKeys[0].lastIndexOf('/') + 1, selectedRowKeys[0].length);
title = <p>您确认要删除"{file}"</p>;
} else {
result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectedRow.key}`);
title = `您确认要删除所选的${selectedRowKeys.length}项目吗?`;
}
if (result.code !== 1) {
message.error(result.message);
} else {
message.success('删除成功');
let path = this.state.selectedRow.key;
let parentPath = path.substring(0, path.lastIndexOf('/'));
let items = await this.getTreeNodes(parentPath);
this.setState({
treeData: this.updateTreeData(this.state.treeData, parentPath, items),
selectedRow: {}
confirm({
title: title,
icon: <ExclamationCircleOutlined/>,
content: '所选项目将立即被删除。',
onOk: async () => {
for (let i = 0; i < selectedRowKeys.length; i++) {
let rowKey = selectedRowKeys[i];
if (rowKey === '..') {
continue;
}
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 () => {
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() {
const columns = [
@ -182,17 +220,17 @@ class FileSystem extends Component {
icon = <FolderTwoTone/>;
} else {
if (item['isLink']) {
icon = <LinkOutlined/>;
icon = <ThunderboltTwoTone/>;
} else {
const fileExtension = item['name'].split('.').pop().toLowerCase();
switch (fileExtension) {
case "doc":
case "docx":
icon = <FileWordTwoTone/>;
icon = <FileWordOutlined/>;
break;
case "xls":
case "xlsx":
icon = <FileExcelTwoTone/>;
icon = <FileExcelOutlined/>;
break;
case "bmp":
case "jpg":
@ -207,24 +245,25 @@ class FileSystem extends Component {
case "psd":
case "ai":
case "webp":
icon = <FileImageTwoTone/>;
icon = <FileImageOutlined/>;
break;
case "md":
icon = <FileMarkdownTwoTone/>;
icon = <FileMarkdownOutlined/>;
break;
case "pdf":
icon = <FilePdfTwoTone/>;
icon = <FilePdfOutlined/>;
break;
case "txt":
icon = <FileTextTwoTone/>;
icon = <FileTextOutlined/>;
break;
case "zip":
case "gz":
case "tar":
icon = <FileZipTwoTone/>;
case "tgz":
icon = <FileZipOutlined/>;
break;
default:
icon = <FileTwoTone/>;
icon = <FileOutlined/>;
break;
}
}
@ -293,30 +332,28 @@ class FileSystem extends Component {
];
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>
远程文件管理
&nbsp;
&nbsp;
<Tooltip title="创建文件夹">
<Button type="primary" size="small" icon={<FolderAddOutlined/>}
onClick={this.mkdir} ghost/>
onClick={() => {
this.setState({
mkdirVisible: true
})
}} 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/>}
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/>
onClick={() => {
this.setState({
uploadVisible: true
})
}} ghost/>
</Tooltip>
<Tooltip title="刷新">
@ -324,22 +361,41 @@ class FileSystem extends Component {
ghost/>
</Tooltip>
</Space>
</Col>
</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 (
<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 => {
if (record['key'] === '..') {
return;
}
this.setState({
selectedRow: record
selectedRow: record,
selectedRowKeys: [record['key']]
});
}, // 点击行
onDoubleClick: event => {
@ -358,12 +414,44 @@ class FileSystem extends Component {
}
},
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 => {
}, // 鼠标移入行
onMouseLeave: event => {
},
};
}}
@ -378,17 +466,19 @@ class FileSystem extends Component {
title="上传文件"
visible={this.state.uploadVisible}
onOk={() => {
this.setState({
uploadVisible: false
})
}}
confirmLoading={this.state.uploadLoading}
onCancel={()=>{
onCancel={() => {
this.setState({
uploadVisible: false
})
}}
>
<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>
</Upload>
</Modal>
@ -396,12 +486,12 @@ class FileSystem extends Component {
<Modal
title="创建文件夹"
visible={this.state.confirmVisible}
visible={this.state.mkdirVisible}
onOk={() => {
this.formRef.current
this.mkdirFormRef.current
.validateFields()
.then(values => {
this.formRef.current.resetFields();
this.mkdirFormRef.current.resetFields();
this.handleOk(values);
})
.catch(info => {
@ -409,21 +499,21 @@ class FileSystem extends Component {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={()=>{
onCancel={() => {
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="请输入文件夹名称"/>
</Form.Item>
</Form>
</Modal>
{this.getNodeTreeRightClickMenu()}
</div>
);
}

View File

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

View File

@ -220,5 +220,5 @@ export function renderSize(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];
return size + ' ' + unitArr[index];
}