// Angular binding doesn't work its usual magic for file
// inputs, so we need to $scope.$apply() this update.
$scope.$apply(function(){
- var i;
- var insertAt;
- for (insertAt=0; (insertAt<$scope.uploadQueue.length &&
- $scope.uploadQueue[insertAt].state != 'Done');
- insertAt++);
+ var i, nItemsTodo;
+ // Add these new files after the items already waiting
+ // in the queue -- but before the items that are
+ // 'Done' and have therefore been pushed to the
+ // bottom.
+ for (nItemsTodo = 0;
+ (nItemsTodo < $scope.uploadQueue.length &&
+ $scope.uploadQueue[nItemsTodo].state !== 'Done'); ) {
+ nItemsTodo++;
+ }
for (i=0; i<files.length; i++) {
- $scope.uploadQueue.splice(insertAt+i, 0,
+ $scope.uploadQueue.splice(nItemsTodo+i, 0,
new FileUploader(files[i]));
}
});
////////////////////////////////
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;
return _deferred.promise();
}
function resolve() {
- if (that._reader.result.length != that._slice.size) {
+ if (that._reader.result.length !== that._slice.size) {
// Sometimes we get an onload event even if the read
// did not return the desired number of bytes. We
// treat that as a fail.
// 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() {
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.
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) {
// 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]);
onUploaderProgress);
}
function onUploaderResolve(locator, dataSize) {
- if (!locator || _currentSlice.size != dataSize) {
- console.log("onUploaderResolve but locator=" + locator +
- " and " + _currentSlice.size + " != " + dataSize);
+ var sizeHint = (''+locator).split('+')[1];
+ if (!locator || parseInt(sizeHint) !== dataSize) {
+ console.log("onUploaderResolve, but locator '" + locator +
+ "' with size hint '" + sizeHint +
+ "' does not look right for dataSize=" + dataSize);
return onUploaderReject({
textStatus: "error",
err: "Bad response from slice upload"
that.state = 'Paused';
setProgress(_readPos);
_currentUploader = null;
- _deferred.reject(reason);
+ if (_deferred)
+ _deferred.reject(reason);
}
function onUploaderProgress(sliceDone, sliceSize) {
setProgress(_readPos + sliceDone);
_maxBlobSize,
that.file.size - _readPos);
setProgress(_readPos);
- if (size == 0) {
+ if (size === 0) {
return false;
}
var blob = that.file.slice(
}
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);
that.statistics = (
'' + $filter('number')(bytesDone/1024, '0') + ' KiB ' +
'at ~' + $filter('number')(kBps, '0') + ' KiB/s')
- if (that.state == 'Paused') {
+ if (that.state === 'Paused') {
that.statistics += ', paused';
- } else if (that.state == 'Uploading') {
+ } else if (that.state === 'Uploading') {
that.statistics += ', ETA ' +
$filter('date')(
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,
});
////////////////////////////////
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(
{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];
return doQueueWork();
}
function doQueueWork() {
- var i;
- that.state = 'Running';
- that.stateReason = null;
- // Push the done things to the bottom of the queue.
- for (i=0; (i<$scope.uploadQueue.length &&
- $scope.uploadQueue[i].state == 'Done'); i++);
- if (i>0)
- $scope.uploadQueue.push.apply($scope.uploadQueue, $scope.uploadQueue.splice(0, i));
- // 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);
+ $scope.uploadQueue[0].state !== 'Done') {
+ 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') +
? (' (from ' + reason.xhr.options.url + ')')
: '') +
': ' +
- (reason.err || ''));
+ (reason.err || defaultErrorMessage));
if (reason.xhr && reason.xhr.responseText)
that.stateReason += ' -- ' + reason.xhr.responseText;
_deferred.reject(reason);
function onQueueResolve() {
that.state = 'Idle';
that.stateReason = 'Done!';
- _deferred.resolve();
+ if (_deferred)
+ _deferred.resolve();
onQueueProgress();
}
function onQueueProgress() {
$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<uploads.length; i++) {
- upload = uploads[i];
+ $.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';
- }
+ });
return ArvadosClient.apiPromise(
'collections', 'update',
{ uuid: $scope.uuid,
collection:
{ manifest_text:
collection.manifest_text }
- }).
- then(deferred.resolve);
- }, onQueueReject).then(function() {
- var i;
- for(i=0; i<uploads.length; i++) {
- uploads[i].committed = true;
+ });
+ }).
+ 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<qLen; i++) {
+ if (uploads.indexOf($scope.uploadQueue[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);
}
}
}