guacamole实现上传下载
分析的入手点,查看websocket连接的frame
看到首先服务端向客户端发送了filesystem请求,紧接着浏览器向服务端发送了get请求,并且后面带有根目录标识(“/”)。
1. 源码解读
查看指令
/**
* Handlers for all instruction opcodes receivable by a Guacamole protocol
* client.
* @private
*/
var instructionHandlers = {
...其它指令
"filesystem" : function handleFilesystem(parameters) {
var objectIndex = parseInt(parameters[0]);
var name = parameters[1];
// Create object, if supported
if (guac_client.onfilesystem) {
//这里实例化一个object,并且传递给客户端监听的onfilesystem方法
var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex);
guac_client.onfilesystem(object, name);
}
// If unsupported, simply ignore the availability of the filesystem
},
...其它指令
}
查看实例化的object源码
/**
* An object used by the Guacamole client to house arbitrarily-many named
* input and output streams.
*
* @constructor
* @param {Guacamole.Client} client
* The client owning this object.
*
* @param {Number} index
* The index of this object.
*/
Guacamole.Object = function guacamoleObject(client, index) {
/**
* Reference to this Guacamole.Object.
*
* @private
* @type {Guacamole.Object}
*/
var guacObject = this;
/**
* Map of stream name to corresponding queue of callbacks. The queue of
* callbacks is guaranteed to be in order of request.
*
* @private
* @type {Object.<String, Function[]>}
*/
var bodyCallbacks = {};
/**
* Removes and returns the callback at the head of the callback queue for
* the stream having the given name. If no such callbacks exist, null is
* returned.
*
* @private
* @param {String} name
* The name of the stream to retrieve a callback for.
*
* @returns {Function}
* The next callback associated with the stream having the given name,
* or null if no such callback exists.
*/
var dequeueBodyCallback = function dequeueBodyCallback(name) {
// If no callbacks defined, simply return null
var callbacks = bodyCallbacks[name];
if (!callbacks)
return null;
// Otherwise, pull off first callback, deleting the queue if empty
var callback = callbacks.shift();
if (callbacks.length === 0)
delete bodyCallbacks[name];
// Return found callback
return callback;
};
/**
* Adds the given callback to the tail of the callback queue for the stream
* having the given name.
*
* @private
* @param {String} name
* The name of the stream to associate with the given callback.
*
* @param {Function} callback
* The callback to add to the queue of the stream with the given name.
*/
var enqueueBodyCallback = function enqueueBodyCallback(name, callback) {
// Get callback queue by name, creating first if necessary
var callbacks = bodyCallbacks[name];
if (!callbacks) {
callbacks = [];
bodyCallbacks[name] = callbacks;
}
// Add callback to end of queue
callbacks.push(callback);
};
/**
* The index of this object.
*
* @type {Number}
*/
this.index = index;
/**
* Called when this object receives the body of a requested input stream.
* By default, all objects will invoke the callbacks provided to their
* requestInputStream() functions based on the name of the stream
* requested. This behavior can be overridden by specifying a different
* handler here.
*
* @event
* @param {Guacamole.InputStream} inputStream
* The input stream of the received body.
*
* @param {String} mimetype
* The mimetype of the data being received.
*
* @param {String} name
* The name of the stream whose body has been received.
*/
this.onbody = function defaultBodyHandler(inputStream, mimetype, name) {
// Call queued callback for the received body, if any
var callback = dequeueBodyCallback(name);
if (callback)
callback(inputStream, mimetype);
};
/**
* Called when this object is being undefined. Once undefined, no further
* communication involving this object may occur.
*
* @event
*/
this.onundefine = null;
/**
* Requests read access to the input stream having the given name. If
* successful, a new input stream will be created.
*
* @param {String} name
* The name of the input stream to request.
*
* @param {Function} [bodyCallback]
* The callback to invoke when the body of the requested input stream
* is received. This callback will be provided a Guacamole.InputStream
* and its mimetype as its two only arguments. If the onbody handler of
* this object is overridden, this callback will not be invoked.
*/
this.requestInputStream = function requestInputStream(name, bodyCallback) {
// Queue body callback if provided
if (bodyCallback)
enqueueBodyCallback(name, bodyCallback);
// Send request for input stream
client.requestObjectInputStream(guacObject.index, name);
};
/**
* Creates a new output stream associated with this object and having the
* given mimetype and name. The legality of a mimetype and name is dictated
* by the object itself.
*
* @param {String} mimetype
* The mimetype of the data which will be sent to the output stream.
*
* @param {String} name
* The defined name of an output stream within this object.
*
* @returns {Guacamole.OutputStream}
* An output stream which will write blobs to the named output stream
* of this object.
*/
this.createOutputStream = function createOutputStream(mimetype, name) {
return client.createObjectOutputStream(guacObject.index, mimetype, name);
};
};
读取下官方的注释,关于此类的定义:
An object used by the Guacamole client to house arbitrarily-many named input and output streams.
我们需要操作的应该就是input 和 output stream,下面我们进行下猜测
1> this.onbody对应的方法应该就是我们需要实际处理inputStream的地方,
2> this.requestInputStream后面调用了client.requestObjectInputStream(guacObject.index, name);方法,源码如下:
Guacamole.Client = function(tunnel) {
...其它内容
this.requestObjectInputStream = function requestObjectInputStream(index, name) {
// Do not send requests if not connected
if (!isConnected())
return;
tunnel.sendMessage("get", index, name);
};
...其它内容
}
可以看出这个方法就是向服务端方发送get请求。我们上面分析websocket请求的时候,提到过向客户端发送过这样一个请求,并且在一直监听的onbody方法中应该能收到服务器返回的响应。
3> this.createOutputStream应该是创建了一个通往guacamole服务器的stream,我们上传文件的时候可能会用到这个stream,调用了client.createObjectOutputStream(guacObject.index, mimetype, name);方法,其源码如下:
Guacamole.Client = function(tunnel) {
...其它内容
/**
* Creates a new output stream associated with the given object and having
* the given mimetype and name. The legality of a mimetype and name is
* dictated by the object itself. The instruction necessary to create this
* stream will automatically be sent.
*
* @param {Number} index
* The index of the object for which the output stream is being
* created.
*
* @param {String} mimetype
* The mimetype of the data which will be sent to the output stream.
*
* @param {String} name
* The defined name of an output stream within the given object.
*
* @returns {Guacamole.OutputStream}
* An output stream which will write blobs to the named output stream
* of the given object.
*/
this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) {
// 得到了stream,并向服务端发送了put请求
// Allocate and ssociate stream with object metadata
var stream = guac_client.createOutputStream();
tunnel.sendMessage("put", index, stream.index, mimetype, name);
return stream;
};
...其它内容
}
继续往下追diamante, 这句var stream = guac_client.createOutputStream(); 源码如下:
Guacamole.Client = function(tunnel) {
...其它内容
/**
* Allocates an available stream index and creates a new
* Guacamole.OutputStream using that index, associating the resulting
* stream with this Guacamole.Client. Note that this stream will not yet
* exist as far as the other end of the Guacamole connection is concerned.
* Streams exist within the Guacamole protocol only when referenced by an
* instruction which creates the stream, such as a "clipboard", "file", or
* "pipe" instruction.
*
* @returns {Guacamole.OutputStream}
* A new Guacamole.OutputStream with a newly-allocated index and
* associated with this Guacamole.Client.
*/
this.createOutputStream = function createOutputStream() {
// Allocate index
var index = stream_indices.next();
// Return new stream
var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
return stream;
};
...其它内容
}
再继续,new Guacamole.OutputStream(guac_client, index);源码:
/**
* Abstract stream which can receive data.
*
* @constructor
* @param {Guacamole.Client} client The client owning this stream.
* @param {Number} index The index of this stream.
*/
Guacamole.OutputStream = function(client, index) {
/**
* Reference to this stream.
* @private
*/
var guac_stream = this;
/**
* The index of this stream.
* @type {Number}
*/
this.index = index;
/**
* Fired whenever an acknowledgement is received from the server, indicating
* that a stream operation has completed, or an error has occurred.
*
* @event
* @param {Guacamole.Status} status The status of the operation.
*/
this.onack = null;
/**
* Writes the given base64-encoded data to this stream as a blob.
*
* @param {String} data The base64-encoded data to send.
*/
this.sendBlob = function(data) {
//发送数据到服务端,并且数据格式应该为该base64-encoded data格式,分块传输过去的
client.sendBlob(guac_stream.index, data);
};
/**
* Closes this stream.
*/
this.sendEnd = function() {
client.endStream(guac_stream.index);
};
};
到此,我们可以知道this.createOutputStream是做的是事情就是建立了一个通往服务器的stream通道,并且,我们可以操作这个通道发送分块数据(stream.sendBlob方法)。
2. 上传下载的核心代码
关于文件系统和下载的代码
var fileSystem;
//初始化文件系统
client.onfilesystem = function(object){
fileSystem=object;
//监听onbody事件,对返回值进行处理,返回内容可能有两种,一种是文件夹,一种是文件。
object.onbody = function(stream, mimetype, filename){
stream.sendAck('OK', Guacamole.Status.Code.SUCCESS);
downloadFile(stream, mimetype, filename);
}
}
//连接有滞后,初始化文件系统给个延迟
setTimeout(function(){
//从根目录开始,想服务端发送get请求
let path = '/';
fileSystem.requestInputStream(path);
}, 5000);
downloadFile = (stream, mimetype, filename) => {
//使用blob reader处理数据
var blob_builder;
if (window.BlobBuilder) blob_builder = new BlobBuilder();
else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder();
else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder();
else
blob_builder = new (function() {
var blobs = [];
/** @ignore */
this.append = function(data) {
blobs.push(new Blob([data], {"type": mimetype}));
};
/** @ignore */
this.getBlob = function() {
return new Blob(blobs, {"type": mimetype});
};
})();
// 收到blob的处理,因为收到的可能是一块一块的数据,需要把他们整合,这里用到了blob_builder
stream.onblob = function(data) {
// Convert to ArrayBuffer
var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
blob_builder.append(arrayBuffer);
length += arrayBuffer.byteLength;
// Send success response
stream.sendAck("OK", 0x0000);
};
// 结束后的操作
stream.onend = function(){
//获取整合后的数据
var blob_data = blob_builder.getBlob();
//数据传输完成后进行下载等处理
if(mimetype.indexOf('stream-index+json') != -1){
//如果是文件夹,需要解决如何将数据读出来,这里使用filereader读取blob数据,最后得到一个json格式数据
var blob_reader = new FileReader();
blob_reader.addEventListener("loadend", function() {
let folder_content = JSON.parse(blob_reader.result)
//这里加入自己代码,实现文件目录的ui,重新组织当前文件目录
});
blob_reader.readAsBinaryString(blob_data);
} else {
//如果是文件,直接下载,但是需要解决个问题,就是如何下载blob数据
//借鉴了https://github.com/eligrey/FileSaver.js这个库
var file_arr = filename.split("/");
var download_file_name = file_arr[file_arr.length - 1];
saveAs(blob_data, download_file_name);
}
}
}
感受下console.log(blob_data)和 console.log(folder_data)的内容如下
关于上传的代码
const input = document.getElementById('file-input');
input.onchange = function() {
const file = input.files[0];
//上传开始
uploadFile(fileSystem, file);
};
uploadFile = (object, file) => {
const _this = this;
const fileUpload = {};
//需要读取文件内容,使用filereader
const reader = new FileReader();
var current_path = $("#header_title").text(); //上传到堡垒机的目录,可以自己动态获取
var STREAM_BLOB_SIZE = 4096;
reader.onloadend = function fileContentsLoaded() {
//上面源码分析过,这里先创建一个连接服务端的数据通道
const stream = object.createOutputStream(file.type, current_path + '/' + file.name);
const bytes = new Uint8Array(reader.result);
let offset = 0;
let progress = 0;
fileUpload.name = file.name;
fileUpload.mimetype = file.type;
fileUpload.length = bytes.length;
stream.onack = function ackReceived(status) {
if (status.isError()) {
//提示错误信息
//layer.msg(status.message);
return false;
}
const slice = bytes.subarray(offset, offset + STREAM_BLOB_SIZE);
const base64 = bufferToBase64(slice);
// Write packet
stream.sendBlob(base64);
// Advance to next packet
offset += STREAM_BLOB_SIZE;
if (offset >= bytes.length) {
stream.sendEnd();
}
}
};
reader.readAsArrayBuffer(file);
return fileUpload;
};
function bufferToBase64(buf) {
var binstr = Array.prototype.map.call(buf, function (ch) {
return String.fromCharCode(ch);
}).join('');
return btoa(binstr);
}