mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-09-21 01:53:23 +00:00
Reimplement the file transfer
Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
This commit is contained in:
+7
-3
@@ -13,8 +13,7 @@
|
||||
"vue": "^2.5.17",
|
||||
"vue-i18n": "^8.1.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"xterm": "^3.7.0",
|
||||
"zmodem.js": "^0.1.7"
|
||||
"xterm": "^3.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.0.4",
|
||||
@@ -35,7 +34,12 @@
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
"vue/no-parsing-error": [2, { "x-invalid-end-tag": false }]
|
||||
"vue/no-parsing-error": [
|
||||
2,
|
||||
{
|
||||
"x-invalid-end-tag": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
const blk_size = 8912; /* 8KB */
|
||||
|
||||
function RttyFile(ws, term, opt) {
|
||||
this.state = '';
|
||||
this.ws = ws;
|
||||
this.term = term;
|
||||
this.cache = [];
|
||||
this.buffer = [];
|
||||
|
||||
this.to_term = function(octets) {
|
||||
this.term.write(Buffer.from(octets).toString());
|
||||
}
|
||||
|
||||
this.detect = function(input) {
|
||||
let type = '';
|
||||
|
||||
if (input.byteLength < 3)
|
||||
return '';
|
||||
|
||||
input = new Uint8Array(input);
|
||||
|
||||
let pos = input.indexOf(0xB6);
|
||||
if (pos < 0)
|
||||
return '';
|
||||
|
||||
if (pos > input.length - 3)
|
||||
return '';
|
||||
|
||||
if (input[pos + 1] != 0xBC)
|
||||
return '';
|
||||
|
||||
type = String.fromCharCode(input[pos + 2]);
|
||||
if (type != 's' && type != 'r')
|
||||
return '';
|
||||
|
||||
if (pos > 0)
|
||||
this.to_term(input.slice(0, pos));
|
||||
|
||||
if (type == 's')
|
||||
this.cache = Array.prototype.slice.call(input.slice(pos + 3));
|
||||
else
|
||||
this.to_term(input.slice(pos + 3));
|
||||
|
||||
this.start_ts = new Date().getTime() / 1000;
|
||||
return type;
|
||||
}
|
||||
|
||||
this.consume = function(input) {
|
||||
if (this.state == '') {
|
||||
let t = this.detect(input);
|
||||
if (t == '') {
|
||||
this.to_term(Buffer.from(input));
|
||||
return;
|
||||
}
|
||||
if (t == 'r') {
|
||||
this.state = 'send_pending';
|
||||
opt.on_detect('r');
|
||||
} else if (t == 's') {
|
||||
this.state = 'recving';
|
||||
opt.on_detect('s');
|
||||
}
|
||||
} else if (this.state == 'recving' || this.state == 'abort_recv') {
|
||||
this.recvFile(input);
|
||||
} else if (this.state == 'sending') {
|
||||
input = new Uint8Array(input);
|
||||
if (input.length == 3) {
|
||||
if (input[0] == 0xB6 && input[1] == 0xBC && String.fromCharCode(input[2]) == 'e') {
|
||||
this.state = 'abort';
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.to_term(Buffer.from(input));
|
||||
} else {
|
||||
this.to_term(Buffer.from(input));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.readFile = function(offset, size) {
|
||||
let blob = this.file.slice(offset, offset + size);
|
||||
this.fr.readAsArrayBuffer(blob);
|
||||
}
|
||||
|
||||
this.sendEof = function() {
|
||||
let b = new Uint8Array([0x03]);
|
||||
this.ws.send(b);
|
||||
this.state = '';
|
||||
}
|
||||
|
||||
this.abort = function() {
|
||||
this.state = 'abort';
|
||||
}
|
||||
|
||||
this.abortRecv = function() {
|
||||
let b = new Uint8Array([0x03]);
|
||||
this.ws.send(b);
|
||||
this.state = 'abort_recv';
|
||||
}
|
||||
|
||||
this.sendInfo = function(file) {
|
||||
let b = Buffer.alloc(6 + file.name.length);
|
||||
|
||||
b[0] = 0x01; /* packet type: file info */
|
||||
b[1] = file.name.length;
|
||||
b.write(file.name, 2);
|
||||
b.writeUInt32BE(file.size, 2 + file.name.length);
|
||||
this.ws.send(b);
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
this.sendData = function(data) {
|
||||
let b = Buffer.alloc(3);
|
||||
let piece = new Uint8Array(data);
|
||||
|
||||
b[0] = 0x02; /* packet type: file data */
|
||||
b.writeUInt16BE(piece.length, 1);
|
||||
this.ws.send(b);
|
||||
this.ws.send(piece);
|
||||
}
|
||||
|
||||
this.sendFile = function(file) {
|
||||
this.fr = new FileReader();
|
||||
|
||||
this.state = 'sending';
|
||||
let offset = 0;
|
||||
|
||||
this.sendInfo(file);
|
||||
|
||||
this.fr.onload = (e) => {
|
||||
|
||||
this.sendData(e.target.result);
|
||||
|
||||
offset += e.loaded;
|
||||
|
||||
if (this.state != 'abort' && offset < file.size) {
|
||||
this.readFile(offset, blk_size);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendEof();
|
||||
};
|
||||
|
||||
this.readFile(offset, blk_size);
|
||||
}
|
||||
|
||||
this.recvFile = function(input) {
|
||||
input = Array.prototype.slice.call(new Uint8Array(input));
|
||||
this.cache.push.apply(this.cache, input);
|
||||
|
||||
while (this.cache.length > 0) {
|
||||
let type = this.cache[0];
|
||||
|
||||
switch (type) {
|
||||
case 0x01: /* file info */
|
||||
if (this.cache.length < 2)
|
||||
return;
|
||||
let nl = this.cache[1];
|
||||
if (this.cache.length < nl + 2)
|
||||
return;
|
||||
this.cache.splice(0, 2);
|
||||
|
||||
this.name = Buffer.from(this.cache.splice(0, nl)).toString();
|
||||
this.size = Buffer.from(this.cache.splice(0, 4)).readUInt32BE(0);
|
||||
this.offset = 0;
|
||||
this.buffer = [];
|
||||
break;
|
||||
case 0x02: /* file data */
|
||||
if (this.cache.length < 3)
|
||||
return;
|
||||
|
||||
let dl = Buffer.from(this.cache.slice(1,3)).readUInt16BE(0);
|
||||
if (this.cache.length < dl + 3)
|
||||
return;
|
||||
|
||||
this.cache.splice(0, 3);
|
||||
this.buffer.push(new Uint8Array(this.cache.splice(0, dl)));
|
||||
this.offset += dl;
|
||||
|
||||
let now_ts = new Date().getTime() / 1000;
|
||||
|
||||
this.term.write(' %d%% %.2f KB %.3fs\r'.format(this.offset / this.size * 100, this.offset / 1024, now_ts - this.start_ts));
|
||||
break;
|
||||
case 0x03: /* file eof */
|
||||
this.cache = [];
|
||||
|
||||
this.term.write('\n');
|
||||
|
||||
if (this.state == 'abort_recv') {
|
||||
this.state = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = '';
|
||||
|
||||
let blob = new Blob(this.buffer);
|
||||
let url = URL.createObjectURL(blob);
|
||||
|
||||
let el = document.createElement("a");
|
||||
el.style.display = "none";
|
||||
el.href = url;
|
||||
el.download = this.name;
|
||||
document.body.appendChild(el);
|
||||
el.click();
|
||||
document.body.removeChild(el);
|
||||
break;
|
||||
default:
|
||||
console.error('invalid type:' + type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RttyFile;
|
||||
+62
-60
@@ -1,61 +1,63 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import enLocale from 'iview/dist/locale/en-US'
|
||||
import zhLocale from 'iview/dist/locale/zh-CN'
|
||||
|
||||
const RttyI18n = {
|
||||
'en-US': {
|
||||
'upfile-info': 'The file "{name}" will be saved in the "/tmp/" directory of your device.',
|
||||
'device-count': 'Online Device: {count}'
|
||||
},
|
||||
'zh-CN': {
|
||||
'Description': '描述',
|
||||
'Uptime': '在线时长',
|
||||
'Connect': '连接',
|
||||
'Please enter the filter key...': '请输入关键字进行过滤……',
|
||||
'No devices connected': '没有设备连接',
|
||||
'Upload file to device': '上传文件到设备',
|
||||
'Download file from device': '从设备下载文件',
|
||||
'Increase font size': '增大字体',
|
||||
'Decrease font size': '减小字体',
|
||||
'Please select the file to upload': '请选择您要上传的文件',
|
||||
'Uploading': '正在上传',
|
||||
'Click to upload': '上传',
|
||||
'Upload success': '上传成功',
|
||||
'Download Finish': '下载成功',
|
||||
'Upload canceled': '上传终止',
|
||||
'Download canceled': '下载终止',
|
||||
'Device offline': '设备离线',
|
||||
'modification': '修改时间',
|
||||
'upfile-info': '文件"{name}"将会保存到你的设备的"/tmp"目录',
|
||||
'Name': '名称',
|
||||
'Size': '大小',
|
||||
'Authorization Required': '需要授权',
|
||||
'Enter username...': '请输入用户名...',
|
||||
'Enter password...': '请输入密码...',
|
||||
'Login': '登录',
|
||||
'username is required': '用户名为必填',
|
||||
'Login Fail! username or password wrong.': '登录失败,用户名或密码错误',
|
||||
'Connect failed': '连接失败',
|
||||
'device-count': '在线设备数:{count}',
|
||||
'Cannot be greater than 500MB': '不能大于500MB',
|
||||
'Sessions is full':'会话已满'
|
||||
}
|
||||
}
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
const messages = {
|
||||
'zh-CN': Object.assign(zhLocale, RttyI18n['zh-CN']),
|
||||
'en-US': Object.assign(enLocale, RttyI18n['en-US'])
|
||||
};
|
||||
|
||||
let language = navigator.language;
|
||||
|
||||
if (!messages[language])
|
||||
language = 'en-US';
|
||||
|
||||
export default new VueI18n({
|
||||
locale: language,
|
||||
messages: messages
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import enLocale from 'iview/dist/locale/en-US'
|
||||
import zhLocale from 'iview/dist/locale/zh-CN'
|
||||
|
||||
const RttyI18n = {
|
||||
'en-US': {
|
||||
'upfile-info': 'The file "{name}" will be saved in the "/tmp/" directory of your device.',
|
||||
'device-count': 'Online Device: {count}'
|
||||
},
|
||||
'zh-CN': {
|
||||
'Description': '描述',
|
||||
'Uptime': '在线时长',
|
||||
'Connect': '连接',
|
||||
'Please enter the filter key...': '请输入关键字进行过滤……',
|
||||
'No devices connected': '没有设备连接',
|
||||
'Upload file to device': '上传文件到设备',
|
||||
'Download file from device': '从设备下载文件',
|
||||
'Increase font size': '增大字体',
|
||||
'Decrease font size': '减小字体',
|
||||
'Please select the file to upload': '请选择您要上传的文件',
|
||||
'Uploading': '正在上传',
|
||||
'Click to upload': '上传',
|
||||
'Upload success': '上传成功',
|
||||
'Download Finish': '下载成功',
|
||||
'Upload canceled': '上传终止',
|
||||
'Download canceled': '下载终止',
|
||||
'Device offline': '设备离线',
|
||||
'modification': '修改时间',
|
||||
'upfile-info': '文件"{name}"将会保存到你的设备的"/tmp"目录',
|
||||
'Name': '名称',
|
||||
'Size': '大小',
|
||||
'Authorization Required': '需要授权',
|
||||
'Enter username...': '请输入用户名...',
|
||||
'Enter password...': '请输入密码...',
|
||||
'Login': '登录',
|
||||
'username is required': '用户名为必填',
|
||||
'Login Fail! username or password wrong.': '登录失败,用户名或密码错误',
|
||||
'Connect failed': '连接失败',
|
||||
'device-count': '在线设备数:{count}',
|
||||
'Cannot be greater than 500MB': '不能大于500MB',
|
||||
'Sessions is full':'会话已满',
|
||||
'The file name too long':'文件名太长',
|
||||
'Only one file can be uploaded at the same time':'同一时刻只能上传一个文件'
|
||||
}
|
||||
}
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
const messages = {
|
||||
'zh-CN': Object.assign(zhLocale, RttyI18n['zh-CN']),
|
||||
'en-US': Object.assign(enLocale, RttyI18n['en-US'])
|
||||
};
|
||||
|
||||
let language = navigator.language;
|
||||
|
||||
if (!messages[language])
|
||||
language = 'en-US';
|
||||
|
||||
export default new VueI18n({
|
||||
locale: language,
|
||||
messages: messages
|
||||
});
|
||||
+34
-143
@@ -20,7 +20,7 @@ import { Terminal } from 'xterm'
|
||||
import 'xterm/lib/xterm.css'
|
||||
import * as fit from 'xterm/lib/addons/fit/fit'
|
||||
import * as overlay from '@/overlay'
|
||||
import 'zmodem.js/dist/zmodem'
|
||||
import RttyFile from '../plugins/rtty-file'
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(overlay);
|
||||
@@ -51,15 +51,18 @@ export default {
|
||||
this.$Message.warning(this.$t('Cannot be greater than 500MB'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.name.length > 255) {
|
||||
this.$Message.warning(this.$t('The file name too long'));
|
||||
return false;
|
||||
}
|
||||
|
||||
this.upfile.file = file;
|
||||
return false;
|
||||
},
|
||||
cancelUpfile() {
|
||||
let zsession = this.zsentry.get_confirmed_session();
|
||||
if (zsession) {
|
||||
zsession.abort();
|
||||
this.term.focus();
|
||||
}
|
||||
this.term.focus();
|
||||
this.rf.sendEof();
|
||||
},
|
||||
formatTime(ts) {
|
||||
let td = 0;
|
||||
@@ -83,13 +86,6 @@ export default {
|
||||
|
||||
return (td > 0) ? '%02d:%02d:%02d:%02d'.format(td, th, tm, ts) : '%02d:%02d:%02d'.format(th, tm, ts);
|
||||
},
|
||||
updateProgress(offset, size, start) {
|
||||
let now = Math.floor(new Date().getTime() / 1000);
|
||||
let percent = 100 * offset / size;
|
||||
let consumed = now - start;
|
||||
offset /= 1024;
|
||||
this.term.write(' %d%% %d KB %d KB/sec %s\r'.format(percent, offset, offset / consumed, this.formatTime(consumed)));
|
||||
},
|
||||
doUpload() {
|
||||
if (!this.upfile.file) {
|
||||
this.$Message.error(this.$t('Select the file to upload'));
|
||||
@@ -98,105 +94,7 @@ export default {
|
||||
|
||||
this.upfile.modal = false;
|
||||
this.term.focus();
|
||||
|
||||
this.handleSendSession(this.zsentry.get_confirmed_session(), this.upfile.file);
|
||||
},
|
||||
readFile(file, fr, offset, size) {
|
||||
let blob = file.slice(offset, offset + size);
|
||||
fr.readAsArrayBuffer(blob);
|
||||
},
|
||||
handleReceiveSession(zsession) {
|
||||
zsession.on("offer", (xfer) => {
|
||||
let start_time = Math.floor(new Date().getTime() / 1000);
|
||||
let fileInfo = xfer.get_details();
|
||||
let size = fileInfo.size;
|
||||
let buffer = [];
|
||||
|
||||
this.term.write('Transferring ' + fileInfo.name + '...\n\r');
|
||||
this.updateProgress(0, size, start_time);
|
||||
|
||||
xfer.on("input", (payload) => {
|
||||
this.updateProgress(xfer.get_offset(), size, start_time);
|
||||
buffer.push(new Uint8Array(payload));
|
||||
});
|
||||
|
||||
xfer.accept().then(() => {
|
||||
window.Zmodem.Browser.save_to_disk(buffer, fileInfo.name);
|
||||
|
||||
/* Maybe lose the 'OO' from the sz command. */
|
||||
setTimeout(() => {
|
||||
let zsession = this.zsentry.get_confirmed_session();
|
||||
if (zsession)
|
||||
zsession.abort();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
zsession.on("session_end", () => {
|
||||
this.term.write('\n');
|
||||
this.ws.send(Buffer.from('\n'));
|
||||
});
|
||||
|
||||
zsession.start();
|
||||
},
|
||||
handleSendSession(zsession, file) {
|
||||
let start_time = Math.floor(new Date().getTime() / 1000);
|
||||
let batch = {
|
||||
obj: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mtime: new Date(file.lastModified),
|
||||
files_remaining: 1,
|
||||
bytes_remaining: file.size
|
||||
};
|
||||
|
||||
zsession.send_offer(batch).then((xfer) => {
|
||||
this.term.write('Transferring ' + file.name + '...\n\r');
|
||||
if (xfer) {
|
||||
this.updateProgress(0, batch.size, start_time);
|
||||
} else {
|
||||
this.term.write(file.name + ' was skipped\n\r');
|
||||
zsession.close().then(() => {
|
||||
this.term.write('\n');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let reader = new FileReader();
|
||||
|
||||
//This really shouldn’t happen … so let’s
|
||||
//blow up if it does.
|
||||
reader.onerror = (e) => {
|
||||
throw("File read error: " + e);
|
||||
};
|
||||
|
||||
reader.onload = (e) => {
|
||||
let piece;
|
||||
|
||||
if (zsession.aborted())
|
||||
return;
|
||||
|
||||
if (e.target.result.byteLength > 0) {
|
||||
piece = new Uint8Array(e.target.result, xfer, piece);
|
||||
xfer.send(piece);
|
||||
|
||||
this.updateProgress(xfer.get_offset(), batch.size, start_time);
|
||||
}
|
||||
|
||||
if (xfer.get_offset() == batch.size) {
|
||||
xfer.end(piece).then(() => {
|
||||
zsession.close().then(() => {
|
||||
this.term.write('\n');
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.readFile(file, reader, xfer.get_offset(), 8192);
|
||||
};
|
||||
|
||||
this.readFile(file, reader, 0, 8192);
|
||||
});
|
||||
this.rf.sendFile(this.upfile.file);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -239,34 +137,17 @@ export default {
|
||||
|
||||
this.term = term;
|
||||
|
||||
let zsentry = new window.Zmodem.Sentry({
|
||||
to_terminal: (octets) => {
|
||||
this.term.write(Buffer.from(octets).toString());
|
||||
},
|
||||
sender: (octets) => {
|
||||
this.ws.send(Buffer.from(octets));
|
||||
},
|
||||
on_retract: () => {
|
||||
this.upfile.modal = false;
|
||||
},
|
||||
on_detect: (detection) => {
|
||||
let zsession = detection.confirm();
|
||||
setTimeout(() => {
|
||||
term.write('\n\rStarting zmodem transfer. Press Ctrl+C to cancel.\n\r');
|
||||
|
||||
if (zsession.type === "send")
|
||||
this.upfile = {modal: true, file: null};
|
||||
else
|
||||
this.handleReceiveSession(zsession);
|
||||
}, 10);
|
||||
this.rf = new RttyFile(ws, term, {
|
||||
on_detect: (t) => {
|
||||
if (t == 'r')
|
||||
this.upfile.modal = true;
|
||||
else if (t == 's')
|
||||
;
|
||||
}
|
||||
});
|
||||
|
||||
this.zsentry = zsentry;
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
let zsentry = this.zsentry;
|
||||
let term = this.term;
|
||||
|
||||
if (typeof ev.data == 'string') {
|
||||
@@ -286,14 +167,24 @@ export default {
|
||||
ws.send(JSON.stringify(msg));
|
||||
|
||||
term.on('data', (data) => {
|
||||
let zsession = zsentry.get_confirmed_session();
|
||||
if (zsession) {
|
||||
if (zsession.aborted())
|
||||
return;
|
||||
if (this.rf.state != '') {
|
||||
if (data.length == 1) {
|
||||
let key = data.charCodeAt(0);
|
||||
|
||||
/* Ctrl + C */
|
||||
if (data.length == 1 && data.charCodeAt(0) == 3)
|
||||
zsession.abort();
|
||||
/* Ctrl + C, Esc */
|
||||
if (key == 3 || key == 27) {
|
||||
if (this.rf.state == 'recving') {
|
||||
this.rf.abortRecv();
|
||||
} else {
|
||||
this.upfile.modal = false;
|
||||
|
||||
if (this.rf.state == 'send_pending')
|
||||
this.rf.sendEof();
|
||||
else
|
||||
this.rf.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -321,7 +212,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
zsentry.consume(ev.data);
|
||||
this.rf.consume(ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user