Merge pull request #14 from glyptodon/GUAC-928

GUAC-928 File Transfer not working
This commit is contained in:
Mike Jumper
2014-12-05 02:12:05 -08:00
11 changed files with 346 additions and 25 deletions

View File

@@ -492,7 +492,7 @@ Guacamole.Client = function(tunnel) {
var stream_index = parseInt(parameters[0]); var stream_index = parseInt(parameters[0]);
var reason = parameters[1]; var reason = parameters[1];
var code = parameters[2]; var code = parseInt(parameters[2]);
// Get stream // Get stream
var stream = output_streams[stream_index]; var stream = output_streams[stream_index];
@@ -718,7 +718,7 @@ Guacamole.Client = function(tunnel) {
"error": function(parameters) { "error": function(parameters) {
var reason = parameters[0]; var reason = parameters[0];
var code = parameters[1]; var code = parseInt(parameters[1]);
// Call handler if defined // Call handler if defined
if (guac_client.onerror) if (guac_client.onerror)

View File

@@ -79,6 +79,24 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
0x0308: true, 0x0308: true,
0x031D: true 0x031D: true
}; };
/**
* All upload error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
var UPLOAD_ERRORS = {
0x0100: true,
0x0201: true,
0x0202: true,
0x0203: true,
0x0204: true,
0x0205: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/** /**
* All error codes for which automatic reconnection is appropriate when a * All error codes for which automatic reconnection is appropriate when a
@@ -113,7 +131,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
callback: RECONNECT_ACTION.callback, callback: RECONNECT_ACTION.callback,
remaining: 15 remaining: 15
}; };
// Get DAO for reading connections and groups // Get DAO for reading connections and groups
var connectionGroupDAO = $injector.get('connectionGroupDAO'); var connectionGroupDAO = $injector.get('connectionGroupDAO');
var connectionDAO = $injector.get('connectionDAO'); var connectionDAO = $injector.get('connectionDAO');
@@ -338,19 +356,68 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
$scope.autoFitDisabled = function() { $scope.autoFitDisabled = function() {
return $scope.clientProperties.minZoom >= 1; return $scope.clientProperties.minZoom >= 1;
}; };
// Mapping of stream index to notification object /**
* Returns a progress object, as required by $scope.addNotification(), which
* contains the given number of bytes as an appropriate combination of
* progress value and associated unit.
*
* @param {String} text
* The translation string to associate with the progress object
* returned.
*
* @param {Number} bytes The number of bytes.
*
* @returns {Object}
* A progress object, as required by $scope.addNotification().
*/
var getFileProgress = function getFileProgress(text, bytes) {
// Gigabytes
if (bytes > 1000000000)
return {
text : text,
value : (bytes / 1000000000).toFixed(1),
unit : "gb"
};
// Megabytes
if (bytes > 1000000)
return {
text : text,
value : (bytes / 1000000).toFixed(1),
unit : "mb"
};
// Kilobytes
if (bytes > 1000)
return {
text : text,
value : (bytes / 1000).toFixed(1),
unit : "kb"
};
// Bytes
return {
text : text,
value : bytes,
unit : "b"
};
};
// Mapping of download stream index to notification object
var downloadNotifications = {}; var downloadNotifications = {};
// Mapping of stream index to notification ID // Mapping of download stream index to notification ID
var downloadNotificationIDs = {}; var downloadNotificationIDs = {};
$scope.$on('guacClientFileStart', function handleClientFileStart(event, guacClient, streamIndex, mimetype, filename) { $scope.$on('guacClientFileDownloadStart', function handleClientFileDownloadStart(event, guacClient, streamIndex, mimetype, filename) {
$scope.safeApply(function() { $scope.safeApply(function() {
var notification = { var notification = {
className : 'download', className : 'download',
title : 'client.fileTransfer.title', title : 'client.fileTransfer.downloadTitle',
text : filename text : filename
}; };
@@ -360,17 +427,17 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
}); });
}); });
$scope.$on('guacClientFileProgress', function handleClientFileProgress(event, guacClient, streamIndex, mimetype, filename, length) { $scope.$on('guacClientFileDownloadProgress', function handleClientFileDownloadProgress(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() { $scope.safeApply(function() {
var notification = downloadNotifications[streamIndex]; var notification = downloadNotifications[streamIndex];
if (notification) if (notification)
notification.progress = length; notification.progress = getFileProgress('client.fileTransfer.progressText', length);
}); });
}); });
$scope.$on('guacClientFileEnd', function handleClientFileEnd(event, guacClient, streamIndex, mimetype, filename, blob) { $scope.$on('guacClientFileDownloadEnd', function handleClientFileDownloadEnd(event, guacClient, streamIndex, mimetype, filename, blob) {
$scope.safeApply(function() { $scope.safeApply(function() {
var notification = downloadNotifications[streamIndex]; var notification = downloadNotifications[streamIndex];
@@ -400,4 +467,98 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
}); });
}); });
// Mapping of upload stream index to notification object
var uploadNotifications = {};
// Mapping of upload stream index to notification ID
var uploadNotificationIDs = {};
$scope.$on('guacClientFileUploadStart', function handleClientFileUploadStart(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
var notification = {
className : 'upload',
title : 'client.fileTransfer.uploadTitle',
text : filename
};
uploadNotifications[streamIndex] = notification;
uploadNotificationIDs[streamIndex] = $scope.addNotification(notification);
});
});
$scope.$on('guacClientFileUploadProgress', function handleClientFileUploadProgress(event, guacClient, streamIndex, mimetype, filename, length, offset) {
$scope.safeApply(function() {
var notification = uploadNotifications[streamIndex];
if (notification)
notification.progress = getFileProgress('client.fileTransfer.progressText', offset);
});
});
$scope.$on('guacClientFileUploadEnd', function handleClientFileUploadEnd(event, guacClient, streamIndex, mimetype, filename, length) {
$scope.safeApply(function() {
var notification = uploadNotifications[streamIndex];
var notificationID = uploadNotificationIDs[streamIndex];
/**
* Close the notification.
*/
var closeNotification = function closeNotification() {
$scope.removeNotification(notificationID);
delete uploadNotifications[streamIndex];
delete uploadNotificationIDs[streamIndex];
};
// Show that the file has uploaded successfully
if (notificationID && notification) {
delete notification.progress;
notification.actions = [
{
name : 'client.fileTransfer.ok',
callback : closeNotification
}
];
}
});
});
$scope.$on('guacClientFileUploadError', function handleClientFileUploadError(event, guacClient, streamIndex, mimetype, fileName, length, status) {
$scope.safeApply(function() {
var notification = uploadNotifications[streamIndex];
var notificationID = uploadNotificationIDs[streamIndex];
// Determine translation name of error
var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
/**
* Close the notification.
*/
var closeNotification = function closeNotification() {
$scope.removeNotification(notificationID);
delete uploadNotifications[streamIndex];
delete uploadNotificationIDs[streamIndex];
};
// Show that the file upload has failed
if (notificationID && notification) {
delete notification.progress;
notification.actions = [
{
name : 'client.fileTransfer.ok',
callback : closeNotification
}
];
notification.text = "client.error.uploadErrors." + errorName;
notification.className = "upload error";
}
});
});
}]); }]);

View File

@@ -447,8 +447,120 @@ angular.module('client').directive('guacClient', [function guacClient() {
if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) { if ($scope.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(0, keysym); client.sendKeyEvent(0, keysym);
event.preventDefault(); event.preventDefault();
} }
}); });
/**
* Converts the given bytes to a base64-encoded string.
*
* @param {Uint8Array} bytes A Uint8Array which contains the data to be
* encoded as base64.
* @return {String} The base64-encoded string.
*/
function getBase64(bytes) {
var data = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
data += String.fromCharCode(bytes[i]);
// Convert to base64
return $window.btoa(data);
}
/**
* Ignores the given event.
*
* @param {Event} e The event to ignore.
*/
function ignoreEvent(e) {
e.preventDefault();
e.stopPropagation();
}
/**
* Uploads the given file to the server.
*
* @param {File} file The file to upload.
*/
function uploadFile(file) {
// Construct reader for file
var reader = new FileReader();
reader.onloadend = function() {
// Open file for writing
var stream = client.createFileStream(file.type, file.name);
var valid = true;
var bytes = new Uint8Array(reader.result);
var offset = 0;
// Add upload notification
$scope.$emit('guacClientFileUploadStart', client, stream.index, file.type, file.name, bytes.length);
// Invalidate stream on all errors
// Continue upload when acknowledged
stream.onack = function(status) {
// Handle errors
if (status.isError()) {
valid = false;
$scope.$emit('guacClientFileUploadError', client, stream.index, file.type, file.name, bytes.length, status.code);
}
// Abort upload if stream is invalid
if (!valid) return false;
// Encode packet as base64
var slice = bytes.subarray(offset, offset+4096);
var base64 = getBase64(slice);
// Write packet
stream.sendBlob(base64);
// Advance to next packet
offset += 4096;
// If at end, stop upload
if (offset >= bytes.length) {
stream.sendEnd();
$scope.$emit('guacClientFileUploadProgress', client, stream.index, file.type, file.name, bytes.length, bytes.length);
$scope.$emit('guacClientFileUploadEnd', client, stream.index, file.type, file.name, bytes.length);
}
// Otherwise, update progress
else
$scope.$emit('guacClientFileUploadProgress', client, stream.index, file.type, file.name, bytes.length, offset);
};
};
reader.readAsArrayBuffer(file);
}
// Handle and ignore dragenter/dragover
displayContainer.addEventListener("dragenter", ignoreEvent, false);
displayContainer.addEventListener("dragover", ignoreEvent, false);
// File drop event handler
displayContainer.addEventListener("drop", function(e) {
e.preventDefault();
e.stopPropagation();
// Ignore file drops if no attached client
if (!client) return;
// Upload each file
var files = e.dataTransfer.files;
for (var i=0; i<files.length; i++)
uploadFile(files[i]);
}, false);
/* /*
* END CLIENT DIRECTIVE * END CLIENT DIRECTIVE

View File

@@ -140,20 +140,20 @@ angular.module('client').factory('guacClientFactory', ['$rootScope',
$scope.safeApply(function() { $scope.safeApply(function() {
// Begin file download // Begin file download
var guacFileStartEvent = $scope.$emit('guacClientFileStart', guacClient, stream.index, mimetype, filename); var guacFileStartEvent = $scope.$emit('guacClientFileDownloadStart', guacClient, stream.index, mimetype, filename);
if (!guacFileStartEvent.defaultPrevented) { if (!guacFileStartEvent.defaultPrevented) {
var blob_reader = new Guacamole.BlobReader(stream, mimetype); var blob_reader = new Guacamole.BlobReader(stream, mimetype);
// Update progress as data is received // Update progress as data is received
blob_reader.onprogress = function onprogress() { blob_reader.onprogress = function onprogress() {
$scope.$emit('guacClientFileProgress', guacClient, stream.index, mimetype, filename, blob_reader.getLength()); $scope.$emit('guacClientFileDownloadProgress', guacClient, stream.index, mimetype, filename, blob_reader.getLength());
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
}; };
// When complete, prompt for download // When complete, prompt for download
blob_reader.onend = function onend() { blob_reader.onend = function onend() {
$scope.$emit('guacClientFileEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob()); $scope.$emit('guacClientFileDownloadEnd', guacClient, stream.index, mimetype, filename, blob_reader.getBlob());
}; };
stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS);

View File

@@ -41,3 +41,9 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#notificationArea .notification.error .text {
white-space: normal;
text-overflow: clip;
text-align: left;
}

View File

@@ -58,10 +58,6 @@
margin: 1em; margin: 1em;
} }
.status-middle .notification.error {
background: #FDD;
}
/* Fade entire status area in/out based on shown status */ /* Fade entire status area in/out based on shown status */
.status-outer { .status-outer {

View File

@@ -78,6 +78,28 @@ angular.module('notification').directive('guacNotification', [function guacNotif
*/ */
defaultCallback : '=', defaultCallback : '=',
/**
* The text to use for displaying the progress. For the sake of
* i18n, the variable PROGRESS will be applied within the
* translation string for formatting plurals, etc., while the
* variable UNIT will be applied, if needed, for whatever units
* are applicable to the progress display.
*
* @type String
* @example
* "{PROGRESS} {UNIT, select, b{B} kb{KB}} uploaded."
*/
progressText : '=',
/**
* The unit which applies to the progress indicator, if any. This
* will be substituted in the progressText string with the UNIT
* variable.
*
* @type String
*/
progressUnit : '=',
/** /**
* Arbitrary value denoting how much progress has been made * Arbitrary value denoting how much progress has been made
* in some ongoing task that this notification represents. * in some ongoing task that this notification represents.

View File

@@ -27,6 +27,10 @@
color: black; color: black;
} }
.notification.error {
background: #FDD;
}
.notification .body { .notification .body {
margin: 0.5em; margin: 0.5em;
} }

View File

@@ -32,10 +32,10 @@
<p ng-show="text" class="text">{{text | translate}}</p> <p ng-show="text" class="text">{{text | translate}}</p>
<!-- Current progress --> <!-- Current progress -->
<div ng-show="progress" class="progress">{{progress}}</div> <div ng-show="progressText" class="progress">{{progressText | translate:"{ PROGRESS: progress, UNIT: progressUnit }"}}</div>
<!-- Default action countdown text --> <!-- Default action countdown text -->
<p ng-show="countdownText" class="countdown-text">{{countdownText | translate:"{ REMAINING: timeRemaining}"}}</p> <p ng-show="countdownText" class="countdown-text">{{countdownText | translate:"{ REMAINING: timeRemaining }"}}</p>
</div> </div>

View File

@@ -42,7 +42,9 @@ THE SOFTWARE.
class-name="status.className" class-name="status.className"
title="status.title" title="status.title"
text="status.text" text="status.text"
progress="status.progress" progress-text="status.progress.text"
progress-unit="status.progress.unit"
progress="status.progress.value"
actions="status.actions" actions="status.actions"
countdown-text="status.countdown.text" countdown-text="status.countdown.text"
countdown="status.countdown.remaining" countdown="status.countdown.remaining"
@@ -62,7 +64,9 @@ THE SOFTWARE.
class-name="wrapper.notification.className" class-name="wrapper.notification.className"
title="wrapper.notification.title" title="wrapper.notification.title"
text="wrapper.notification.text" text="wrapper.notification.text"
progress="wrapper.notification.progress" progress-text="wrapper.notification.progress.text"
progress-unit="wrapper.notification.progress.unit"
progress="wrapper.notification.progress.value"
actions="wrapper.notification.actions" actions="wrapper.notification.actions"
countdown-text="wrapper.notification.countdown.text" countdown-text="wrapper.notification.countdown.text"
countdown="wrapper.notification.countdown.remaining" countdown="wrapper.notification.countdown.remaining"

View File

@@ -252,6 +252,19 @@
"308" : "The Guacamole server has closed the connection because there has been no response from your browser for long enough that it appeared to be disconnected. This is commonly caused by network problems, such as spotty wireless signal, or simply very slow network speeds. Please check your network and try again.", "308" : "The Guacamole server has closed the connection because there has been no response from your browser for long enough that it appeared to be disconnected. This is commonly caused by network problems, such as spotty wireless signal, or simply very slow network speeds. Please check your network and try again.",
"31D" : "The Guacamole server is denying access to this connection because you have exhausted the limit for simultaneous connection use by an individual user. Please close one or more connections and try again.", "31D" : "The Guacamole server is denying access to this connection because you have exhausted the limit for simultaneous connection use by an individual user. Please close one or more connections and try again.",
"DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs." "DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs."
},
"uploadErrors": {
"100": "File transfer is either not supported or not enabled. Please contact your system administrator, or check your system logs.",
"201": "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.",
"202": "The file cannot be transferred because the remote desktop server is taking too long to respond. Please try again or or contact your system administrator.",
"203": "The remote desktop server encountered an error during transfer. Please try again or contact your system administrator.",
"204": "The destination for the file transfer does not exist. Please check that the destionation exists and try again.",
"205": "The destination for the file transfer is currently locked. Please wait for any in-progress tasks to complete and try again.",
"301": "You do not have permission to upload this file because you are not logged in. Please log in and try again.",
"303": "You do not have permission to upload this file. If you require access, please check your system settings, or check with your system administrator.",
"308": "The file transfer has stalled. This is commonly caused by network problems, such as spotty wireless signal, or simply very slow network speeds. Please check your network and try again.",
"31D": "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.",
"DEFAULT": "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs."
} }
}, },
"status" : { "status" : {
@@ -271,8 +284,11 @@
"reconnectCountdown" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}..." "reconnectCountdown" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}..."
}, },
"fileTransfer" : { "fileTransfer" : {
"title" : "File Transfer", "downloadTitle" : "File Transfer",
"save" : "Save" "uploadTitle" : "File Transfer",
"progressText" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
"ok" : "OK",
"save" : "Save"
} }
} }
} }