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);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM