From 2c977a31349848826596ef510993a45217145ccc Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 2 Jun 2016 15:40:09 -0700 Subject: [PATCH 1/4] GUACAMOLE-44: Explicitly define and document the magic 6048-byte blob within ArrayBufferWriter. Allow the blob size to be overridden. --- .../main/webapp/modules/ArrayBufferWriter.js | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js b/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js index 8f33dd2be..2fc3e938b 100644 --- a/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js +++ b/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js @@ -61,6 +61,20 @@ Guacamole.ArrayBufferWriter = function(stream) { } + /** + * The maximum length of any blob sent by this Guacamole.ArrayBufferWriter, + * in bytes. Data sent via + * {@link Guacamole.ArrayBufferWriter#sendData|sendData()} which exceeds + * this length will be split into multiple blobs. As the Guacamole protocol + * limits the maximum size of any instruction or instruction element to + * 8192 bytes, and the contents of blobs will be base64-encoded, this value + * should only be increased with extreme caution. + * + * @type {Number} + * @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH} + */ + this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH; + /** * Sends the given data. * @@ -71,13 +85,13 @@ Guacamole.ArrayBufferWriter = function(stream) { var bytes = new Uint8Array(data); // If small enough to fit into single instruction, send as-is - if (bytes.length <= 6048) + if (bytes.length <= guac_writer.blobLength) __send_blob(bytes); // Otherwise, send as multiple instructions else { - for (var offset=0; offset Date: Thu, 2 Jun 2016 15:41:08 -0700 Subject: [PATCH 2/4] GUACAMOLE-44: Implement Guacamole.FileWriter which provides for streaming local files over a Guacamole.OutputStream. --- .../src/main/webapp/modules/FileWriter.js | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 guacamole-common-js/src/main/webapp/modules/FileWriter.js diff --git a/guacamole-common-js/src/main/webapp/modules/FileWriter.js b/guacamole-common-js/src/main/webapp/modules/FileWriter.js new file mode 100644 index 000000000..3d221ef4b --- /dev/null +++ b/guacamole-common-js/src/main/webapp/modules/FileWriter.js @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var Guacamole = Guacamole || {}; + +/** + * A writer which automatically writes to the given output stream with the + * contents of a local files, supplied as standard File objects. + * + * @constructor + * @param {Guacamole.OutputStream} stream + * The stream that data will be written to. + */ +Guacamole.FileWriter = function FileWriter(stream) { + + /** + * Reference to this Guacamole.FileWriter. + * + * @private + * @type {Guacamole.FileWriter} + */ + var guacWriter = this; + + /** + * Wrapped Guacamole.ArrayBufferWriter which will be used to send any + * provided file data. + * + * @private + * @type {Guacamole.ArrayBufferWriter} + */ + var arrayBufferWriter = new Guacamole.ArrayBufferWriter(stream); + + // Initially, simply call onack for acknowledgements + arrayBufferWriter.onack = function(status) { + if (guacWriter.onack) + guacWriter.onack(status); + }; + + /** + * Browser-independent implementation of Blob.slice() which uses an end + * offset to determine the span of the resulting slice, rather than a + * length. + * + * @private + * @param {Blob} blob + * The Blob to slice. + * + * @param {Number} start + * The starting offset of the slice, in bytes, inclusive. + * + * @param {Number} end + * The ending offset of the slice, in bytes, exclusive. + * + * @returns {Blob} + * A Blob containing the data within the given Blob starting at + * start and ending at end - 1. + */ + var slice = function slice(blob, start, end) { + + // Use prefixed implementations if necessary + var sliceImplementation = ( + blob.slice + || blob.webkitSlice + || blob.mozSlice + ).bind(blob); + + var length = end - start; + + // The old Blob.slice() was length-based (not end-based). Try the + // length version first, if the two calls are not equivalent. + if (length !== end) { + + // If the result of the slice() call matches the expected length, + // trust that result. It must be correct. + var sliceResult = sliceImplementation(start, length); + if (sliceResult.size === length) + return sliceResult; + + } + + // Otherwise, use the most-recent standard: end-based slice() + return sliceImplementation(start, end); + + }; + + /** + * Sends the contents of the given file over the underlying stream. + * + * @param {File} file + * The file to send. + */ + this.sendFile = function sendFile(file) { + + var offset = 0; + var reader = new FileReader(); + + /** + * Reads the next chunk of the file provided to + * {@link Guacamole.FileWriter#sendFile|sendFile()}. The chunk itself + * is read asynchronously, and will not be available until + * reader.onload fires. + * + * @private + */ + var readNextChunk = function readNextChunk() { + + // If no further chunks remain, inform of completion and stop + if (offset >= file.size) { + + // Fire completion event for completed file + if (guacWriter.oncomplete) + guacWriter.oncomplete(file); + + // No further chunks to read + return; + + } + + // Obtain reference to next chunk as a new blob + var chunk = slice(file, offset, offset + arrayBufferWriter.blobLength); + offset += arrayBufferWriter.blobLength; + + // Attempt to read the file contents represented by the blob into + // a new array buffer + reader.readAsArrayBuffer(chunk); + + }; + + // Send each chunk over the stream, continue reading the next chunk + reader.onload = function chunkLoadComplete() { + + // Send the successfully-read chunk + arrayBufferWriter.sendData(reader.result); + + // Continue sending more chunks after the latest chunk is + // acknowledged + arrayBufferWriter.onack = function sendMoreChunks(status) { + + if (guacWriter.onack) + guacWriter.onack(status); + + // Abort transfer if an error occurs + if (status.isError()) + return; + + // Inform of file upload progress via progress events + if (guacWriter.onprogress) + guacWriter.onprogress(file, offset - arrayBufferWriter.blobLength); + + // Queue the next chunk for reading + readNextChunk(); + + }; + + }; + + // If an error prevents further reading, inform of error and stop + reader.onerror = function chunkLoadFailed() { + + // Fire error event, including the context of the error + if (guacWriter.onerror) + guacWriter.onerror(file, offset, reader.error); + + }; + + // Begin reading the first chunk + readNextChunk(); + + }; + + /** + * Signals that no further text will be sent, effectively closing the + * stream. + */ + this.sendEnd = function sendEnd() { + arrayBufferWriter.sendEnd(); + }; + + /** + * Fired for received data, if acknowledged by the server. + * + * @event + * @param {Guacamole.Status} status + * The status of the operation. + */ + this.onack = null; + + /** + * Fired when an error occurs reading a file passed to + * {@link Guacamole.FileWriter#sendFile|sendFile()}. The file transfer for + * the given file will cease, but the stream will remain open. + * + * @event + * @param {File} file + * The file that was being read when the error occurred. + * + * @param {Number} offset + * The offset of the failed read attempt within the file, in bytes. + * + * @param {DOMError} error + * The error that occurred. + */ + this.onerror = null; + + /** + * Fired for each successfully-read chunk of file data as a file is being + * sent via {@link Guacamole.FileWriter#sendFile|sendFile()}. + * + * @event + * @param {File} file + * The file that is being read. + * + * @param {Number} offset + * The offset of the read that just succeeded. + */ + this.onprogress = null; + + /** + * Fired when a file passed to + * {@link Guacamole.FileWriter#sendFile|sendFile()} has finished being sent. + * + * @event + * @param {File} file + * The file that was sent. + */ + this.oncomplete = null; + +}; From 2934f4a9be67952d2be03124fc4ff34a3fd9c33b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 2 Jun 2016 15:51:15 -0700 Subject: [PATCH 3/4] GUACAMOLE-44: Use Guacamole.FileWriter within ManagedFileUpload (rather than load entire file into memory). --- .../app/client/types/ManagedFileUpload.js | 156 ++++++++---------- 1 file changed, 72 insertions(+), 84 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js b/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js index 50d8125ab..761790f5f 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js @@ -29,16 +29,6 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector' // Required services var $window = $injector.get('$window'); - /** - * The maximum number of bytes to include in each blob for the Guacamole - * file stream. Note that this, along with instruction opcode and protocol- - * related overhead, must not exceed the 8192 byte maximum imposed by the - * Guacamole protocol. - * - * @type Number - */ - var STREAM_BLOB_SIZE = 4096; - /** * Object which serves as a surrogate interface, encapsulating a Guacamole * file upload while it is active, allowing it to be detached and @@ -136,93 +126,91 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector' ManagedFileUpload.getInstance = function getInstance(client, file, object, streamName) { var managedFileUpload = new ManagedFileUpload(); + var streamAcknowledged = false; - // Construct reader for file - var reader = new FileReader(); - reader.onloadend = function fileContentsLoaded() { + // Open file for writing + var stream; + if (!object) + stream = client.createFileStream(file.type, file.name); - // Open file for writing - var stream; - if (!object) - stream = client.createFileStream(file.type, file.name); + // If object/streamName specified, upload to that instead of a file + // stream + else + stream = object.createOutputStream(file.type, streamName); - // If object/streamName specified, upload to that instead of a file - // stream - else - stream = object.createOutputStream(file.type, streamName); + // Notify that the file transfer is pending + $rootScope.$apply(function uploadStreamOpen() { - var valid = true; - var bytes = new Uint8Array(reader.result); - var offset = 0; + // Init managed upload + managedFileUpload.filename = file.name; + managedFileUpload.mimetype = file.type; + managedFileUpload.progress = 0; + managedFileUpload.length = file.size; - $rootScope.$apply(function uploadStreamOpen() { + // Notify that stream is open + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.OPEN); - // Init managed upload - managedFileUpload.filename = file.name; - managedFileUpload.mimetype = file.type; - managedFileUpload.progress = 0; - managedFileUpload.length = bytes.length; + }); - // Notify that stream is open - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.OPEN); + var writer = new Guacamole.FileWriter(stream); - }); - - // Invalidate stream on all errors - // Continue upload when acknowledged - stream.onack = function ackReceived(status) { - - // Handle errors - if (status.isError()) { - valid = false; - $rootScope.$apply(function uploadStreamError() { - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.ERROR, - status.code); - }); - } - - // Abort upload if stream is invalid - if (!valid) - return false; - - // Encode packet as base64 - var slice = bytes.subarray(offset, offset + STREAM_BLOB_SIZE); - var base64 = getBase64(slice); - - // Write packet - stream.sendBlob(base64); - - // Advance to next packet - offset += STREAM_BLOB_SIZE; - - $rootScope.$apply(function uploadStreamProgress() { - - // If at end, stop upload - if (offset >= bytes.length) { - stream.sendEnd(); - managedFileUpload.progress = bytes.length; - - // Upload complete - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.CLOSED); - - // Notify of upload completion - $rootScope.$broadcast('guacUploadComplete', file.name); - - } - - // Otherwise, update progress - else - managedFileUpload.progress = offset; + // Begin upload when stream is acknowledged, notify of any errors + writer.onack = function ackReceived(status) { + // Notify of any errors from the Guacamole server + if (status.isError()) { + $rootScope.$apply(function uploadStreamError() { + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.ERROR, + status.code); }); + } - }; // end ack handler + // Begin sending the requested file once stream is acknowledged + else if (!streamAcknowledged) { + writer.sendFile(file); + streamAcknowledged = true; + } + + }; + + // Abort and notify if the file cannot be read + writer.onerror = function fileReadError(file, offset, error) { + + // Abort transfer + writer.sendEnd(); + + // Upload failed + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.ERROR); + + }; + + // Notify of upload progress + writer.onprogress = function uploadProgressing(file, length) { + + $rootScope.$apply(function uploadStreamProgress() { + managedFileUpload.progress = length; + }); + + }; + + // Clean up and notify when upload completes + writer.oncomplete = function uploadComplete(file) { + + // If at end, stop upload + writer.sendEnd(); + managedFileUpload.progress = file.size; + + // Upload complete + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.CLOSED); + + // Notify of upload completion + $rootScope.$broadcast('guacUploadComplete', file.name); }; - reader.readAsArrayBuffer(file); return managedFileUpload; From 4d501e78d03d9d8a123da10ef6f84c139971bc5b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 2 Jun 2016 16:14:16 -0700 Subject: [PATCH 4/4] GUACAMOLE-44: Use more-readable JSDoc3 syntax for links. --- .../src/main/webapp/modules/ArrayBufferWriter.js | 2 +- .../src/main/webapp/modules/FileWriter.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js b/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js index 2fc3e938b..3b0f366c2 100644 --- a/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js +++ b/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js @@ -64,7 +64,7 @@ Guacamole.ArrayBufferWriter = function(stream) { /** * The maximum length of any blob sent by this Guacamole.ArrayBufferWriter, * in bytes. Data sent via - * {@link Guacamole.ArrayBufferWriter#sendData|sendData()} which exceeds + * [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds * this length will be split into multiple blobs. As the Guacamole protocol * limits the maximum size of any instruction or instruction element to * 8192 bytes, and the contents of blobs will be base64-encoded, this value diff --git a/guacamole-common-js/src/main/webapp/modules/FileWriter.js b/guacamole-common-js/src/main/webapp/modules/FileWriter.js index 3d221ef4b..c48a8f602 100644 --- a/guacamole-common-js/src/main/webapp/modules/FileWriter.js +++ b/guacamole-common-js/src/main/webapp/modules/FileWriter.js @@ -112,7 +112,7 @@ Guacamole.FileWriter = function FileWriter(stream) { /** * Reads the next chunk of the file provided to - * {@link Guacamole.FileWriter#sendFile|sendFile()}. The chunk itself + * [sendFile()]{@link Guacamole.FileWriter#sendFile}. The chunk itself * is read asynchronously, and will not be available until * reader.onload fires. * @@ -203,7 +203,7 @@ Guacamole.FileWriter = function FileWriter(stream) { /** * Fired when an error occurs reading a file passed to - * {@link Guacamole.FileWriter#sendFile|sendFile()}. The file transfer for + * [sendFile()]{@link Guacamole.FileWriter#sendFile}. The file transfer for * the given file will cease, but the stream will remain open. * * @event @@ -220,7 +220,7 @@ Guacamole.FileWriter = function FileWriter(stream) { /** * Fired for each successfully-read chunk of file data as a file is being - * sent via {@link Guacamole.FileWriter#sendFile|sendFile()}. + * sent via [sendFile()]{@link Guacamole.FileWriter#sendFile}. * * @event * @param {File} file @@ -233,7 +233,8 @@ Guacamole.FileWriter = function FileWriter(stream) { /** * Fired when a file passed to - * {@link Guacamole.FileWriter#sendFile|sendFile()} has finished being sent. + * [sendFile()]{@link Guacamole.FileWriter#sendFile} has finished being + * sent. * * @event * @param {File} file