zoukankan      html  css  js  c++  java
  • 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);
    }
    
  • 相关阅读:
    2017年5月15号课堂笔记
    2017年5月12号课堂笔记
    2017年5月8号课堂笔记
    2017年5月5号课堂笔记
    2017年4月26号课堂笔记
    不忘初心,坚持走下去
    2017年4月24号课堂笔记
    2017年4月21号课堂笔记
    2017年4月19号课堂笔记
    autoit UIA获取Listview的信息
  • 原文地址:https://www.cnblogs.com/redirect/p/10066731.html
Copyright © 2011-2022 走看看