mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUAC-928 Restore file upload functionality, improve error appearance, and add translated units and file upload sizes.
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}]);
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -41,3 +41,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#notificationArea .notification.error .text {
|
||||
white-space: normal;
|
||||
text-overflow: clip;
|
||||
text-align: left;
|
||||
}
|
||||
|
@@ -58,10 +58,6 @@
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.status-middle .notification.error {
|
||||
background: #FDD;
|
||||
}
|
||||
|
||||
/* Fade entire status area in/out based on shown status */
|
||||
|
||||
.status-outer {
|
||||
|
@@ -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.
|
||||
|
@@ -27,6 +27,10 @@
|
||||
color: black;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: #FDD;
|
||||
}
|
||||
|
||||
.notification .body {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user