diff --git a/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js b/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js index 8f33dd2be..3b0f366c2 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 + * [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 + * 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; offsetstart 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 + * [sendFile()]{@link Guacamole.FileWriter#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 + * [sendFile()]{@link Guacamole.FileWriter#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 [sendFile()]{@link Guacamole.FileWriter#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 + * [sendFile()]{@link Guacamole.FileWriter#sendFile} has finished being + * sent. + * + * @event + * @param {File} file + * The file that was sent. + */ + this.oncomplete = null; + +}; 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;