Closed G-Pig closed 3 years ago
我直接引用了zmodem进去,但是在new Zmodem的时候报错Sentry的问题。我直接打断点到构建对象里面,直接没进去
index.vue
<template>
<div class="app-container">
<el-row :gutter="15">
<el-col :span="4" class="card-box">
<el-card shadow="always" style="border-radius: 0px;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)">
<el-input v-model="filterText" clearable placeholder="输入关键字进行过滤" />
<el-tree
ref="tree"
class="filter-tree"
:data="treeData"
:props="defaultProps"
highlight-current
:default-expand-all="true"
:filter-node-method="filterNode"
@node-contextmenu="nodeRightClick"
@node-click="nodeClick"
/>
</el-card>
</el-col>
<el-col :span="20" class="card-box">
<el-card shadow="always" style="border-radius: 0px;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)">
<!--div style="margin-bottom: 10px;">
<el-button
type="primary"
@click="addTab(editableTabsValue)"
>
添加webssh
</el-button>
<el-button
type="danger"
@click="removeAllTab"
>
移除所有webssh
</el-button>
<span style="margin-left: 5px">当前webssh数:{{ tabIndex }}</span>
<span v-if="tabIndex > 0" style="margin-left: 5px">当前webssh:{{ currentTab }}</span>
</div-->
<el-tabs v-model="editableTabsValue" closable @edit="editTab">
<el-tab-pane v-for="item in editableTabs" :key="item.name" :label="item.title" :name="item.name">
<console :ref="item.name" :sid="item.name" :current-sid="editableTabsValue" />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<vue-context ref="menu" lazy @close="closeMenu">
<template v-if="currentData.menus">
<li v-for="menu in currentData.menus" :key="menu.label">
<a @click.prevent="addTab(menu.label)" @contextmenu.prevent="addTabRightClick(menu.label)">
{{ menu.label }}
</a>
</li>
<li class="v-context__sub">
<a>
sftp
</a>
<ul class="v-context">
<li>
<a @click.prevent="addTab('root')" @contextmenu.prevent="addTabRightClick('root')">
root
</a>
</li>
<li>
<a @click.prevent="addTab('test')" @contextmenu.prevent="addTabRightClick('test')">
test
</a>
</li>
</ul>
</li>
</template>
</vue-context>
</div>
</template>
<script>
import 'xterm/css/xterm.css'
import Console from './components/Console'
import VueContext from 'vue-context'
// import 'vue-context/src/sass/vue-context.scss'
// import 'vue-context/dist/css/vue-context.css'
export default {
name: 'WebSsh',
components: { Console, VueContext },
data() {
return {
top: 0,
left: 0,
currentData: {},
tabOpen: 5,
editableTabsValue: '192.168.223.111',
editableTabs: [{
title: '192.168.223.111',
name: '192.168.223.111'
}],
// tabIndex: 2
filterText: '',
treeData: [{
label: '全部',
children: [{
label: '运维',
children: [{
id: 1,
label: '主机1',
menus: [{
label: 'webssh'
}, {
label: 'clissh'
}]
}]
},
{
label: '开发',
children: [{
id: 2,
label: '主机2',
menus: [{
label: 'webssh'
}, {
label: 'vnc'
}]
}, {
id: 3,
label: '主机3',
menus: [{
label: 'webssh'
}, {
label: 'clissh'
}, {
label: 'vnc'
}]
}]
},
{
label: '测试',
children: [{
id: 4,
label: '主机4',
menus: [{
label: 'clissh'
}]
},
{
id: 5,
label: '主机5',
menus: [{
label: 'webssh'
}]
}]
},
{
id: 6,
label: '主机6',
menus: [{
label: 'rdp'
}]
}, {
id: 7,
label: '主机7'
},
{
id: 8,
label: '主机8',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 8,
label: '主机8',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 9,
label: '主机9',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 10,
label: '主机10',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 11,
label: '主机11',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 12,
label: '主机12',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 13,
label: '主机13',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 14,
label: '主机14',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 15,
label: '主机15',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 16,
label: '主机16',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 17,
label: '主机17',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
},
{
id: 18,
label: '主机18',
menus: [{
label: 'rdp'
}, {
label: 'ssh'
}]
}]
}],
defaultProps: {
children: 'children',
label: 'label'
}
}
},
computed: {
tabIndex: function() {
return this.editableTabs.length
},
currentTab: function() {
if (this.tabIndex === 0) {
return ''
} else {
return this.editableTabsValue
}
}
},
watch: {
filterText(val) {
this.$refs.tree.filter(val)
}
},
mounted() {
window.onbeforeunload = function(e) {
const dialogText = '禁止刷新'
e.returnValue = dialogText
return dialogText
}
// 自定义的 evil 函数(替代 eval)需要使用这个全局变量
window.$refs = this.$refs
},
beforeDestroy() {
window.onbeforeunload = function(e) {}
},
methods: {
randomString(length, chars) {
const xChars = chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
let result = ''
for (let i = length; i > 0; --i) result += xChars[Math.floor(Math.random() * xChars.length)]
return result
},
addTab(targetName) {
if (this.tabIndex < this.tabOpen) {
// this.tabIndex += 1
const newTabName = this.randomString(8)
this.editableTabs.push({
title: newTabName,
name: newTabName
})
this.editableTabsValue = newTabName
} else {
this.$message({
type: 'error',
message: `最多打开 ${this.tabOpen} 个标签`
})
}
},
addTabRightClick(targetName) {
this.$refs.menu.close()
this.addTab(targetName)
},
removeAllTab() {
this.editableTabs = []
},
removeTab(targetName) {
const tabs = this.editableTabs
let activeName = this.editableTabsValue
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
const nextTab = tabs[index + 1] || tabs[index - 1]
if (nextTab) {
activeName = nextTab.name
}
}
})
}
this.editableTabsValue = activeName
this.editableTabs = tabs.filter(tab => tab.name !== targetName)
// this.tabIndex -= 1
},
editTab(targetName, action) {
if (action === 'remove') {
// 根据 $refs 获取子组件数据判断 webssh 是否关闭,如果已经关闭直接标签
const closed = this.evil(`window.$refs['${targetName}'][0].closed`)
if (closed) {
this.removeTab(targetName)
} else {
this.$confirm(`确定关闭标签 ${targetName} 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.removeTab(targetName)
}).catch(() => {
//
})
}
}
},
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
nodeClick(object, node, element) {
if (!object.children) {
this.currentData = object
const event = window.event || undefined
if (event) {
this.openMenu(event)
} else {
this.$message({
type: 'error',
message: '浏览器不支持 event 事件'
})
}
}
},
nodeRightClick(event, object, node, element) {
if (!object.children) {
this.currentData = object
this.openMenu(event)
}
},
openMenu(e) {
if (this.currentData.menus) {
this.$refs.menu.open(event, name)
} else {
this.$message.error(`${this.currentData.label} 无可操作项`)
this.$refs.menu.close()
}
},
closeMenu() {
this.currentData = {}
},
evil(fn) {
const Fn = Function
return new Fn('return ' + fn)()
}
}
}
</script>
<style lang="scss" scoped>
// vue-content 内容参考 'vue-context/src/sass/vue-context.scss'
$menu-bg: #fff;
$menu-border: rgba(0, 0, 0, 0.15);
$item-color: #212529;
$item-hover-bg: #ece6e6;
$item-hover-color: #212529;
.v-context {
&, & ul {
background-color: $menu-bg;
background-clip: padding-box;
// border-radius: .25rem;
border-radius: 0;
// border: 1px solid $menu-border;
// box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
display: block;
margin: 0;
padding: 3px 0;
min-width: 8rem;
z-index: 1500;
position: fixed;
list-style: none;
box-sizing: border-box;
max-height: calc(100% - 50px);
overflow-y: auto;
> li {
margin: 0;
position: relative;
> a {
display: block;
padding: .5rem 1.5rem;
font-size: 14px;
font-weight: 400;
color: $item-color;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border: 0;
&:hover,
&:focus {
text-decoration: none;
color: $item-hover-color;
background-color: $item-hover-bg;
}
&:focus {
outline: 0;
}
}
}
&:focus {
outline: 0;
}
}
&__sub {
> a:after {
content: "\203A";
float: right;
padding-left: 1rem;
}
> ul {
display: none;
}
}
}
</style>
xterm.js
import { Terminal } from 'xterm'
import { AttachAddon } from 'xterm-addon-attach'
import { FitAddon } from 'xterm-addon-fit'
import { SearchAddon } from 'xterm-addon-search'
import { Unicode11Addon } from 'xterm-addon-unicode11'
import { WebLinksAddon } from 'xterm-addon-web-links'
import { WebglAddon } from 'xterm-addon-webgl'
export { Terminal, AttachAddon, FitAddon, SearchAddon, Unicode11Addon, WebLinksAddon, WebglAddon }
Console.vue
<template>
<div>
<div>
<el-input v-model="search" placeholder="搜索终端内容" clearable style="width: 250px;" @keyup.enter.native="searchContent" @clear="searchContent" />
<el-button style="margin-left: 10px;" type="primary" icon="el-icon-search" @click="searchContent">
搜索
</el-button>
</div>
<div :id="sid" style="margin-top: 10px;height: 100%" />
<input :id="'upload-' + sid" type="text" hidden>
<el-dialog v-el-drag-dialog :visible.sync="dialogUploadVisible" width="480px" style="text-align: center;" :before-close="dialogBeforeClose">
<el-upload
drag
action="/upload"
:multiple="true"
:limit="1"
:show-file-list="false"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处,或 <em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">
文件个数上限: {{ fileLimit }} ,单文件大小上限: {{ maxFileSize | bytesHuman }}
</div>
</el-upload>
</el-dialog>
</div>
</template>
<script>
import elDragDialog from '@/directive/el-drag-dialog' // base on element-ui
import { Terminal, SearchAddon, Unicode11Addon } from './xterm'
import store from '@/store'
import { getToken } from '@/utils/auth'
import { node } from '@/api/node'
import { bytesHuman, getBrowser } from '@/utils'
// import Zmodem from 'zmodem.js'
import * as Zmodem from 'zmodem.js/src/zmodem_browser'
import streamSaver from 'streamsaver'
import * as ponyfill from 'web-streams-polyfill/ponyfill'
streamSaver.WritableStream = streamSaver.WritableStream || ponyfill.WritableStream
streamSaver.mitm = '/streamsaver/mitm.html' // 默认使用 https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0
// if (!streamSaver.WritableStream) {
// // 有些浏览器不支持 WritableStream (比如 firefox),无法使用 streamsaver.js
// // 使用 web-streams-polyfill/ponyfill 库弥补
// const ponyfill = require('web-streams-polyfill/ponyfill')
// streamSaver.WritableStream = ponyfill.WritableStream
// streamSaver.ReadableStream = ponyfill.ReadableStream
// window.ReadableStream = ponyfill.ReadableStream
// }
export default {
directives: { elDragDialog },
props: {
sid: {
type: String,
default: 'terminal'
},
currentSid: {
type: String,
default: 'terminal'
}
},
data() {
return {
// 当使用 zmodemjs 自带的 Zmodem.Browser.send_files 函数上传文件的时候,
// 由于没有分段读取(不同浏览器默认读取方式可能不一样,比如 chrome 会分段读取
// 但是,分段不固定,可能会很大,firefox 直接一次性读取,两种在读大文件时都很占
// 内存),会很大几率导致服务端抛出数据包超时错误,好在 html5 FileReader 提供了
// 分段读取 api,所以这里重写了 Zmodem.Browser.send_files 函数,使用分段读取的
// 方式避免超时错误(也可以 rz -O 解决)。理论上来说,分段读取后就不会占用大内存,
// 但是经过实际测试发现 chrome 84 在上传较大文件时还是会占用较大内存,最终出现 out
// of memory 错误,而 firefox 78 这方面就做得比较好了,不会占用大内存,原因未知,可
// 能是内部机制不一样。然后下载使用 streamsaver.js 写入流的方式以减小内存占用,但是
// 由于 firefox 不支持 WritableStream 需要使用第三方库 web-streams-polyfill/ponyfill 弥补。
// 并且经过测试发现 chrome 84 下载时还是会占用较大内存(但是比直接完全保存到内存再保存
// 文件要小很多),而 firefox 会占用很大内存,原因猜测是 zmodem.js 这个库内存泄露吧。
// 所以最优的使用方法是使用 chrome 下载,firefox 上传。
// rz firefox 可以 4096, chrome 2048
// sz firefox 可以 1024, chrome 2048
// 并且根据最新的测试,如果直接分段读取文件,不管 chrome 或者 firefox 都非常快,并且
// 不占用内存,如果分段读取后直接发送 websocket,这时候会占用约等同于文件大小的内存,估计
// 是 websocket 网络发送速率跟不上读取速度,文件内容缓存在内存中的原因;然后如果直接从
// websocket 读取二进制数据不经过 zmodem.js,而是直接使用 streamsaver 保存到文件,速度
// 相对来说比较快,并且都不占用内存(chrome、firefox 都是)。故上面使用 zmodem.js 上传
// 和下载文件所遇到的速度以及内存占用问题,估计是 websocket(缓存内容占用内存) 或者
// zmodem.js 库的问题(内存泄露或者其他?)
maxFileSize: 2048 * 1024 * 1024, // rz 最大上传文件大小
maxFileSizeFireFox: 4096 * 1024 * 1024, // rz firefox 最大上传文件大小;firefox 分段读取支持好,可以上传更大文件
maxDownloadFileSize: 2048 * 1024 * 1024, // sz 最大下载文件大小
fileLimit: 50, // rz 一次性最大上传文件个数
block: 1024 * 1024, // rz 上传分段读取大小
webssh: '',
terminal: '',
// attachAddon: '',
// fitAddon: '',
// webglAddon: '',
unicode11Addon: '',
// webLinksAddon: '',
searchAddon: '',
terminalWidth: 0,
// cols: 80,
rows: 40,
search: '',
closed: false,
wsServer: '',
dialogUploadVisible: false,
zsentry: '',
zsession: '',
selectFile: '',
fileElement: '',
host: '192.168.223.101',
port: '22',
user: 'root',
pass: '123456',
zmodemMode: false,
keyDispose: '',
lastOffset: 0,
perOffset: 1024 * 1024
}
},
computed: {
cols: function() {
if (this.terminalWidth === 0) {
return Math.floor(document.getElementById(this.sid).offsetWidth / 9)
}
return Math.floor(this.terminalWidth / 9)
}
},
watch: {
currentSid(val) {
// 监听父组件中 tab 标签页活动页,如果当前子组件是活动页,则注册当前组件的 id 到 windows.resize 事件
// 以实现 webssh 动态改变窗口大小,不过经过测试发现改变窗口大小的操作有时会有点问题,比如会导致 top 命令显示错乱
if (val === this.sid) {
this.terminal.focus()
this.setResize()
this.terminalWidth = document.getElementById(this.sid).offsetWidth
}
},
terminalWidth(val) {
if (!this.zmodemMode) {
this.webssh.send(JSON.stringify({ type: 'resize', cols: this.cols, rows: this.rows }))
this.terminal.resize(this.cols, this.rows)
}
},
selectFile(val) {
if (val !== '') {
this.fileElement._value = this.randomString(24)
}
}
},
mounted() {
const browser = getBrowser()
if (browser === 'Firefox') {
this.maxFileSize = this.maxFileSizeFireFox
}
this.fileElement = document.getElementById('upload-' + this.sid)
this.terminal = new Terminal({
rendererType: 'canvas', // 渲染类型 dom,canvas,渲染速度:dom < canvas < webgl,兼容性反之,使用 webgl 的方式是设置 canvas,然后加载 WebglAddon 插件
scrollback: 12800, // 终端回滚量
cols: this.cols,
rows: this.rows,
useStyle: true,
cursorStyle: 'block', // 光标样式 'block' | 'underline' | 'bar'
cursorBlink: true,
theme: {
foreground: '#8eb1b2',
background: '#002833'
// cursor: 'help', // 设置光标
}
})
const terminalContainer = document.getElementById(this.sid)
this.terminal.open(terminalContainer)
// 使用 fit 插件可能会导致 top 命令显示问题
// this.fitAddon = new FitAddon()
// this.terminal.loadAddon(this.fitAddon)
// this.fitAddon.fit()
// 部分浏览器目前默认没有开启 WebGL 支持,比如 chrome 81(firefox 75 可用)
// 使用 webgl 渲染时可以解决部分汉字显示被截头的问题
// if (this.checkWebgl2Support()) {
// this.webglAddon = new WebglAddon()
// this.terminal.loadAddon(this.webglAddon)
// }
this.unicode11Addon = new Unicode11Addon()
this.terminal.loadAddon(this.unicode11Addon)
// this.webLinksAddon = new WebLinksAddon()
// this.terminal.loadAddon(this.webLinksAddon)
this.searchAddon = new SearchAddon()
this.terminal.loadAddon(this.searchAddon)
this.terminal.focus()
node().then(response => {
this.wsServer = response.data.lists
const wsUrl = `ws://${this.wsServer.ip}:${this.wsServer.http}/ws/webssh`
// const wsUrl = `ws://${this.wsServer.ip}:${this.wsServer.http}/ws/webssh2`
let token = ''
if (store.getters.token) {
token = getToken()
}
if (token === '') {
this.webssh = new WebSocket(wsUrl)
} else {
this.webssh = new WebSocket(wsUrl, [token, 'webssh'])
}
// this.webssh = new WebSocket('ws://127.0.0.1:8000/api/ssh', ['webssh'])
this.webssh.binaryType = 'arraybuffer'
this.webssh.onopen = this.wsOnOpen
this.webssh.onerror = this.wsOnError
this.webssh.onclose = this.wsOnClose
this.setResize()
const that = this
this.zsentry = new Zmodem.Sentry({
to_terminal: (octets) => {
//
},
on_detect: (detection) => {
const zsession = detection.confirm()
const zsessionType = zsession.type
that.keyDispose = that.terminal.onKey(e => {
if (zsessionType === 'receive') {
const event = e.domEvent
if (event.ctrlKey && event.key === 'c') {
// 这种方式 ctrl + c 终止好像只能 sz 下载时工作正常
console.log('ctrl + c 终止')
detection.deny()
}
} else {
const event = e.domEvent
if (event.ctrlKey && event.key === 'c') {
// console.log('ctrl + c 终止')
// that.webssh.send(new Uint8Array([42, 42, 24, 66, 48, 56, 48, 48, 48, 48, 48, 48, 48, 48, 48, 50, 50, 100, 13, 10]))
// that.webssh.send(new Uint8Array([79, 79]))
}
}
})
let promise
if (zsessionType === 'receive') {
// promise = that.downloadFile(zsession)
promise = that.downloadFileStreamSaver(zsession)
} else {
promise = that.uploadFile(zsession)
}
try {
promise.catch(console.error.bind(console)).then(() => {
//
})
} catch (err) {
console.log(err)
}
},
on_retract: () => {
//
},
sender: (octets) => {
that.webssh.send(new Uint8Array(octets))
}
})
}).catch(() => {
//
})
},
beforeDestroy() {
this.webssh.close()
this.terminal.dispose()
},
methods: {
closeZsession() {
try {
// zsession 每 5s 发送一个 ZACK 包,5s 后会出现提示最后一个包是 ”ZACK“ 无法正常关闭
// 这里直接设置 _last_header_name 为 ZRINIT,就可以强制关闭了
this.zsession._last_header_name = 'ZRINIT'
this.zsession.close()
} catch (err) {
console.log(err)
}
},
dialogBeforeClose(done) {
done()
this.closeZsession()
},
handleExceed(files, fileList) {
// 多文件上传
if (files.length > this.fileLimit) {
this.$message.error(`文件个数不能超过 ${this.fileLimit} 个`)
return ''
} else {
const tmp = []
for (let i = 0; i <= files.length - 1; i++) {
const checkSize = files[i].size <= this.maxFileSize
if (!checkSize) {
this.$message.error('文件大小不能超过 ' + bytesHuman(this.maxFileSize, 1))
return ''
}
tmp[i] = files[i]
}
this.selectFile = tmp
this.dialogUploadVisible = false
}
},
beforeUpload(file) {
// 单文件上传
const checkSize = file.size <= this.maxFileSize
if (!checkSize) {
this.$message.error('文件大小不能超过 ' + bytesHuman(this.maxFileSize, 1))
} else {
this.selectFile = file
// 直接设置 dialogUploadVisible 隐藏对话框不会触发对话框的 before-close 回调
this.dialogUploadVisible = false
}
return false
},
checkWebgl2Support() {
const isWebGL2Supported = () => !!document.createElement('canvas').getContext('webgl2')
return isWebGL2Supported()
},
setResize() {
const that = this
window.onresize = () => {
return (() => {
that.terminalWidth = document.getElementById(that.sid).offsetWidth
})()
}
},
utf8_to_b64(rawString) {
return btoa(unescape(encodeURIComponent(rawString)))
},
b64_to_utf8(encodeString) {
return decodeURIComponent(escape(atob(encodeString)))
},
randomString(len) {
len = len || 32
const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
const maxPos = $chars.length
let pwd = ''
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
}
return pwd
},
searchContent() {
if (this.search !== '') {
this.searchAddon.findNext(this.search)
}
},
uploadFile(zsession) {
// const that = this
this.zsession = zsession
this.selectFile = ''
this.dialogUploadVisible = true
const that = this
return new Promise((res) => {
const upload = function(e) {
that.terminal.setOption('disableStdin', true)
that.zmodemMode = true
let files_obj
if (typeof that.selectFile.length === 'undefined') {
files_obj = [that.selectFile]
} else {
files_obj = that.selectFile
}
that.sendBlockFile(zsession, files_obj, {
// that.sendFile(zsession, files_obj, {
// Zmodem.Browser.send_files(zsession, files_obj, {
on_offer_response(obj, xfer) {
if (xfer) {
// that.terminal.write('\r\n')
} else {
that.terminal.write(obj.name + ' was skipped upload\r\n')
}
},
on_progress(obj, xfer) {
that.updateProgress(xfer, 'upload')
},
on_file_complete(obj) {
that.terminal.write('\r\n')
}
}).then(zsession.close.bind(zsession), console.error.bind(console)
).then(() => {
res()
that.terminal.setOption('disableStdin', false)
that.keyDispose.dispose()
that.keyDispose = ''
that.zmodemMode = false
})
}
that.fileElement.addEventListener('onchange', upload, false)
Object.defineProperty(that.fileElement, '_value', {
configurable: true,
set: function(value) {
this.value = value
upload()
},
get: function() {
return this.value
}
})
})
},
downloadFileStreamSaver(zsession) {
const that = this
zsession.on('offer', (xfer) => {
if (xfer.get_details().size > that.maxDownloadFileSize) {
xfer.skip()
that.$message({
type: 'error',
message: xfer.get_details().name + ' 文件大小超过: ' + bytesHuman(that.maxDownloadFileSize, 1) + ', 跳过'
})
return
}
that.terminal.setOption('disableStdin', true)
that.zmodemMode = true
const fileStream = streamSaver.createWriteStream(xfer.get_details().name, {
size: xfer.get_details().size
})
const writer = fileStream.getWriter()
xfer.on('input', (payload) => {
that.updateProgress(xfer, 'download')
writer.write(new Uint8Array(payload))
})
xfer.accept().then(
() => {
that.terminal.write('\r\n')
writer.close()
},
console.error.bind(console)
)
// function makeReadableZsessionStream() {
// return new ReadableStream({
// start(ctrl) {
// xfer.on('input', (payload) => {
// that.updateProgress(xfer, 'download')
// ctrl.enqueue(new Uint8Array(payload))
// })
// xfer.accept().then(
// () => {
// ctrl.close()
// that.terminal.write('\r\n')
// },
// console.error.bind(console)
// )
// },
// cancel() {
// //
// }
// })
// }
// const readable = makeReadableZsessionStream()
// readable.pipeTo(fileStream).then(() => {
// console.log('success write file')
// }, (err) => {
// console.log(err, 'failed write file')
// })
})
const promise = new Promise((res) => {
zsession.on('session_end', () => {
res()
that.terminal.setOption('disableStdin', false)
that.keyDispose.dispose()
that.keyDispose = ''
that.zmodemMode = false
})
})
zsession.start()
return promise
},
downloadFile(zsession) {
const that = this
zsession.on('offer', (xfer) => {
if (xfer.get_details().size > that.maxDownloadFileSize) {
xfer.skip()
that.$message({
type: 'error',
message: xfer.get_details().name + ' 文件大小超过: ' + bytesHuman(that.maxDownloadFileSize, 1) + ', 跳过'
})
return
}
that.terminal.setOption('disableStdin', true)
that.zmodemMode = true
const fileBuffer = []
xfer.on('input', (payload) => {
that.updateProgress(xfer, 'download')
fileBuffer.push(new Uint8Array(payload))
})
xfer.accept().then(
() => {
that.saveFile(xfer, fileBuffer)
that.terminal.write('\r\n')
},
console.error.bind(console)
)
})
const promise = new Promise((res) => {
zsession.on('session_end', () => {
res()
that.terminal.setOption('disableStdin', false)
that.keyDispose.dispose()
that.keyDispose = ''
that.zmodemMode = false
})
})
zsession.start()
return promise
},
async updateProgressX(xfer, action = 'download') {
// if (typeof action === 'undefined') action = 'download'
const detail = xfer.get_details()
const name = detail.name
const total = detail.size
const offset = xfer.get_offset()
let percent
if (total === 0 || offset === total) {
percent = 100
} else {
percent = Math.round(offset / total * 100)
}
this.terminal.write('\r' + action + ' ' + name + ': ' + bytesHuman(offset, 1) + '/' + bytesHuman(total, 1) + ' ' + percent + '% ')
},
async updateProgress(xfer, action) {
if (typeof action === 'undefined') action = 'download'
const detail = xfer.get_details()
const name = detail.name
const total = detail.size
const offset = xfer.get_offset()
if ((offset - this.lastOffset) >= this.perOffset) {
this.lastOffset = offset
let percent
if (total === 0 || offset === total) {
percent = 100
this.lastOffset = 0
} else {
percent = Math.round(offset / total * 100)
}
this.terminal.write('\r' + action + ' ' + name + ': ' + bytesHuman(offset, 1) + '/' + bytesHuman(total, 1) + ' ' + percent + '% ')
} else {
if (total === 0 || offset === total) {
this.terminal.write('\r' + action + ' ' + name + ': ' + bytesHuman(offset, 1) + '/' + bytesHuman(total, 1) + ' 100% ')
this.lastOffset = 0
}
}
},
saveFile(xfer, buffer) {
return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name)
},
sendBlockFile(session, files, options) {
// 分段上传,可以读取大文件,网页不会因内存占用奔溃
const that = this
if (!options) options = {}
const batch = []
let total_size = 0
for (let f = files.length - 1; f >= 0; f--) {
const fobj = files[f]
total_size += fobj.size
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size
}
}
let file_idx = 0
function promise_callback() {
const cur_b = batch[file_idx]
if (!cur_b) {
return Promise.resolve()
}
file_idx++
return session.send_offer(cur_b).then(function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer)
}
if (xfer === undefined) {
return promise_callback()
}
return new Promise(function(res) {
const fileSize = cur_b.size // 文件总大小
let fileLoaded = 0 // 当前已读取大小
const reader = new FileReader()
reader.onerror = function reader_onerror(e) {
console.error('file read error', e)
throw ('File read error: ' + e)
}
function readBlob() {
// 每次读取一个 block
let blob
if (cur_b.obj.slice) {
blob = cur_b.obj.slice(fileLoaded, fileLoaded + that.block + 1)
} else if (cur_b.obj.mozSlice) {
blob = cur_b.obj.mozSlice(fileLoaded, fileLoaded + that.block + 1)
} else if (cur_b.obj.webkitSlice) {
blob = cur_b.obj.webkitSlice(fileLoaded, fileLoaded + that.block + 1)
} else {
blob = cur_b.obj
}
reader.readAsArrayBuffer(blob)
}
reader.onload = function reader_onload(e) {
fileLoaded += e.total
if (fileLoaded < fileSize) {
if (e.target.result) {
const piece = new Uint8Array(e.target.result)
that.checkAborted(session)
xfer.send(piece)
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece)
}
}
readBlob()
} else {
if (e.target.result) {
const piece = new Uint8Array(e.target.result)
that.checkAborted(session)
xfer.end(piece).then(function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece)
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer)
}
res(promise_callback())
})
}
}
}
readBlob()
})
})
}
return promise_callback()
},
sendFile(session, files, options) {
// FileReader 默认分段读取,分段大小不定,会占用较大内存
// 还会造成大文件上传时超时错误
const that = this
if (!options) options = {}
const batch = []
let total_size = 0
for (let f = files.length - 1; f >= 0; f--) {
const fobj = files[f]
total_size += fobj.size
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size
}
}
let file_idx = 0
function promise_callback() {
const cur_b = batch[file_idx]
if (!cur_b) {
return Promise.resolve()
}
file_idx++
return session.send_offer(cur_b).then(function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer)
}
if (xfer === undefined) {
return promise_callback()
}
return new Promise(function(res) {
const reader = new FileReader()
reader.onerror = function reader_onerror(e) {
console.error('file read error', e)
throw ('File read error: ' + e)
}
let piece
reader.onprogress = function reader_onprogress(e) {
if (e.target.result) {
piece = new Uint8Array(e.target.result, xfer.get_offset())
that.checkAborted(session)
xfer.send(piece)
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece)
}
}
}
reader.onload = function reader_onload(e) {
piece = new Uint8Array(e.target.result, xfer, piece)
that.checkAborted(session)
xfer.end(piece).then(function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece)
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer)
}
res(promise_callback())
})
}
reader.readAsArrayBuffer(cur_b.obj)
})
})
}
return promise_callback()
},
checkAborted(session) {
if (session.aborted()) {
throw new Zmodem.Error('aborted')
}
},
sendData(data) {
this.webssh.send(JSON.stringify({ type: 'stdin', data: this.utf8_to_b64(data) }))
},
wsOnOpen() {
this.webssh.send(JSON.stringify({ type: 'addr', data: this.utf8_to_b64(this.host + ':' + this.port) }))
this.webssh.send(JSON.stringify({ type: 'login', data: this.utf8_to_b64(this.user) }))
this.webssh.send(JSON.stringify({ type: 'password', data: this.utf8_to_b64(this.pass) }))
this.webssh.send(JSON.stringify({ type: 'resize', cols: this.cols, rows: this.rows }))
this.terminal.resize(this.cols, this.rows)
// this.attachAddon = new AttachAddon(this.webssh)
// this.terminal.loadAddon(this.attachAddon)
this.webssh.onmessage = this.wsOnMessage
this.terminal.onData(this.sendData)
},
wsOnMessage(recv) {
if (typeof recv.data === 'object') {
this.zsentry.consume(recv.data)
} else {
try {
const msg = JSON.parse(recv.data)
switch (msg.type) {
case 'stdout':
case 'stderr':
this.terminal.write(this.b64_to_utf8(msg.data))
// this.terminal.writeUtf8(this.b64_to_utf8(msg.data))
break
case 'console':
console.log(this.b64_to_utf8(msg.data))
break
case 'alert':
this.$message.warning(this.b64_to_utf8(msg.data))
break
case 'disableStdin':
this.terminal.setOption('disableStdin', true)
break
case 'enableStdin':
this.terminal.setOption('disableStdin', false)
break
default:
console.log('unsupport type msg', msg)
}
} catch (e) {
console.log(e, 'unsupport data', recv.data)
}
}
},
wsOnError(err) {
this.closed = true
this.$message.error(`${this.sid} connect error`)
this.terminal.write('connect error\r\n')
console.log(err)
},
wsOnClose() {
this.closed = true
this.$message.error(`${this.sid} disconnect`)
this.terminal.write('disconnect\r\n')
// this.attachAddon.dispose()
}
}
}
</script>
这是我写的,是可以正常工作的,只能帮你到这了
我看你引入的时候 import * as Zmodem from 'zmodem.js/src/zmodem_browser' 这个文件从哪来的?我在你的项目中没有看到,你之后也是直接new Zmodem.Sentry(),我也是跟你写的一样的但会报错,所以我怀疑就是在引入zmodem的时候没引入对导致的报错
噢噢噢噢 我知道了 原来安装的依赖名字叫做zmodem.js.... 我一直安装别的名字安装不上去.. - -。
噢噢噢噢 我知道了 原来安装的依赖名字叫做zmodem.js.... 我一直安装别的名字安装不上去.. - -。
噢噢噢噢 我知道了 原来安装的依赖名字叫做zmodem.js.... 我一直安装别的名字安装不上去.. - -。
好的,感谢大佬。自己一直试着安装依赖,试了zmodemjs、zmodem-js、zmodem-browser之类的...唯独没有试zmodem.js。。。感谢大佬的醍醐灌顶~
请问这个要怎解决呀 `import { Zmodem } from '@/utils/zmodem/zmodem.devel.js' const self = this this.zsentry = new Zmodem.Sentry({ to_terminal: function(octets) { console.log(octets) }, // i.e. send to the terminal