From 8ddf6e99b7973a09be52e18b8c1a994805b76ecf Mon Sep 17 00:00:00 2001
From: James Muehlner
Date: Thu, 4 Dec 2014 23:52:02 -0800
Subject: [PATCH] GUAC-928 Restore file upload functionality, improve error
appearance, and add translated units and file upload sizes.
---
.../client/controllers/clientController.js | 179 +++++++++++++++++-
.../app/client/directives/guacClient.js | 114 ++++++++++-
.../app/client/services/guacClientFactory.js | 6 +-
.../app/client/styles/notification-area.css | 6 +
.../main/webapp/app/index/styles/status.css | 4 -
.../directives/guacNotification.js | 22 +++
.../app/notification/styles/notification.css | 4 +
.../templates/guacNotification.html | 4 +-
guacamole/src/main/webapp/index.html | 8 +-
.../src/main/webapp/translations/en_US.json | 20 +-
10 files changed, 344 insertions(+), 23 deletions(-)
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index 2ce19d486..3b2dedba2 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -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";
+ }
+
+ });
+ });
+
}]);
diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js
index 34f79e42f..3a51729c9 100644
--- a/guacamole/src/main/webapp/app/client/directives/guacClient.js
+++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js
@@ -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.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{{text | translate}}
- {{progress}}
+ {{progressText | translate:"{ PROGRESS: progress, UNIT: progressUnit }"}}
- {{countdownText | translate:"{ REMAINING: timeRemaining}"}}
+ {{countdownText | translate:"{ REMAINING: timeRemaining }"}}
diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html
index 5f3ac9404..3dbeeecb4 100644
--- a/guacamole/src/main/webapp/index.html
+++ b/guacamole/src/main/webapp/index.html
@@ -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"
diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json
index 679d7c5c0..5860af733 100644
--- a/guacamole/src/main/webapp/translations/en_US.json
+++ b/guacamole/src/main/webapp/translations/en_US.json
@@ -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"
}
}
}