GUAC-928 Restore file upload functionality, improve error appearance, and add translated units and file upload sizes.

This commit is contained in:
James Muehlner
2014-12-04 23:52:02 -08:00
parent 466aa8ba2d
commit 8ddf6e99b7
10 changed files with 344 additions and 23 deletions

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