X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/e320b9009a294a81a68e56e1998782c445d1affe..37860134053cda88c7ee3a3f4300e949cad016f5:/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 4ca5df68a1..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,19 +383,35 @@ function UploadToCollection($scope, $filter, $q, $timeout, return doQueueWork(); } function doQueueWork() { - that.state = 'Running'; - that.stateReason = null; // 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') + @@ -365,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); @@ -374,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() { @@ -382,18 +437,25 @@ 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 = ''; $.each(uploads, function(_, upload) { + var locators = upload.locators; + if (locators.length === 0) { + // Every stream must have at least one + // data locator, even if it is zero bytes + // long: + locators = ['d41d8cd98f00b204e9800998ecf8427e+0']; + } filename = ArvadosClient.uniqueNameForManifest( collection.manifest_text, '.', upload.file.name); collection.manifest_text += '. ' + - upload.locators.join(' ') + + locators.join(' ') + ' 0:' + upload.file.size.toString() + ':' + filename + '\n'; @@ -404,14 +466,15 @@ function UploadToCollection($scope, $filter, $q, $timeout, collection: { manifest_text: collection.manifest_text } - }). - then(deferred.resolve); - }, onQueueReject).then(function() { - // Push the completed upload(s) to the bottom of the queue. + }); + }). + then(function() { + // Mark the completed upload(s) as Done and push + // them to the bottom of the queue. var i, qLen = $scope.uploadQueue.length; for (i=0; i= 0) { - $scope.uploadQueue[i].committed = true; + $scope.uploadQueue[i].state = 'Done'; $scope.uploadQueue.push.apply( $scope.uploadQueue, $scope.uploadQueue.splice(i, 1)); @@ -419,8 +482,13 @@ function UploadToCollection($scope, $filter, $q, $timeout, --qLen; } } + }). + then(_deferredAppend.resolve, + _deferredAppend.reject); + return _deferredAppend.promise(). + always(function() { + _deferredAppend = null; }); - return deferred.promise.then(doQueueWork); } } }