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

View File

@@ -79,6 +79,24 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
0x0308: 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
@@ -113,7 +131,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
callback: RECONNECT_ACTION.callback,
remaining: 15
};
// Get DAO for reading connections and groups
var connectionGroupDAO = $injector.get('connectionGroupDAO');
var connectionDAO = $injector.get('connectionDAO');
@@ -338,19 +356,68 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams',
$scope.autoFitDisabled = function() {
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 = {};
// Mapping of stream index to notification ID
// Mapping of download stream index to notification ID
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() {
var notification = {
className : 'download',
title : 'client.fileTransfer.title',
title : 'client.fileTransfer.downloadTitle',
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() {
var notification = downloadNotifications[streamIndex];
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() {
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) {
client.sendKeyEvent(0, keysym);
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

View File

@@ -140,20 +140,20 @@ angular.module('client').factory('guacClientFactory', ['$rootScope',
$scope.safeApply(function() {
// 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) {
var blob_reader = new Guacamole.BlobReader(stream, mimetype);
// Update progress as data is received
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);
};
// When complete, prompt for download
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);

View File

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

View File

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

View File

@@ -78,6 +78,28 @@ angular.module('notification').directive('guacNotification', [function guacNotif
*/
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
* in some ongoing task that this notification represents.

View File

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

View File

@@ -32,10 +32,10 @@
<p ng-show="text" class="text">{{text | translate}}</p>
<!-- 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 -->
<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>

View File

@@ -42,7 +42,9 @@ THE SOFTWARE.
class-name="status.className"
title="status.title"
text="status.text"
progress="status.progress"
progress-text="status.progress.text"
progress-unit="status.progress.unit"
progress="status.progress.value"
actions="status.actions"
countdown-text="status.countdown.text"
countdown="status.countdown.remaining"
@@ -62,7 +64,9 @@ THE SOFTWARE.
class-name="wrapper.notification.className"
title="wrapper.notification.title"
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"
countdown-text="wrapper.notification.countdown.text"
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.",
"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."
},
"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" : {
@@ -271,8 +284,11 @@
"reconnectCountdown" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}..."
},
"fileTransfer" : {
"title" : "File Transfer",
"save" : "Save"
"downloadTitle" : "File Transfer",
"uploadTitle" : "File Transfer",
"progressText" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
"ok" : "OK",
"save" : "Save"
}
}
}