GUACAMOLE-1320: Merge support for chunked file uploads.

This commit is contained in:
Mike Jumper
2022-07-29 22:12:29 -07:00
committed by GitHub
3 changed files with 107 additions and 42 deletions

View File

@@ -163,12 +163,14 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector'
// Upload complete // Upload complete
managedFileUpload.progress = file.size; managedFileUpload.progress = file.size;
// Close the stream
stream.sendEnd();
ManagedFileTransferState.setStreamState(managedFileUpload.transferState, ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.CLOSED); ManagedFileTransferState.StreamState.CLOSED);
// Notify of upload completion // Notify of upload completion
$rootScope.$broadcast('guacUploadComplete', file.name); $rootScope.$broadcast('guacUploadComplete', file.name);
}, },
// Notify if upload fails // Notify if upload fails
@@ -186,11 +188,15 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector'
ManagedFileTransferState.StreamState.ERROR, ManagedFileTransferState.StreamState.ERROR,
Guacamole.Status.Code.INTERNAL_ERROR); Guacamole.Status.Code.INTERNAL_ERROR);
// Close the stream
stream.sendEnd();
})); }));
// Ignore all further acks // Ignore all further acks
stream.onack = null; stream.onack = null;
}; };
return managedFileUpload; return managedFileUpload;

View File

@@ -54,6 +54,15 @@ angular.module('rest').factory('tunnelService', ['$injector',
*/ */
var DOWNLOAD_CLEANUP_WAIT = 5000; var DOWNLOAD_CLEANUP_WAIT = 5000;
/**
* The maximum size a chunk may be during uploadToStream() in bytes.
*
* @private
* @constant
* @type Number
*/
const CHUNK_SIZE = 1024 * 1024 * 4;
/** /**
* Makes a request to the REST API to get the list of all tunnels * Makes a request to the REST API to get the list of all tunnels
* associated with in-progress connections, returning a promise that * associated with in-progress connections, returning a promise that
@@ -301,51 +310,103 @@ angular.module('rest').factory('tunnelService', ['$injector',
+ '/' + encodeURIComponent(sanitizeFilename(file.name)) + '/' + encodeURIComponent(sanitizeFilename(file.name))
+ '?token=' + encodeURIComponent(authenticationService.getCurrentToken()); + '?token=' + encodeURIComponent(authenticationService.getCurrentToken());
var xhr = new XMLHttpRequest(); /**
* Creates a chunk of the inputted file to be uploaded.
*
* @param {Number} offset
* The byte at which to begin the chunk.
*
* @return {File}
* The file chunk created by this function.
*/
const createChunk = (offset) => {
var chunkEnd = Math.min(offset + CHUNK_SIZE, file.size);
const chunk = file.slice(offset, chunkEnd);
return chunk;
};
// Invoke provided callback if upload tracking is supported /**
if (progressCallback && xhr.upload) { * POSTs the inputted chunks and recursively calls uploadHandler()
xhr.upload.addEventListener('progress', function updateProgress(e) { * until the upload is complete.
progressCallback(e.loaded); *
}); * @param {File} chunk
} * The chunk to be uploaded to the stream.
*
* @param {Number} offset
* The byte at which the inputted chunk begins.
*/
const uploadChunk = (chunk, offset) => {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
// Resolve/reject promise once upload has stopped // Invoke provided callback if upload tracking is supported.
xhr.onreadystatechange = function uploadStatusChanged() { if (progressCallback && xhr.upload) {
xhr.upload.addEventListener('progress', function updateProgress(e) {
progressCallback(e.loaded + offset);
});
};
// Ignore state changes prior to completion // Continue to next chunk, resolve, or reject promise as appropriate
if (xhr.readyState !== 4) // once upload has stopped
return; xhr.onreadystatechange = function uploadStatusChanged() {
// Resolve if HTTP status code indicates success // Ignore state changes prior to completion.
if (xhr.status >= 200 && xhr.status < 300) if (xhr.readyState !== 4)
deferred.resolve(); return;
// Parse and reject with resulting JSON error // Resolve if last chunk or begin next chunk if HTTP status
else if (xhr.getResponseHeader('Content-Type') === 'application/json') // code indicates success.
deferred.reject(new Error(angular.fromJson(xhr.responseText))); if (xhr.status >= 200 && xhr.status < 300) {
offset += CHUNK_SIZE;
// Warn of lack of permission of a proxy rejects the upload if (offset < file.size)
else if (xhr.status >= 400 && xhr.status < 500) uploadHandler(offset);
deferred.reject(new Error({ else
'type' : Error.Type.STREAM_ERROR, deferred.resolve();
'statusCode' : Guacamole.Status.Code.CLIENT_FORBIDDEN, }
'message' : 'HTTP ' + xhr.status
}));
// Assume internal error for all other cases // Parse and reject with resulting JSON error
else else if (xhr.getResponseHeader('Content-Type') === 'application/json')
deferred.reject(new Error({ deferred.reject(new Error(angular.fromJson(xhr.responseText)));
'type' : Error.Type.STREAM_ERROR,
'statusCode' : Guacamole.Status.Code.INTERNAL_ERROR, // Warn of lack of permission of a proxy rejects the upload
'message' : 'HTTP ' + xhr.status else if (xhr.status >= 400 && xhr.status < 500)
})); deferred.reject(new Error({
'type': Error.Type.STREAM_ERROR,
'statusCode': Guacamole.Status.Code.CLIENT_FORBIDDEN,
'message': 'HTTP ' + xhr.status
}));
// Assume internal error for all other cases
else
deferred.reject(new Error({
'type': Error.Type.STREAM_ERROR,
'statusCode': Guacamole.Status.Code.INTERNAL_ERROR,
'message': 'HTTP ' + xhr.status
}));
};
// Perform upload
xhr.send(chunk);
}; };
// Perform upload /**
xhr.open('POST', url, true); * Handles the recursive upload process. Each time it is called, a
xhr.send(file); * chunk is made with createChunk(), starting at the offset parameter.
* The chunk is then sent by uploadChunk(), which recursively calls
* this handler until the upload process is either completed and the
* promise is resolved, or fails and the promise is rejected.
*
* @param {Number} offset
* The byte at which to begin the chunk.
*/
const uploadHandler = (offset) => {
uploadChunk(createChunk(offset), offset);
};
uploadHandler(0);
return deferred.promise; return deferred.promise;

View File

@@ -94,8 +94,7 @@ public class InputStreamInterceptingFilter
/** /**
* Reads the next chunk of data from the InputStream associated with an * Reads the next chunk of data from the InputStream associated with an
* intercepted stream, sending that data as a "blob" instruction over the * intercepted stream, sending that data as a "blob" instruction over the
* GuacamoleTunnel associated with this filter. If the end of the * GuacamoleTunnel associated with this filter.
* InputStream is reached, an "end" instruction will automatically be sent.
* *
* @param stream * @param stream
* The stream from which the next chunk of data should be read. * The stream from which the next chunk of data should be read.
@@ -112,9 +111,8 @@ public class InputStreamInterceptingFilter
// End stream if no more data // End stream if no more data
if (length == -1) { if (length == -1) {
// Close stream, send end if the stream is still valid // Close stream
if (closeInterceptedStream(stream)) closeInterceptedStream(stream);
sendEnd(stream.getIndex());
return; return;