Fork me on GitHub

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);
}
posted @ 2018-12-04 20:42  archer-wong  阅读(4497)  评论(6编辑  收藏  举报