X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/7bc525d5a14a19c4dbbe89e71516372a5190c4b3..74fec3cd8284eae4829dad2c287588d52c621c4b:/apps/workbench/app/assets/javascripts/upload_to_collection.js diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js index 89c6c3dc16..d66be63853 100644 --- a/apps/workbench/app/assets/javascripts/upload_to_collection.js +++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js @@ -1,3 +1,7 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + var app = angular.module('Workbench', ['Arvados']); app.controller('UploadToCollection', UploadToCollection); app.directive('arvUuid', arvUuid); @@ -65,6 +69,7 @@ function UploadToCollection($scope, $filter, $q, $timeout, //////////////////////////////// var keepProxy; + var defaultErrorMessage = 'A network error occurred: either the server was unreachable, or there is a server configuration problem. Please check your browser debug console for a more specific error message (browser security features prevent us from showing the details here).'; function SliceReader(_slice) { var that = this; @@ -117,7 +122,20 @@ function UploadToCollection($scope, $filter, $q, $timeout, // resolve(locator) when the block is accepted by the // proxy. _deferred = $.Deferred(); - goSend(); + if (proxyUriBase().match(/^http:/) && + window.location.origin.match(/^https:/)) { + // In this case, requests will fail, and no ajax + // success/fail handlers will be called (!), which + // will leave our status saying "uploading" and the + // user waiting for something to happen. Better to + // give up now. + _deferred.reject({ + textStatus: 'error', + err: 'There is a server configuration problem. Proxy ' + proxyUriBase() + ' cannot be used from origin ' + window.location.origin + ' due to the browser\'s mixed-content (https/http) policy.' + }); + } else { + goSend(); + } return _deferred.promise(); } function stop() { @@ -177,11 +195,10 @@ function UploadToCollection($scope, $filter, $q, $timeout, function FileUploader(file) { $.extend(this, { - committed: false, file: file, locators: [], progress: 0.0, - state: 'Queued', // Queued, Uploading, Paused, Done + state: 'Queued', // Queued, Uploading, Paused, Uploaded, Done statistics: null, go: go, stop: stop // User wants to stop. @@ -201,13 +218,13 @@ function UploadToCollection($scope, $filter, $q, $timeout, function go() { if (_deferred) _deferred.reject({textStatus: 'restarted'}); - _deferred = $q.defer(); + _deferred = $.Deferred(); that.state = 'Uploading'; _startTime = Date.now(); _startByte = _readPos; setProgress(); goSlice(); - return _deferred.promise; + return _deferred.promise().always(function() { _deferred = null; }); } function stop() { if (_deferred) { @@ -225,7 +242,10 @@ function UploadToCollection($scope, $filter, $q, $timeout, // here is fulfilled. _currentSlice = nextSlice(); if (!_currentSlice) { - that.state = 'Done'; + // All slices have been uploaded, but the work won't + // be truly Done until the target collection has been + // updated by the QueueUploader. This state is called: + that.state = 'Uploaded'; setProgress(_readPos); _currentUploader = null; _deferred.resolve([that]); @@ -260,7 +280,8 @@ function UploadToCollection($scope, $filter, $q, $timeout, that.state = 'Paused'; setProgress(_readPos); _currentUploader = null; - _deferred.reject(reason); + if (_deferred) + _deferred.reject(reason); } function onUploaderProgress(sliceDone, sliceSize) { setProgress(_readPos + sliceDone); @@ -280,7 +301,10 @@ function UploadToCollection($scope, $filter, $q, $timeout, } function setProgress(bytesDone) { var kBps; - that.progress = Math.min(100, 100 * bytesDone / that.file.size) + if (that.file.size == 0) + that.progress = 100; + else + that.progress = Math.min(100, 100 * bytesDone / that.file.size); if (bytesDone > _startByte) { kBps = (bytesDone - _startByte) / (Date.now() - _startTime); @@ -295,21 +319,28 @@ function UploadToCollection($scope, $filter, $q, $timeout, new Date( Date.now() + (that.file.size - bytesDone) / kBps), 'shortTime') - } else { - that.statistics += ', finished ' + - $filter('date')(Date.now(), 'shortTime'); - _finishTime = Date.now(); } } else { that.statistics = that.state; } - _deferred.notify(); + if (that.state === 'Uploaded') { + // 'Uploaded' gets reported as 'finished', which is a + // little misleading because the collection hasn't + // been updated yet. But FileUploader's portion of the + // work (and the time when it makes sense to show + // speed and ETA) is finished. + that.statistics += ', finished ' + + $filter('date')(Date.now(), 'shortTime'); + _finishTime = Date.now(); + } + if (_deferred) + _deferred.notify(); } } function QueueUploader() { $.extend(this, { - state: 'Idle', + state: 'Idle', // Idle, Running, Stopped, Failed stateReason: null, statusSuccess: null, go: go, @@ -317,9 +348,11 @@ function UploadToCollection($scope, $filter, $q, $timeout, }); //////////////////////////////// var that = this; - var _deferred; + var _deferred; // the one we promise to go()'s caller + var _deferredAppend; // tracks current appendToCollection function go() { - if (that.state === 'Running') return _deferred.promise; + if (_deferred) return _deferred.promise(); + if (_deferredAppend) return _deferredAppend.promise(); _deferred = $.Deferred(); that.state = 'Running'; ArvadosClient.apiPromise( @@ -327,11 +360,16 @@ function UploadToCollection($scope, $filter, $q, $timeout, {filters: [['service_type','=','proxy']]}). then(doQueueWithProxy); onQueueProgress(); - return _deferred.promise(); + return _deferred.promise().always(function() { _deferred = null; }); } function stop() { + that.state = 'Stopped'; + if (_deferred) { + _deferred.reject({}); + } for (var i=0; i<$scope.uploadQueue.length; i++) $scope.uploadQueue[i].stop(); + onQueueProgress(); } function doQueueWithProxy(data) { keepProxy = data.items[0]; @@ -345,32 +383,35 @@ function UploadToCollection($scope, $filter, $q, $timeout, return doQueueWork(); } function doQueueWork() { - var nItemsDone; - that.state = 'Running'; - that.stateReason = null; - // Are there any Done things at the top of the queue? - for (nItemsDone = 0; - (nItemsDone < $scope.uploadQueue.length && - $scope.uploadQueue[nItemsDone].state === 'Done'); ) { - nItemsDone++; - } - // If so, push them down to the bottom of the queue. - if (nItemsDone > 0) { - $scope.uploadQueue.push.apply( - $scope.uploadQueue, - $scope.uploadQueue.splice(0, nItemsDone)); - } - // If anything is not-done, do it. + // If anything is not Done, do it. if ($scope.uploadQueue.length > 0 && $scope.uploadQueue[0].state !== 'Done') { - return $scope.uploadQueue[0].go(). - then(appendToCollection, null, onQueueProgress). - then(doQueueWork, onQueueReject); + if (_deferred) { + that.stateReason = null; + return $scope.uploadQueue[0].go(). + then(appendToCollection, null, onQueueProgress). + then(doQueueWork, onQueueReject); + } else { + // Queue work has been stopped. Just update the + // view. + onQueueProgress(); + return; + } } - // If everything is done, resolve the promise and clean up. - return onQueueResolve(); + // If everything is Done, resolve the promise and clean + // up. Note this can happen even after the _deferred + // promise has been rejected: specifically, when stop() is + // called too late to prevent completion of the last + // upload. In that case we want to update state to "Idle", + // rather than leave it at "Stopped". + onQueueResolve(); } function onQueueReject(reason) { + if (!_deferred) { + // Outcome has already been decided (by stop()). + return; + } + that.state = 'Failed'; that.stateReason = ( (reason.textStatus || 'Error') + @@ -378,7 +419,7 @@ function UploadToCollection($scope, $filter, $q, $timeout, ? (' (from ' + reason.xhr.options.url + ')') : '') + ': ' + - (reason.err || '')); + (reason.err || defaultErrorMessage)); if (reason.xhr && reason.xhr.responseText) that.stateReason += ' -- ' + reason.xhr.responseText; _deferred.reject(reason); @@ -387,7 +428,8 @@ function UploadToCollection($scope, $filter, $q, $timeout, function onQueueResolve() { that.state = 'Idle'; that.stateReason = 'Done!'; - _deferred.resolve(); + if (_deferred) + _deferred.resolve(); onQueueProgress(); } function onQueueProgress() { @@ -395,39 +437,58 @@ function UploadToCollection($scope, $filter, $q, $timeout, $timeout(function(){$scope.$apply();}); } function appendToCollection(uploads) { - var deferred = $q.defer(); - return ArvadosClient.apiPromise( + _deferredAppend = $.Deferred(); + ArvadosClient.apiPromise( 'collections', 'get', { uuid: $scope.uuid }). then(function(collection) { var manifestText = ''; - var upload, i; - for (i=0; i= 0) { + $scope.uploadQueue[i].state = 'Done'; + $scope.uploadQueue.push.apply( + $scope.uploadQueue, + $scope.uploadQueue.splice(i, 1)); + --i; + --qLen; + } } + }). + then(_deferredAppend.resolve, + _deferredAppend.reject); + return _deferredAppend.promise(). + always(function() { + _deferredAppend = null; }); - return deferred.promise.then(doQueueWork); } } }