Reimplement the file transfer

Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
This commit is contained in:
Jianhui Zhao
2018-11-27 22:44:17 +08:00
parent d7e8883f28
commit 75481f93bc
5 changed files with 318 additions and 207 deletions
+7 -3
View File
@@ -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"
+214
View File
@@ -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
View File
@@ -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
View File
@@ -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 shouldnt happen … so lets
//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
View File
File diff suppressed because one or more lines are too long