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