[ARVADOS] created: d2e9fba1946ead6c059d3df3cf34139563f2db60
git at public.curoverse.com
git at public.curoverse.com
Sun Nov 23 18:35:17 EST 2014
at d2e9fba1946ead6c059d3df3cf34139563f2db60 (commit)
commit d2e9fba1946ead6c059d3df3cf34139563f2db60
Author: Tom Clegg <tom at curoverse.com>
Date: Sun Nov 23 18:35:15 2014 -0500
3781: Clean up pause/resume.
diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js
index 33d11f2..bbe76eb 100644
--- a/apps/workbench/app/assets/javascripts/upload_to_collection.js
+++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js
@@ -1,12 +1,12 @@
var app = angular.module('Workbench', []);
app.controller(
'UploadController',
- ['$scope', 'numberFilter', 'dateFilter', UploadController]);
+ ['$scope', '$q', '$timeout', 'numberFilter', 'dateFilter', UploadController]);
-function UploadController($scope, numberFilter, dateFilter) {
+function UploadController($scope, $q, $timeout, numberFilter, dateFilter) {
$.extend($scope, {
uploadQueue: [],
- uploader: new QueueUploader($scope),
+ uploader: new QueueUploader($scope, $q, $timeout),
numberFilter: numberFilter,
dateFilter: dateFilter,
addFilesToQueue: function(files) {
@@ -16,16 +16,22 @@ function UploadController($scope, numberFilter, dateFilter) {
var i;
for (i=0; i<files.length; i++) {
$scope.uploadQueue.push(
- new FileUploader($scope, files[i]));
+ new FileUploader($scope, $q, files[i]));
}
});
- $scope.uploader.go($scope).then(
- $scope.sync,
- $scope.sync,
- $scope.sync);
},
- sync: function() {
- $scope.$digest();
+ go: function() {
+ $scope.uploader.go();
+ },
+ stop: function() {
+ $scope.uploader.stop();
+ },
+ removeFileFromQueue: function(index) {
+ var wasRunning = $scope.uploader.running;
+ $scope.uploadQueue[index].stop();
+ $scope.uploadQueue.splice(index, 1);
+ if (wasRunning)
+ $scope.go();
}
});
// TODO: watch uploadQueue, abort uploads if entries disappear
@@ -34,12 +40,9 @@ function UploadController($scope, numberFilter, dateFilter) {
function SliceReader(uploader, slice) {
var that = this;
$.extend(this, {
- _deferred: null,
- _failCount: 0,
- _reader: null,
- _slice: slice,
- _uploader: uploader,
go: function() {
+ // Return a promise, which will be resolved with the
+ // requested slice data.
this._deferred = $.Deferred();
this._reader = new FileReader();
this._reader.onload = this.resolve;
@@ -48,6 +51,11 @@ function SliceReader(uploader, slice) {
this._reader.readAsArrayBuffer(this._slice.blob);
return this._deferred.promise();
},
+ ////////////////////////////////
+ _deferred: null,
+ _reader: null,
+ _slice: slice,
+ _uploader: uploader,
resolve: function() {
if (that._reader.result.length != that._slice.size) {
// Sometimes we get an onload event even if the read
@@ -63,21 +71,14 @@ function SliceReader(uploader, slice) {
});
}
-function SliceUploader(uploader) {
+function SliceUploader(uploader, label, data, dataSize) {
var that = this;
$.extend(this, {
- _data: null,
- _dataSize: null,
- _deferred: null,
- _jqxhr: null,
- _uploader: uploader,
- go: function(data, dataSize) {
+ go: function() {
// Send data to the Keep proxy. Retry a few times on
// fail. Return a promise that will get resolved with
// resolve(locator) when the block is accepted by the
// proxy.
- this._data = data;
- this._dataSize = dataSize;
this._jqxhr = $.ajax({
url: this.proxyUriBase(),
type: 'POST',
@@ -96,25 +97,37 @@ function SliceUploader(uploader) {
return xhr;
},
processData: false,
- data: data,
- context: this
+ data: that._data
});
- return (this._deferred = this._jqxhr.then(this.doneSendSlice));
+ return this._jqxhr.then(this.doneSendSlice, this.failSendSlice);
},
+ stop: function() {
+ this._failMax = 0;
+ this._jqxhr.abort();
+ },
+ _data: data,
+ _dataSize: dataSize,
+ _failCount: 0,
+ _failMax: 3,
+ _label: label,
+ _jqxhr: null,
+ _uploader: uploader,
progressSendSlice: function(x,y,z) {
console.log(['uploadProgress',x,y,z]);
- that._deferred.notify(50,100);
+ that._jqxhr.notify(50, that._dataSize);
},
doneSendSlice: function(data, textStatus, jqxhr) {
return $.Deferred().resolve(data, that._dataSize).promise();
},
failSendSlice: function(xhr, textStatus, err) {
- console.log([xhr, textStatus, err]);
- if (++that._failCount <= 3) {
- console.log("Error (" + textStatus + "), retrying slice at " +
- that.bytesDone);
- return that.go(that._data, that._dataSize);
+ if (++that._failCount <= that._failMax) {
+ // TODO: nice to tell the user that retry is happening.
+ console.log('slice ' + that._label + ': ' +
+ textStatus + ', retry ' + that._failCount);
+ return that.go();
}
+ // Can't propagate multiple arguments with "return a,b,c"
+ // so we do this indirectly by returning a new promise.
return $.Deferred().reject(xhr, textStatus, err).promise();
},
proxyUriBase: function() {
@@ -126,55 +139,77 @@ function SliceUploader(uploader) {
});
}
-function FileUploader($scope, file) {
+function FileUploader($scope, $q, file) {
var that = this;
$.extend(this, {
+ state: 'Queued', // Queued, Uploading, Paused, Done
+ progress: 0.0,
+ statistics: null,
+ file: file,
+ go: function() {
+ if (this._deferred)
+ this._deferred.reject(null, 'Restarted', null);
+ this._deferred = $q.defer();
+ this.state = 'Uploading';
+ this.startTime = Date.now();
+ this.startByte = this._readPos;
+ this.goSlice();
+ return this._deferred.promise;
+ },
+ stop: function() {
+ if (this._deferred) {
+ this.state = 'Paused';
+ this._deferred.reject(null, 'Interrupted', null);
+ }
+ if (this._currentUploader) {
+ this._currentUploader.stop();
+ this._currentUploader = null;
+ }
+ },
_currentSlice: null,
_deferred: null,
_locators: [],
$scope: $scope,
uploader: $scope.uploader,
- file: file,
maxBlobSize: Math.pow(2,26),
bytesDone: 0,
queueTime: Date.now(),
startTime: null,
startByte: null,
- _readPos: 0,
- done: false,
- state: 'Queued',
- progress: 0.0,
- statistics: null,
- go: function() {
- this.state = 'Uploading';
- this.startTime = Date.now();
- this.startByte = this._readPos;
- if (this._deferred)
- this._deferred.reject(null, "Cancelled", "Restarted");
- this._deferred = $.Deferred();
- this.goSlice();
- return this._deferred.promise();
- },
+ finishTime: null,
+ _readPos: 0, // number of bytes confirmed uploaded
goSlice: function() {
// Ensure this._deferred gets resolved or rejected --
// either right here, or when a new promise arranged right
// here is fulfilled.
this._currentSlice = this.nextSlice();
if (!this._currentSlice) {
+ this.state = 'Done';
+ this._currentUploader = null;
this._deferred.resolve('Done');
return;
}
- (new SliceUploader(this.uploader)).
- go(this._currentSlice.blob, this._currentSlice.size).
- then(this.onUploaderSuccess,
- this._deferred.reject,
- this.onUploaderProgress);
+ this._currentUploader = new SliceUploader(
+ this.uploader,
+ this._readPos.toString(),
+ this._currentSlice.blob,
+ this._currentSlice.size);
+ this._currentUploader.go().then(
+ this.onUploaderResolve,
+ this.onUploaderReject,
+ this.onUploaderProgress);
},
- onUploaderSuccess: function(locator, dataSize) {
+ onUploaderResolve: function(locator, dataSize) {
// TODO: check that._currentSlice.size == dataSize
that._locators.push(locator);
+ that._readPos += dataSize;
that.goSlice();
},
+ onUploaderReject: function(xhr, status, err) {
+ that.state = 'Paused';
+ that.setProgress(that._readPos);
+ that._deferred.reject(xhr, status, err);
+ },
onUploaderProgress: function(sliceDone, sliceSize) {
that.setProgress(that._readPos - sliceSize + sliceDone);
console.log("upload progress: " + that.progress);
@@ -185,36 +220,36 @@ function FileUploader($scope, file) {
this.file.size - this._readPos);
this.setProgress(this._readPos);
if (size == 0) {
- this.state = 'Done';
return false;
}
var blob = this.file.slice(
- this._readPos,
- this._readPos + size,
+ this._readPos, this._readPos+size,
'application/octet-stream; charset=x-user-defined');
- this._readPos += size;
return {blob: blob, size: size};
},
setProgress: function(bytesDone) {
var kBps;
this.progress = Math.min(100, 100 * bytesDone / this.file.size)
if (bytesDone <= this.startByte) {
- this.statistics = '';
+ this.statistics = 'Starting';
} else {
kBps = (bytesDone - this.startByte) /
(Date.now() - this.startTime);
this.statistics = (
- '' + this.$scope.numberFilter(bytesDone/1024, '0') + ' KB ' +
- 'at ~' + this.$scope.numberFilter(kBps, '0') + ' KB/s')
- if (bytesDone < this.file.size) {
- this.statistics += ' -- ETA ' +
+ '' + this.$scope.numberFilter(bytesDone/1024, '0') + 'K ' +
+ 'at ~' + this.$scope.numberFilter(kBps, '0') + 'K/s')
+ if (this.state == 'Paused') {
+ // That's all I have to say about that.
+ } else if (bytesDone < this.file.size) {
+ this.statistics += ', ETA ' +
this.$scope.dateFilter(
new Date(
Date.now() + (this.file.size - bytesDone) / kBps),
'shortTime')
} else {
- this.statistics += ' -- finished @ ' +
+ this.statistics += ', finished ' +
this.$scope.dateFilter(Date.now(), 'shortTime');
+ this.finishTime = Date.now();
}
}
console.log(this.progress);
@@ -223,61 +258,77 @@ function FileUploader($scope, file) {
});
}
-function QueueUploader($scope) {
+function QueueUploader($scope, $q, $timeout) {
var that = this;
$.extend(this, {
- _running: false,
- statusError: null,
+ state: 'Idle',
+ stateReason: null,
statusSuccess: null,
+ go: function() {
+ if (this.state == 'Running') return this._deferred.promise;
+ this._deferred = $.Deferred();
+ this.state = 'Running';
+ this.arvados.apiPromise(
+ 'keep_services', 'list',
+ {filters: [['service_type','=','proxy']]}).
+ then(this.doQueueWithProxy);
+ this.reportUploadProgress();
+ return this._deferred.promise();
+ },
+ stop: function() {
+ for (var i=0; i<$scope.uploadQueue.length; i++)
+ $scope.uploadQueue[i].stop();
+ },
+ ////////////////////////////////
arvados: new ArvadosClient(
$('meta[name=arvados-discovery-uri]').attr('content'),
$('meta[name=arvados-api-token]').attr('content')
),
- go: function($scope) {
- if (this._running) return $.when(true,true);
- this._running = true;
- return this.arvados.apiPromise(
- 'keep_services', 'list',
- {filters: [['service_type','=','proxy']]}).
- then(this.doQueueWithProxy).
- then(this.reportUploadSuccess, this.reportUploadError).
- always(function(){ that._running = false; });
- },
doQueueWithProxy: function(data) {
that.keepProxy = data.items[0];
+ if (!that.keepProxy) {
+ that.state = 'Failed';
+ that.stateReason =
+ 'There seems to be no Keep proxy service available.';
+ that._deferred.reject();
+ return;
+ }
return that.doQueueWork();
},
doQueueWork: function() {
- that.statusError = null;
- that.statusSuccess = null;
- if (!that.keepProxy) {
- return $.Deferred().
- reject(null, null, "Sorry, there seems to be no Keep proxy service available.").
- promise();
- }
+ that.state = 'Running';
+ that.stateReason = null;
for (var i=0; i<$scope.uploadQueue.length; i++) {
- if ($scope.uploadQueue[i].state == 'Queued') {
- return $scope.uploadQueue[i].
- go().
- then(
- that.doQueueWork,
- that.reportUploadError);
+ if ($scope.uploadQueue[i].state != 'Done') {
+ return $scope.uploadQueue[i].go().then(
+ that.doQueueWork,
+ that.reportUploadError,
+ that.reportUploadProgress);
}
}
- return $.Deferred().resolve("Done!").promise();
+ that.reportUploadSuccess("Done!");
},
reportUploadError: function(jqxhr, textStatus, err) {
- console.log(["error", jqxhr,textStatus,err]);
- that.statusError = (
+ that.state = 'Failed';
+ that.stateReason = (
(textStatus || 'Error') +
(jqxhr && jqxhr.options
? (' (from ' + jqxhr.options.url + ')')
: '') +
': ' +
(err || '(no further details available, sorry!)'));
+ that._deferred.reject();
+ that.reportUploadProgress();
},
reportUploadSuccess: function(message) {
- that.statusSuccess = message;
+ that.state = 'Idle';
+ that.stateReason = message;
+ that._deferred.resolve();
+ that.reportUploadProgress();
+ },
+ reportUploadProgress: function() {
+ // Ensure updates happen after FileUpload promise callbacks.
+ $timeout(function(){$scope.$apply();});
}
});
}
diff --git a/apps/workbench/app/views/collections/_show_files.html.erb b/apps/workbench/app/views/collections/_show_files.html.erb
index ea31c84..1b7ad96 100644
--- a/apps/workbench/app/views/collections/_show_files.html.erb
+++ b/apps/workbench/app/views/collections/_show_files.html.erb
@@ -68,24 +68,39 @@ function unselect_all_files() {
<div class="panel-body" ng-controller="UploadController">
<div class="panel panel-primary">
<div class="panel-body">
- <input type="file" multiple ng-model="incoming" onchange="angular.element(this).scope().addFilesToQueue(this.files); $(this).val('');" />
+ <div class="row">
+ <div class="col-sm-6">
+ <input type="file" multiple ng-model="incoming" onchange="angular.element(this).scope().addFilesToQueue(this.files); $(this).val('');" />
+ </div>
+ <div class="col-sm-6">
+ <div class="btn-group btn-group-sm pull-right" role="group">
+ <button type="button" class="btn btn-default" ng-click="stop()" ng-disabled="uploader.state != 'Running'"><i class="fa fa-fw fa-pause"></i> Pause</button>
+ <button type="button" class="btn btn-primary" ng-click="go()" ng-disabled="uploader.state == 'Running' || uploadQueue.length == 0"><i class="fa fa-fw fa-play"></i> Start</button>
+ </div>
+ </div>
+ </div>
</div>
</div>
<div ng-show="uploader.statusSuccess" class="alert alert-success"><i class="fa fa-flag-checkered"></i> {{uploader.statusSuccess}}</div>
<div ng-show="uploader.statusError" class="alert alert-danger"><i class="fa fa-warning"></i> {{uploader.statusError}}</div>
<div ng-repeat="upload in uploadQueue" class="row">
- <div class="col-sm-2">
- <div class="progress">
- <span class="progress-bar" style="width: {{upload.progress}}%"></span>
- </div>
+ <div class="col-sm-1">
+ <button class="btn btn-xs btn-default" ng-click="removeFileFromQueue($index)" title="cancel" ng-show="upload.state != 'Done'"><i class="fa fa-fw fa-trash-o"></i></button>
+ </div>
+ <div class="col-sm-4 nowrap" style="overflow-x:hidden;text-overflow:ellipsis">
+ <span title="{{upload.file.name}}">
+ {{upload.file.name}}
+ </span>
</div>
<div class="col-sm-1" style="text-align: right">
{{upload.file.size/1024 | number:0}}K
</div>
- <div class="col-sm-4">
- {{upload.file.name}}
+ <div class="col-sm-2">
+ <div class="progress">
+ <span class="progress-bar" style="width: {{upload.progress}}%"></span>
+ </div>
</div>
- <div class="col-sm-5" ng-class="{lighten: upload.state != 'Uploading'}">
+ <div class="col-sm-4" ng-class="{lighten: upload.state != 'Uploading'}">
{{upload.statistics}}
</div>
</div>
commit a75145983529310528a6cf0a54d78f85e9101eb6
Author: Tom Clegg <tom at curoverse.com>
Date: Sun Nov 23 05:07:15 2014 -0500
3781: Really work the angular queue.
diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js
index 1c4df0f..33d11f2 100644
--- a/apps/workbench/app/assets/javascripts/upload_to_collection.js
+++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js
@@ -1,30 +1,35 @@
var app = angular.module('Workbench', []);
-app.controller('UploadToCollection', ['$scope', function($scope) {
+app.controller(
+ 'UploadController',
+ ['$scope', 'numberFilter', 'dateFilter', UploadController]);
+
+function UploadController($scope, numberFilter, dateFilter) {
$.extend($scope, {
uploadQueue: [],
+ uploader: new QueueUploader($scope),
+ numberFilter: numberFilter,
+ dateFilter: dateFilter,
addFilesToQueue: function(files) {
// Angular binding doesn't work its usual magic for file
// inputs, so we need to $scope.$apply() this update.
$scope.$apply(function(){
var i;
for (i=0; i<files.length; i++) {
- $scope.uploadQueue.push({
- file: files[i]
- });
+ $scope.uploadQueue.push(
+ new FileUploader($scope, files[i]));
}
});
+ $scope.uploader.go($scope).then(
+ $scope.sync,
+ $scope.sync,
+ $scope.sync);
},
- statusSuccess: null,
- statusError: null
+ sync: function() {
+ $scope.$digest();
+ }
});
-}]);
-
-$(document).on('ready ajax:success', function() {
- $('.arv-upload-to-collection').
- not('.arv-upload-setup').
- addClass('.arv-upload-setup').
- each(function() { new QueueUploader($(this)) });
-});
+ // TODO: watch uploadQueue, abort uploads if entries disappear
+}
function SliceReader(uploader, slice) {
var that = this;
@@ -63,6 +68,7 @@ function SliceUploader(uploader) {
$.extend(this, {
_data: null,
_dataSize: null,
+ _deferred: null,
_jqxhr: null,
_uploader: uploader,
go: function(data, dataSize) {
@@ -93,10 +99,11 @@ function SliceUploader(uploader) {
data: data,
context: this
});
- return this._jqxhr.then(this.doneSendSlice);
+ return (this._deferred = this._jqxhr.then(this.doneSendSlice));
},
progressSendSlice: function(x,y,z) {
console.log(['uploadProgress',x,y,z]);
+ that._deferred.notify(50,100);
},
doneSendSlice: function(data, textStatus, jqxhr) {
return $.Deferred().resolve(data, that._dataSize).promise();
@@ -119,38 +126,66 @@ function SliceUploader(uploader) {
});
}
-function FileUploader(uploader, file) {
+function FileUploader($scope, file) {
var that = this;
$.extend(this, {
_currentSlice: null,
+ _deferred: null,
_locators: [],
- uploader: uploader,
+ $scope: $scope,
+ uploader: $scope.uploader,
file: file,
- maxBlobSize: 65536,
+ maxBlobSize: Math.pow(2,26),
bytesDone: 0,
queueTime: Date.now(),
startTime: null,
+ startByte: null,
_readPos: 0,
done: false,
+ state: 'Queued',
+ progress: 0.0,
+ statistics: null,
go: function() {
+ this.state = 'Uploading';
+ this.startTime = Date.now();
+ this.startByte = this._readPos;
+ if (this._deferred)
+ this._deferred.reject(null, "Cancelled", "Restarted");
+ this._deferred = $.Deferred();
+ this.goSlice();
+ return this._deferred.promise();
+ },
+ goSlice: function() {
+ // Ensure this._deferred gets resolved or rejected --
+ // either right here, or when a new promise arranged right
+ // here is fulfilled.
this._currentSlice = this.nextSlice();
if (!this._currentSlice) {
- return $.when(true, true);
+ this._deferred.resolve('Done');
+ return;
}
- return (new SliceUploader(this.uploader)).
+ (new SliceUploader(this.uploader)).
go(this._currentSlice.blob, this._currentSlice.size).
- then(this.onUploaderSuccess);
+ then(this.onUploaderSuccess,
+ this._deferred.reject,
+ this.onUploaderProgress);
},
onUploaderSuccess: function(locator, dataSize) {
+ // TODO: check that._currentSlice.size == dataSize
that._locators.push(locator);
- return that.go();
+ that.goSlice();
+ },
+ onUploaderProgress: function(sliceDone, sliceSize) {
+ that.setProgress(that._readPos - sliceSize + sliceDone);
+ console.log("upload progress: " + that.progress);
},
nextSlice: function() {
var size = Math.min(
this.maxBlobSize,
this.file.size - this._readPos);
+ this.setProgress(this._readPos);
if (size == 0) {
- this.done = true;
+ this.state = 'Done';
return false;
}
var blob = this.file.slice(
@@ -159,71 +194,90 @@ function FileUploader(uploader, file) {
'application/octet-stream; charset=x-user-defined');
this._readPos += size;
return {blob: blob, size: size};
+ },
+ setProgress: function(bytesDone) {
+ var kBps;
+ this.progress = Math.min(100, 100 * bytesDone / this.file.size)
+ if (bytesDone <= this.startByte) {
+ this.statistics = '';
+ } else {
+ kBps = (bytesDone - this.startByte) /
+ (Date.now() - this.startTime);
+ this.statistics = (
+ '' + this.$scope.numberFilter(bytesDone/1024, '0') + ' KB ' +
+ 'at ~' + this.$scope.numberFilter(kBps, '0') + ' KB/s')
+ if (bytesDone < this.file.size) {
+ this.statistics += ' -- ETA ' +
+ this.$scope.dateFilter(
+ new Date(
+ Date.now() + (this.file.size - bytesDone) / kBps),
+ 'shortTime')
+ } else {
+ this.statistics += ' -- finished @ ' +
+ this.$scope.dateFilter(Date.now(), 'shortTime');
+ }
+ }
+ console.log(this.progress);
+ this._deferred.notify();
}
});
}
-function QueueUploader(workarea) {
+function QueueUploader($scope) {
var that = this;
- $('.arv-upload-start', workarea).on('change', function(event){
- that.queueFilesFromInput(event);
- });
$.extend(this, {
- workarea: workarea,
- queue: [],
+ _running: false,
+ statusError: null,
+ statusSuccess: null,
arvados: new ArvadosClient(
- $('[data-discovery-uri]').data('discovery-uri'),
+ $('meta[name=arvados-discovery-uri]').attr('content'),
$('meta[name=arvados-api-token]').attr('content')
),
- queueFilesFromInput: function(event) {
- var i, f;
- for (i=0; i<event.target.files.length; i++) {
- f = event.target.files[i];
- console.log("Queueing file: " + f.name + " (" + f.type + ")");
- this.queue.push(new FileUploader(this, f));
- }
- this.arvados.apiPromise(
+ go: function($scope) {
+ if (this._running) return $.when(true,true);
+ this._running = true;
+ return this.arvados.apiPromise(
'keep_services', 'list',
{filters: [['service_type','=','proxy']]}).
then(this.doQueueWithProxy).
- then(this.reportUploadSuccess, this.reportUploadError);
+ then(this.reportUploadSuccess, this.reportUploadError).
+ always(function(){ that._running = false; });
},
doQueueWithProxy: function(data) {
that.keepProxy = data.items[0];
return that.doQueueWork();
},
doQueueWork: function() {
- $('.alert-danger,.alert-success', that.workarea).hide();
+ that.statusError = null;
+ that.statusSuccess = null;
if (!that.keepProxy) {
return $.Deferred().
reject(null, null, "Sorry, there seems to be no Keep proxy service available.").
promise();
}
- while (that.queue.length > 0 && that.queue[0].done) {
- that.queue.shift();
- }
- if (that.queue.length === 0) {
- return $.Deferred().resolve("Done!").promise();
- } else {
- return that.queue[0].go().then(
- that.doQueueWork,
- that.reportUploadError);
+ for (var i=0; i<$scope.uploadQueue.length; i++) {
+ if ($scope.uploadQueue[i].state == 'Queued') {
+ return $scope.uploadQueue[i].
+ go().
+ then(
+ that.doQueueWork,
+ that.reportUploadError);
+ }
}
+ return $.Deferred().resolve("Done!").promise();
},
reportUploadError: function(jqxhr, textStatus, err) {
console.log(["error", jqxhr,textStatus,err]);
- $('.alert-danger', that.workarea).show().find('.content').
- html((textStatus || 'Error') +
- (jqxhr && jqxhr.options
- ? (' (from ' + jqxhr.options.url + ')')
- : '') +
- ': ' +
- (err || '(no further details available, sorry!)'));
+ that.statusError = (
+ (textStatus || 'Error') +
+ (jqxhr && jqxhr.options
+ ? (' (from ' + jqxhr.options.url + ')')
+ : '') +
+ ': ' +
+ (err || '(no further details available, sorry!)'));
},
reportUploadSuccess: function(message) {
- console.log(message);
- $('.alert-success', that.workarea).show().find('.content').
- html(message);
+ that.statusSuccess = message;
}
});
}
diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss
index 7dbeac9..8b5580f 100644
--- a/apps/workbench/app/assets/stylesheets/application.css.scss
+++ b/apps/workbench/app/assets/stylesheets/application.css.scss
@@ -47,6 +47,9 @@ table.table-justforlayout {
font-size: .8em;
color: #888;
}
+.lighten {
+ color: #888;
+}
.arvados-filename,
.arvados-uuid {
font-size: .8em;
diff --git a/apps/workbench/app/views/collections/_show_files.html.erb b/apps/workbench/app/views/collections/_show_files.html.erb
index e25f29a..ea31c84 100644
--- a/apps/workbench/app/views/collections/_show_files.html.erb
+++ b/apps/workbench/app/views/collections/_show_files.html.erb
@@ -63,16 +63,16 @@ function unselect_all_files() {
<% file_tree = @object.andand.files_tree %>
<% if file_tree.nil? or file_tree.empty? %>
<p>This collection is empty.</p>
- <div ng-app="Workbench" data-discovery-uri="<%= Rails.configuration.arvados_v1_base.sub('/arvados/v1', '') %>/discovery/v1/apis/arvados/v1/rest" class="panel panel-info">
+ <div ng-app="Workbench" class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">Upload files</h3></div>
- <div class="panel-body" ng-controller="UploadToCollection">
+ <div class="panel-body" ng-controller="UploadController">
<div class="panel panel-primary">
<div class="panel-body">
<input type="file" multiple ng-model="incoming" onchange="angular.element(this).scope().addFilesToQueue(this.files); $(this).val('');" />
</div>
</div>
- <div ng-show="statusSuccess" class="alert alert-success"><i class="fa fa-flag-checkered"></i> {{statusSuccess}}</div>
- <div ng-show="statusError" class="alert alert-danger"><i class="fa fa-warning"></i> {{statusError}}</div>
+ <div ng-show="uploader.statusSuccess" class="alert alert-success"><i class="fa fa-flag-checkered"></i> {{uploader.statusSuccess}}</div>
+ <div ng-show="uploader.statusError" class="alert alert-danger"><i class="fa fa-warning"></i> {{uploader.statusError}}</div>
<div ng-repeat="upload in uploadQueue" class="row">
<div class="col-sm-2">
<div class="progress">
@@ -85,8 +85,8 @@ function unselect_all_files() {
<div class="col-sm-4">
{{upload.file.name}}
</div>
- <div class="col-sm-5">
- {{upload.state}}
+ <div class="col-sm-5" ng-class="{lighten: upload.state != 'Uploading'}">
+ {{upload.statistics}}
</div>
</div>
</div>
diff --git a/apps/workbench/app/views/layouts/application.html.erb b/apps/workbench/app/views/layouts/application.html.erb
index 3fff432..6fab360 100644
--- a/apps/workbench/app/views/layouts/application.html.erb
+++ b/apps/workbench/app/views/layouts/application.html.erb
@@ -18,6 +18,7 @@
<meta name="arv-websocket-url" content="<%=$arvados_api_client.discovery[:websocketUrl]%>?api_token=<%=Thread.current[:arvados_api_token]%>">
<% end %>
<meta name="arvados-api-token" content="<%=Thread.current[:arvados_api_token]%>">
+ <meta name="arvados-discovery-uri" content="<%= Rails.configuration.arvados_v1_base.sub '/arvados/v1', '/discovery/v1/apis/arvados/v1/rest' %>">
<meta name="robots" content="NOINDEX, NOFOLLOW">
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
commit bfe5b9d98f73532231f8a2d4c0f8906e28d536f0
Author: Tom Clegg <tom at curoverse.com>
Date: Sun Nov 23 03:24:10 2014 -0500
3781: Build and show upload queue with angular.
diff --git a/apps/workbench/Gemfile b/apps/workbench/Gemfile
index 5ab6eac..365cefc 100644
--- a/apps/workbench/Gemfile
+++ b/apps/workbench/Gemfile
@@ -56,6 +56,8 @@ gem 'bootstrap-sass', '~> 3.1.0'
gem 'bootstrap-x-editable-rails'
gem 'bootstrap-tab-history-rails'
+gem 'angularjs-rails'
+
gem 'less'
gem 'less-rails'
gem 'wiselinks'
diff --git a/apps/workbench/Gemfile.lock b/apps/workbench/Gemfile.lock
index 8b9ea94..8c18847 100644
--- a/apps/workbench/Gemfile.lock
+++ b/apps/workbench/Gemfile.lock
@@ -38,6 +38,7 @@ GEM
tzinfo (~> 1.1)
addressable (2.3.6)
andand (1.3.3)
+ angularjs-rails (1.3.3)
arel (5.0.1.20140414130214)
arvados (0.1.20141114230720)
activesupport (>= 3.2.13)
@@ -242,6 +243,7 @@ PLATFORMS
DEPENDENCIES
RedCloth
andand
+ angularjs-rails
arvados (>= 0.1.20141114230720)
bootstrap-sass (~> 3.1.0)
bootstrap-tab-history-rails
diff --git a/apps/workbench/app/assets/javascripts/application.js b/apps/workbench/app/assets/javascripts/application.js
index 7737eee..6b98fd9 100644
--- a/apps/workbench/app/assets/javascripts/application.js
+++ b/apps/workbench/app/assets/javascripts/application.js
@@ -23,6 +23,7 @@
//= require bootstrap3-editable/bootstrap-editable
//= require bootstrap-tab-history
//= require wiselinks
+//= require angular
//= require_tree .
jQuery(function($){
diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js
index 767c38e..1c4df0f 100644
--- a/apps/workbench/app/assets/javascripts/upload_to_collection.js
+++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js
@@ -1,3 +1,24 @@
+var app = angular.module('Workbench', []);
+app.controller('UploadToCollection', ['$scope', function($scope) {
+ $.extend($scope, {
+ uploadQueue: [],
+ addFilesToQueue: function(files) {
+ // Angular binding doesn't work its usual magic for file
+ // inputs, so we need to $scope.$apply() this update.
+ $scope.$apply(function(){
+ var i;
+ for (i=0; i<files.length; i++) {
+ $scope.uploadQueue.push({
+ file: files[i]
+ });
+ }
+ });
+ },
+ statusSuccess: null,
+ statusError: null
+ });
+}]);
+
$(document).on('ready ajax:success', function() {
$('.arv-upload-to-collection').
not('.arv-upload-setup').
@@ -151,7 +172,7 @@ function QueueUploader(workarea) {
workarea: workarea,
queue: [],
arvados: new ArvadosClient(
- workarea.data('discovery-uri'),
+ $('[data-discovery-uri]').data('discovery-uri'),
$('meta[name=arvados-api-token]').attr('content')
),
queueFilesFromInput: function(event) {
diff --git a/apps/workbench/app/views/collections/_show_files.html.erb b/apps/workbench/app/views/collections/_show_files.html.erb
index 135c5c0..e25f29a 100644
--- a/apps/workbench/app/views/collections/_show_files.html.erb
+++ b/apps/workbench/app/views/collections/_show_files.html.erb
@@ -63,12 +63,33 @@ function unselect_all_files() {
<% file_tree = @object.andand.files_tree %>
<% if file_tree.nil? or file_tree.empty? %>
<p>This collection is empty.</p>
- <div class="arv-upload-to-collection" data-discovery-uri="<%= Rails.configuration.arvados_v1_base.sub('/arvados/v1', '') %>/discovery/v1/apis/arvados/v1/rest">
- <form>
- <input type="file" multiple class="arv-upload-start" />
- <div style="display:none" class="alert alert-success"><i class="fa fa-flag-checkered"></i> <span class="content"></span></div>
- <div style="display:none" class="alert alert-danger"><i class="fa fa-warning"></i> <span class="content"></span></div>
- </form>
+ <div ng-app="Workbench" data-discovery-uri="<%= Rails.configuration.arvados_v1_base.sub('/arvados/v1', '') %>/discovery/v1/apis/arvados/v1/rest" class="panel panel-info">
+ <div class="panel-heading"><h3 class="panel-title">Upload files</h3></div>
+ <div class="panel-body" ng-controller="UploadToCollection">
+ <div class="panel panel-primary">
+ <div class="panel-body">
+ <input type="file" multiple ng-model="incoming" onchange="angular.element(this).scope().addFilesToQueue(this.files); $(this).val('');" />
+ </div>
+ </div>
+ <div ng-show="statusSuccess" class="alert alert-success"><i class="fa fa-flag-checkered"></i> {{statusSuccess}}</div>
+ <div ng-show="statusError" class="alert alert-danger"><i class="fa fa-warning"></i> {{statusError}}</div>
+ <div ng-repeat="upload in uploadQueue" class="row">
+ <div class="col-sm-2">
+ <div class="progress">
+ <span class="progress-bar" style="width: {{upload.progress}}%"></span>
+ </div>
+ </div>
+ <div class="col-sm-1" style="text-align: right">
+ {{upload.file.size/1024 | number:0}}K
+ </div>
+ <div class="col-sm-4">
+ {{upload.file.name}}
+ </div>
+ <div class="col-sm-5">
+ {{upload.state}}
+ </div>
+ </div>
+ </div>
</div>
<% else %>
<ul id="collection_files" class="collection_files <%=preview_selectable_container%>">
commit 1fc13b7710c858b88cdd15957202d3b43000f33f
Author: Tom Clegg <tom at curoverse.com>
Date: Sun Nov 23 02:09:11 2014 -0500
3781: Add allowed headers. Respond to OPTIONS at any path.
diff --git a/services/keepproxy/keepproxy.go b/services/keepproxy/keepproxy.go
index 620434f..e547efd 100644
--- a/services/keepproxy/keepproxy.go
+++ b/services/keepproxy/keepproxy.go
@@ -248,7 +248,7 @@ func MakeRESTRouter(
rest.Handle(`/{hash:[0-9a-f]{32}}+{hints}`, PutBlockHandler{kc, t}).Methods("PUT")
rest.Handle(`/{hash:[0-9a-f]{32}}`, PutBlockHandler{kc, t}).Methods("PUT")
rest.Handle(`/`, PutBlockHandler{kc, t}).Methods("POST")
- rest.Handle(`/{hash:[0-9a-f]{32}}{ignore}`, OptionsHandler{}).Methods("OPTIONS")
+ rest.Handle(`/`, OptionsHandler{}).Methods("OPTIONS")
}
rest.NotFoundHandler = InvalidPathHandler{}
@@ -256,6 +256,13 @@ func MakeRESTRouter(
return rest
}
+func SetCorsHeaders(resp http.ResponseWriter) {
+ resp.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, OPTIONS")
+ resp.Header().Set("Access-Control-Allow-Origin", "*")
+ resp.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, X-Keep-Desired-Replicas")
+ resp.Header().Set("Access-Control-Max-Age", "86486400")
+}
+
func (this InvalidPathHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
log.Printf("%s: %s %s unroutable", GetRemoteAddress(req), req.Method, req.URL.Path)
http.Error(resp, "Bad request", http.StatusBadRequest)
@@ -263,15 +270,11 @@ func (this InvalidPathHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req
func (this OptionsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
log.Printf("%s: %s %s", GetRemoteAddress(req), req.Method, req.URL.Path)
- resp.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, OPTIONS")
- resp.Header().Set("Access-Control-Allow-Origin", "*")
- resp.Header().Set("Access-Control-Allow-Headers", "Authorization, X-Keep-Desired-Replicas")
- resp.Header().Set("Access-Control-Max-Age", "86486400")
+ SetCorsHeaders(resp)
}
func (this GetBlockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
- resp.Header().Set("Access-Control-Allow-Origin", "*")
- resp.Header().Set("Access-Control-Allow-Headers", "Authorization")
+ SetCorsHeaders(resp)
kc := *this.KeepClient
@@ -335,6 +338,7 @@ func (this GetBlockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Reques
}
func (this PutBlockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ SetCorsHeaders(resp)
kc := *this.KeepClient
diff --git a/services/keepproxy/keepproxy_test.go b/services/keepproxy/keepproxy_test.go
index 667a0b2..0ac843f 100644
--- a/services/keepproxy/keepproxy_test.go
+++ b/services/keepproxy/keepproxy_test.go
@@ -349,7 +349,7 @@ func (s *ServerRequiredSuite) TestCorsHeaders(c *C) {
fmt.Sprintf("http://localhost:29954/%x+3",
md5.Sum([]byte("foo"))))
c.Check(err, Equals, nil)
- c.Check(resp.Header.Get("Access-Control-Allow-Headers"), Equals, "Authorization")
+ c.Check(resp.Header.Get("Access-Control-Allow-Headers"), Equals, "Authorization, Content-Length, Content-Type, X-Keep-Desired-Replicas")
c.Check(resp.Header.Get("Access-Control-Allow-Origin"), Equals, "*")
}
}
commit 9d5164a06a3dbf2732a149f4e1b9945b942fb30f
Author: Tom Clegg <tom at curoverse.com>
Date: Sat Nov 22 16:03:36 2014 -0500
3781: Upload blobs to proxy.
diff --git a/apps/workbench/app/assets/javascripts/arvados_client.js b/apps/workbench/app/assets/javascripts/arvados_client.js
index 6f4eed1..92db84f 100644
--- a/apps/workbench/app/assets/javascripts/arvados_client.js
+++ b/apps/workbench/app/assets/javascripts/arvados_client.js
@@ -1,9 +1,10 @@
-function arvadosClient(discoveryUri, apiToken) {
- var self = {
+function ArvadosClient(discoveryUri, apiToken) {
+ $.extend(this, {
apiToken: apiToken,
- api: function(controller, action, params) {
- return self.getDiscoveryDoc().then(function() {
- meth = self.discoveryDoc.resources[controller].methods[action];
+ discoveryUri: discoveryUri,
+ apiPromise: function(controller, action, params) {
+ return this.getDiscoveryDoc().then(function() {
+ meth = this.discoveryDoc.resources[controller].methods[action];
data = $.extend({}, params, {_method: meth.httpMethod});
$.each(data, function(k, v) {
if (typeof(v) == 'object') {
@@ -11,27 +12,27 @@ function arvadosClient(discoveryUri, apiToken) {
}
});
return $.ajax({
- url: self.discoveryDoc.baseUrl + meth.path,
+ url: this.discoveryDoc.baseUrl + meth.path,
type: 'POST',
crossDomain: true,
dataType: 'json',
data: data,
headers: {
- Authorization: 'OAuth2 ' + self.apiToken
+ Authorization: 'OAuth2 ' + this.apiToken
}
});
});
},
getDiscoveryDoc: function() {
- if (self.promiseDiscovery) return self.promiseDiscovery;
- self.promiseDiscovery = $.ajax({
- url: discoveryUri,
- crossDomain: true
+ if (this.promiseDiscovery) return this.promiseDiscovery;
+ this.promiseDiscovery = $.ajax({
+ url: this.discoveryUri,
+ crossDomain: true,
+ context: this
}).then(function(data, status, xhr) {
- self.discoveryDoc = data;
+ this.discoveryDoc = data;
});
- return self.promiseDiscovery;
+ return this.promiseDiscovery;
}
- };
- return self;
+ });
}
diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js
index aff6c91..767c38e 100644
--- a/apps/workbench/app/assets/javascripts/upload_to_collection.js
+++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js
@@ -2,39 +2,207 @@ $(document).on('ready ajax:success', function() {
$('.arv-upload-to-collection').
not('.arv-upload-setup').
addClass('.arv-upload-setup').
- each(setupUploader);
+ each(function() { new QueueUploader($(this)) });
});
-function setupUploader() {
- var $workarea = $(this);
- var self = {
- $workarea: $workarea,
+function SliceReader(uploader, slice) {
+ var that = this;
+ $.extend(this, {
+ _deferred: null,
+ _failCount: 0,
+ _reader: null,
+ _slice: slice,
+ _uploader: uploader,
+ go: function() {
+ this._deferred = $.Deferred();
+ this._reader = new FileReader();
+ this._reader.onload = this.resolve;
+ this._reader.onerror = this._deferred.reject;
+ this._reader.onprogress = this._deferred.notify;
+ this._reader.readAsArrayBuffer(this._slice.blob);
+ return this._deferred.promise();
+ },
+ resolve: function() {
+ 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.
+ return $.Deferred().reject(
+ null, "Read error",
+ "Short read: wanted " + that._slice.size +
+ ", received " + that._reader.result.length).promise();
+ }
+ return that._deferred.resolve(that._reader.result);
+ }
+ });
+}
+
+function SliceUploader(uploader) {
+ var that = this;
+ $.extend(this, {
+ _data: null,
+ _dataSize: null,
+ _jqxhr: null,
+ _uploader: uploader,
+ go: function(data, dataSize) {
+ // Send data to the Keep proxy. Retry a few times on
+ // fail. Return a promise that will get resolved with
+ // resolve(locator) when the block is accepted by the
+ // proxy.
+ this._data = data;
+ this._dataSize = dataSize;
+ this._jqxhr = $.ajax({
+ url: this.proxyUriBase(),
+ type: 'POST',
+ crossDomain: true,
+ headers: {
+ 'Authorization': 'OAuth2 '+this._uploader.arvados.apiToken,
+ 'Content-Type': 'application/octet-stream',
+ 'X-Keep-Desired-Replicas': '2'
+ },
+ xhr: function() {
+ var xhr = $.ajaxSettings.xhr();
+ if (xhr.upload) {
+ xhr.upload.addEventListener(
+ 'progress', this.progressSendSlice);
+ }
+ return xhr;
+ },
+ processData: false,
+ data: data,
+ context: this
+ });
+ return this._jqxhr.then(this.doneSendSlice);
+ },
+ progressSendSlice: function(x,y,z) {
+ console.log(['uploadProgress',x,y,z]);
+ },
+ doneSendSlice: function(data, textStatus, jqxhr) {
+ return $.Deferred().resolve(data, that._dataSize).promise();
+ },
+ failSendSlice: function(xhr, textStatus, err) {
+ console.log([xhr, textStatus, err]);
+ if (++that._failCount <= 3) {
+ console.log("Error (" + textStatus + "), retrying slice at " +
+ that.bytesDone);
+ return that.go(that._data, that._dataSize);
+ }
+ return $.Deferred().reject(xhr, textStatus, err).promise();
+ },
+ proxyUriBase: function() {
+ var proxy = this._uploader.keepProxy;
+ return ((proxy.service_ssl_flag ? 'https' : 'http') +
+ '://' + proxy.service_host + ':' +
+ proxy.service_port + '/');
+ }
+ });
+}
+
+function FileUploader(uploader, file) {
+ var that = this;
+ $.extend(this, {
+ _currentSlice: null,
+ _locators: [],
+ uploader: uploader,
+ file: file,
+ maxBlobSize: 65536,
+ bytesDone: 0,
+ queueTime: Date.now(),
+ startTime: null,
+ _readPos: 0,
+ done: false,
+ go: function() {
+ this._currentSlice = this.nextSlice();
+ if (!this._currentSlice) {
+ return $.when(true, true);
+ }
+ return (new SliceUploader(this.uploader)).
+ go(this._currentSlice.blob, this._currentSlice.size).
+ then(this.onUploaderSuccess);
+ },
+ onUploaderSuccess: function(locator, dataSize) {
+ that._locators.push(locator);
+ return that.go();
+ },
+ nextSlice: function() {
+ var size = Math.min(
+ this.maxBlobSize,
+ this.file.size - this._readPos);
+ if (size == 0) {
+ this.done = true;
+ return false;
+ }
+ var blob = this.file.slice(
+ this._readPos,
+ this._readPos + size,
+ 'application/octet-stream; charset=x-user-defined');
+ this._readPos += size;
+ return {blob: blob, size: size};
+ }
+ });
+}
+
+function QueueUploader(workarea) {
+ var that = this;
+ $('.arv-upload-start', workarea).on('change', function(event){
+ that.queueFilesFromInput(event);
+ });
+ $.extend(this, {
+ workarea: workarea,
queue: [],
- arvados: arvadosClient(
- $workarea.data('discovery-uri'),
+ arvados: new ArvadosClient(
+ workarea.data('discovery-uri'),
$('meta[name=arvados-api-token]').attr('content')
),
queueFilesFromInput: function(event) {
- self.queue.push({fileInput: event.target});
- self.arvados.api('keep_services', 'list', {filters: [['service_type','=','proxy']]}).then(function(data) {
- self.keepProxy = data.items[0];
- self.doUploadWork();
- }, self.reportUploadError);
- },
- doUploadWork: function() {
- console.log(["work queue is", self.queue]);
- console.log(["keep proxy is", self.keepProxy]);
- if (!self.keepProxy) {
- self.reportUploadError(
- "Sorry, there seems to be no Keep proxy service available.");
- return;
- }
- },
- reportUploadError: function(err) {
- $('.alert-danger', $workarea).show().find('.content').html(err);
+ var i, f;
+ for (i=0; i<event.target.files.length; i++) {
+ f = event.target.files[i];
+ console.log("Queueing file: " + f.name + " (" + f.type + ")");
+ this.queue.push(new FileUploader(this, f));
+ }
+ this.arvados.apiPromise(
+ 'keep_services', 'list',
+ {filters: [['service_type','=','proxy']]}).
+ then(this.doQueueWithProxy).
+ then(this.reportUploadSuccess, this.reportUploadError);
+ },
+ doQueueWithProxy: function(data) {
+ that.keepProxy = data.items[0];
+ return that.doQueueWork();
+ },
+ doQueueWork: function() {
+ $('.alert-danger,.alert-success', that.workarea).hide();
+ if (!that.keepProxy) {
+ return $.Deferred().
+ reject(null, null, "Sorry, there seems to be no Keep proxy service available.").
+ promise();
+ }
+ while (that.queue.length > 0 && that.queue[0].done) {
+ that.queue.shift();
+ }
+ if (that.queue.length === 0) {
+ return $.Deferred().resolve("Done!").promise();
+ } else {
+ return that.queue[0].go().then(
+ that.doQueueWork,
+ that.reportUploadError);
+ }
+ },
+ reportUploadError: function(jqxhr, textStatus, err) {
+ console.log(["error", jqxhr,textStatus,err]);
+ $('.alert-danger', that.workarea).show().find('.content').
+ html((textStatus || 'Error') +
+ (jqxhr && jqxhr.options
+ ? (' (from ' + jqxhr.options.url + ')')
+ : '') +
+ ': ' +
+ (err || '(no further details available, sorry!)'));
+ },
+ reportUploadSuccess: function(message) {
+ console.log(message);
+ $('.alert-success', that.workarea).show().find('.content').
+ html(message);
}
- };
- $('.arv-upload-start', $workarea).on('change', self.queueFilesFromInput);
- $workarea.on('arv:upload:work', self.doUploadWork);
- return self;
+ });
}
diff --git a/apps/workbench/app/views/collections/_show_files.html.erb b/apps/workbench/app/views/collections/_show_files.html.erb
index 4ca58e9..135c5c0 100644
--- a/apps/workbench/app/views/collections/_show_files.html.erb
+++ b/apps/workbench/app/views/collections/_show_files.html.erb
@@ -66,6 +66,7 @@ function unselect_all_files() {
<div class="arv-upload-to-collection" data-discovery-uri="<%= Rails.configuration.arvados_v1_base.sub('/arvados/v1', '') %>/discovery/v1/apis/arvados/v1/rest">
<form>
<input type="file" multiple class="arv-upload-start" />
+ <div style="display:none" class="alert alert-success"><i class="fa fa-flag-checkered"></i> <span class="content"></span></div>
<div style="display:none" class="alert alert-danger"><i class="fa fa-warning"></i> <span class="content"></span></div>
</form>
</div>
commit b982b8ac8c61b862067ced40e001714d6dedf800
Author: Tom Clegg <tom at curoverse.com>
Date: Sat Nov 22 15:45:14 2014 -0500
3781: Add POST method for writing without knowing MD5.
diff --git a/services/keepproxy/keepproxy.go b/services/keepproxy/keepproxy.go
index b74a982..620434f 100644
--- a/services/keepproxy/keepproxy.go
+++ b/services/keepproxy/keepproxy.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/gorilla/mux"
"io"
+ "io/ioutil"
"log"
"net"
"net/http"
@@ -246,6 +247,7 @@ func MakeRESTRouter(
if enable_put {
rest.Handle(`/{hash:[0-9a-f]{32}}+{hints}`, PutBlockHandler{kc, t}).Methods("PUT")
rest.Handle(`/{hash:[0-9a-f]{32}}`, PutBlockHandler{kc, t}).Methods("PUT")
+ rest.Handle(`/`, PutBlockHandler{kc, t}).Methods("POST")
rest.Handle(`/{hash:[0-9a-f]{32}}{ignore}`, OptionsHandler{}).Methods("OPTIONS")
}
@@ -261,7 +263,7 @@ func (this InvalidPathHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req
func (this OptionsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
log.Printf("%s: %s %s", GetRemoteAddress(req), req.Method, req.URL.Path)
- resp.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, OPTIONS")
+ resp.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, OPTIONS")
resp.Header().Set("Access-Control-Allow-Origin", "*")
resp.Header().Set("Access-Control-Allow-Headers", "Authorization, X-Keep-Desired-Replicas")
resp.Header().Set("Access-Control-Max-Age", "86486400")
@@ -384,7 +386,20 @@ func (this PutBlockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Reques
}
// Now try to put the block through
- hash, replicas, err := kc.PutHR(hash, req.Body, contentLength)
+ var replicas int
+ var err error
+ if hash == "" {
+ if bytes, err := ioutil.ReadAll(req.Body); err != nil {
+ msg := fmt.Sprintf("Error reading request body: %s", err)
+ log.Printf(msg)
+ http.Error(resp, msg, http.StatusInternalServerError)
+ return
+ } else {
+ hash, replicas, err = kc.PutB(bytes)
+ }
+ } else {
+ hash, replicas, err = kc.PutHR(hash, req.Body, contentLength)
+ }
// Tell the client how many successful PUTs we accomplished
resp.Header().Set(keepclient.X_Keep_Replicas_Stored, fmt.Sprintf("%d", replicas))
diff --git a/services/keepproxy/keepproxy_test.go b/services/keepproxy/keepproxy_test.go
index ba9793d..667a0b2 100644
--- a/services/keepproxy/keepproxy_test.go
+++ b/services/keepproxy/keepproxy_test.go
@@ -14,6 +14,7 @@ import (
"net/url"
"os"
"os/exec"
+ "strings"
"testing"
"time"
)
@@ -339,7 +340,7 @@ func (s *ServerRequiredSuite) TestCorsHeaders(c *C) {
c.Check(resp.StatusCode, Equals, 200)
body, err := ioutil.ReadAll(resp.Body)
c.Check(string(body), Equals, "")
- c.Check(resp.Header.Get("Access-Control-Allow-Methods"), Equals, "GET, HEAD, PUT, OPTIONS")
+ c.Check(resp.Header.Get("Access-Control-Allow-Methods"), Equals, "GET, HEAD, POST, PUT, OPTIONS")
c.Check(resp.Header.Get("Access-Control-Allow-Origin"), Equals, "*")
}
@@ -352,3 +353,24 @@ func (s *ServerRequiredSuite) TestCorsHeaders(c *C) {
c.Check(resp.Header.Get("Access-Control-Allow-Origin"), Equals, "*")
}
}
+
+func (s *ServerRequiredSuite) TestPostWithoutHash(c *C) {
+ runProxy(c, []string{"keepproxy"}, "4axaw8zxe0qm22wa6urpp5nskcne8z88cvbupv653y1njyi05h", 29955)
+ waitForListener()
+ defer closeListener()
+
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("POST",
+ "http://localhost:29955/",
+ strings.NewReader("qux"))
+ req.Header.Add("Authorization", "OAuth2 4axaw8zxe0qm22wa6urpp5nskcne8z88cvbupv653y1njyi05h")
+ req.Header.Add("Content-Type", "application/octet-stream")
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(err, Equals, nil)
+ c.Check(string(body), Equals,
+ fmt.Sprintf("%x+%d", md5.Sum([]byte("qux")), 3))
+ }
+}
commit 3df51093d693e89e14e35d3c5f73e74978b7e20e
Author: Tom Clegg <tom at curoverse.com>
Date: Sat Nov 22 14:40:34 2014 -0500
3781: Beginnings of API client and uploader
diff --git a/apps/workbench/app/assets/javascripts/application.js b/apps/workbench/app/assets/javascripts/application.js
index 1990b8b..7737eee 100644
--- a/apps/workbench/app/assets/javascripts/application.js
+++ b/apps/workbench/app/assets/javascripts/application.js
@@ -26,12 +26,6 @@
//= require_tree .
jQuery(function($){
- $.ajaxSetup({
- headers: {
- 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
- }
- });
-
$(document).ajaxStart(function(){
$('.modal-with-loading-spinner .spinner').show();
}).ajaxStop(function(){
diff --git a/apps/workbench/app/assets/javascripts/arvados_client.js b/apps/workbench/app/assets/javascripts/arvados_client.js
new file mode 100644
index 0000000..6f4eed1
--- /dev/null
+++ b/apps/workbench/app/assets/javascripts/arvados_client.js
@@ -0,0 +1,37 @@
+function arvadosClient(discoveryUri, apiToken) {
+ var self = {
+ apiToken: apiToken,
+ api: function(controller, action, params) {
+ return self.getDiscoveryDoc().then(function() {
+ meth = self.discoveryDoc.resources[controller].methods[action];
+ data = $.extend({}, params, {_method: meth.httpMethod});
+ $.each(data, function(k, v) {
+ if (typeof(v) == 'object') {
+ data[k] = JSON.stringify(v);
+ }
+ });
+ return $.ajax({
+ url: self.discoveryDoc.baseUrl + meth.path,
+ type: 'POST',
+ crossDomain: true,
+ dataType: 'json',
+ data: data,
+ headers: {
+ Authorization: 'OAuth2 ' + self.apiToken
+ }
+ });
+ });
+ },
+ getDiscoveryDoc: function() {
+ if (self.promiseDiscovery) return self.promiseDiscovery;
+ self.promiseDiscovery = $.ajax({
+ url: discoveryUri,
+ crossDomain: true
+ }).then(function(data, status, xhr) {
+ self.discoveryDoc = data;
+ });
+ return self.promiseDiscovery;
+ }
+ };
+ return self;
+}
diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js
new file mode 100644
index 0000000..aff6c91
--- /dev/null
+++ b/apps/workbench/app/assets/javascripts/upload_to_collection.js
@@ -0,0 +1,40 @@
+$(document).on('ready ajax:success', function() {
+ $('.arv-upload-to-collection').
+ not('.arv-upload-setup').
+ addClass('.arv-upload-setup').
+ each(setupUploader);
+});
+
+function setupUploader() {
+ var $workarea = $(this);
+ var self = {
+ $workarea: $workarea,
+ queue: [],
+ arvados: arvadosClient(
+ $workarea.data('discovery-uri'),
+ $('meta[name=arvados-api-token]').attr('content')
+ ),
+ queueFilesFromInput: function(event) {
+ self.queue.push({fileInput: event.target});
+ self.arvados.api('keep_services', 'list', {filters: [['service_type','=','proxy']]}).then(function(data) {
+ self.keepProxy = data.items[0];
+ self.doUploadWork();
+ }, self.reportUploadError);
+ },
+ doUploadWork: function() {
+ console.log(["work queue is", self.queue]);
+ console.log(["keep proxy is", self.keepProxy]);
+ if (!self.keepProxy) {
+ self.reportUploadError(
+ "Sorry, there seems to be no Keep proxy service available.");
+ return;
+ }
+ },
+ reportUploadError: function(err) {
+ $('.alert-danger', $workarea).show().find('.content').html(err);
+ }
+ };
+ $('.arv-upload-start', $workarea).on('change', self.queueFilesFromInput);
+ $workarea.on('arv:upload:work', self.doUploadWork);
+ return self;
+}
diff --git a/apps/workbench/app/views/collections/_show_files.html.erb b/apps/workbench/app/views/collections/_show_files.html.erb
index 76d8731..4ca58e9 100644
--- a/apps/workbench/app/views/collections/_show_files.html.erb
+++ b/apps/workbench/app/views/collections/_show_files.html.erb
@@ -63,6 +63,12 @@ function unselect_all_files() {
<% file_tree = @object.andand.files_tree %>
<% if file_tree.nil? or file_tree.empty? %>
<p>This collection is empty.</p>
+ <div class="arv-upload-to-collection" data-discovery-uri="<%= Rails.configuration.arvados_v1_base.sub('/arvados/v1', '') %>/discovery/v1/apis/arvados/v1/rest">
+ <form>
+ <input type="file" multiple class="arv-upload-start" />
+ <div style="display:none" class="alert alert-danger"><i class="fa fa-warning"></i> <span class="content"></span></div>
+ </form>
+ </div>
<% else %>
<ul id="collection_files" class="collection_files <%=preview_selectable_container%>">
<% dirstack = [file_tree.first.first] %>
diff --git a/apps/workbench/app/views/layouts/application.html.erb b/apps/workbench/app/views/layouts/application.html.erb
index 324714e..3fff432 100644
--- a/apps/workbench/app/views/layouts/application.html.erb
+++ b/apps/workbench/app/views/layouts/application.html.erb
@@ -17,6 +17,7 @@
<% if current_user and $arvados_api_client.discovery[:websocketUrl] %>
<meta name="arv-websocket-url" content="<%=$arvados_api_client.discovery[:websocketUrl]%>?api_token=<%=Thread.current[:arvados_api_token]%>">
<% end %>
+ <meta name="arvados-api-token" content="<%=Thread.current[:arvados_api_token]%>">
<meta name="robots" content="NOINDEX, NOFOLLOW">
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
diff --git a/apps/workbench/app/views/projects/show.html.erb b/apps/workbench/app/views/projects/show.html.erb
index 0429f33..7769e1b 100644
--- a/apps/workbench/app/views/projects/show.html.erb
+++ b/apps/workbench/app/views/projects/show.html.erb
@@ -6,17 +6,30 @@
<% content_for :tab_line_buttons do %>
<% if @object.editable? %>
- <%= link_to(
- choose_collections_path(
- title: 'Add data to project:',
- multiple: true,
- action_name: 'Add',
- action_href: actions_path(id: @object.uuid),
- action_method: 'post',
- action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
- { class: "btn btn-primary btn-sm", remote: true, method: 'get', title: "Add data to this project", data: {'event-after-select' => 'page-refresh'} }) do %>
- <i class="fa fa-fw fa-plus"></i> Add data...
- <% end %>
+ <div class="btn-group btn-group-sm">
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">Add data <span class="caret"></span></button>
+ <ul class="dropdown-menu" role="menu">
+ <li>
+ <%= link_to(
+ choose_collections_path(
+ title: 'Choose a collection to copy into this project:',
+ multiple: true,
+ action_name: 'Copy',
+ action_href: actions_path(id: @object.uuid),
+ action_method: 'post',
+ action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
+ { remote: true, method: 'get', title: "Copy a collection from another project into this one", data: {'event-after-select' => 'page-refresh', 'toggle' => 'dropdown'} }) do %>
+ <i class="fa fa-fw fa-clipboard"></i> ...from a different project
+ <% end %>
+ </li>
+ <li>
+ <%= link_to(collections_path(collection: {manifest_text: ""}),
+ { method: 'post', title: "Add data to this project", data: {'event-after-select' => 'page-refresh', 'toggle' => 'dropdown'} }) do %>
+ <i class="fa fa-fw fa-upload"></i> ...from your computer
+ <% end %>
+ </li>
+ </ul>
+ </div>
<%= link_to(
choose_pipeline_templates_path(
title: 'Choose a pipeline to run:',
commit af04cad1fbc8b90fca94610b312b1f710a0a94b1
Author: Tom Clegg <tom at curoverse.com>
Date: Sat Nov 22 05:01:13 2014 -0500
3781: Set CORS headers in keepproxy responses.
diff --git a/services/keepproxy/keepproxy.go b/services/keepproxy/keepproxy.go
index de4ccaf..b74a982 100644
--- a/services/keepproxy/keepproxy.go
+++ b/services/keepproxy/keepproxy.go
@@ -222,6 +222,8 @@ type PutBlockHandler struct {
type InvalidPathHandler struct{}
+type OptionsHandler struct{}
+
// MakeRESTRouter
// Returns a mux.Router that passes GET and PUT requests to the
// appropriate handlers.
@@ -244,6 +246,7 @@ func MakeRESTRouter(
if enable_put {
rest.Handle(`/{hash:[0-9a-f]{32}}+{hints}`, PutBlockHandler{kc, t}).Methods("PUT")
rest.Handle(`/{hash:[0-9a-f]{32}}`, PutBlockHandler{kc, t}).Methods("PUT")
+ rest.Handle(`/{hash:[0-9a-f]{32}}{ignore}`, OptionsHandler{}).Methods("OPTIONS")
}
rest.NotFoundHandler = InvalidPathHandler{}
@@ -256,7 +259,17 @@ func (this InvalidPathHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req
http.Error(resp, "Bad request", http.StatusBadRequest)
}
+func (this OptionsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ log.Printf("%s: %s %s", GetRemoteAddress(req), req.Method, req.URL.Path)
+ resp.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, OPTIONS")
+ resp.Header().Set("Access-Control-Allow-Origin", "*")
+ resp.Header().Set("Access-Control-Allow-Headers", "Authorization, X-Keep-Desired-Replicas")
+ resp.Header().Set("Access-Control-Max-Age", "86486400")
+}
+
func (this GetBlockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ resp.Header().Set("Access-Control-Allow-Origin", "*")
+ resp.Header().Set("Access-Control-Allow-Headers", "Authorization")
kc := *this.KeepClient
diff --git a/services/keepproxy/keepproxy_test.go b/services/keepproxy/keepproxy_test.go
index 88ac8a6..ba9793d 100644
--- a/services/keepproxy/keepproxy_test.go
+++ b/services/keepproxy/keepproxy_test.go
@@ -222,7 +222,7 @@ func (s *ServerRequiredSuite) TestPutAskGet(c *C) {
}
func (s *ServerRequiredSuite) TestPutAskGetForbidden(c *C) {
- log.Print("TestPutAndGet start")
+ log.Print("TestPutAskGetForbidden start")
kc := runProxy(c, []string{"keepproxy"}, "123abc", 29951)
waitForListener()
@@ -260,7 +260,7 @@ func (s *ServerRequiredSuite) TestPutAskGetForbidden(c *C) {
log.Print("Get")
}
- log.Print("TestPutAndGetForbidden done")
+ log.Print("TestPutAskGetForbidden done")
}
func (s *ServerRequiredSuite) TestGetDisabled(c *C) {
@@ -320,3 +320,35 @@ func (s *ServerRequiredSuite) TestPutDisabled(c *C) {
log.Print("TestPutDisabled done")
}
+
+func (s *ServerRequiredSuite) TestCorsHeaders(c *C) {
+ runProxy(c, []string{"keepproxy"}, "4axaw8zxe0qm22wa6urpp5nskcne8z88cvbupv653y1njyi05h", 29954)
+ waitForListener()
+ defer closeListener()
+
+ {
+ client := http.Client{}
+ req, err := http.NewRequest("OPTIONS",
+ fmt.Sprintf("http://localhost:29954/%x+3",
+ md5.Sum([]byte("foo"))),
+ nil)
+ req.Header.Add("Access-Control-Request-Method", "PUT")
+ req.Header.Add("Access-Control-Request-Headers", "Authorization, X-Keep-Desired-Replicas")
+ resp, err := client.Do(req)
+ c.Check(err, Equals, nil)
+ c.Check(resp.StatusCode, Equals, 200)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(string(body), Equals, "")
+ c.Check(resp.Header.Get("Access-Control-Allow-Methods"), Equals, "GET, HEAD, PUT, OPTIONS")
+ c.Check(resp.Header.Get("Access-Control-Allow-Origin"), Equals, "*")
+ }
+
+ {
+ resp, err := http.Get(
+ fmt.Sprintf("http://localhost:29954/%x+3",
+ md5.Sum([]byte("foo"))))
+ c.Check(err, Equals, nil)
+ c.Check(resp.Header.Get("Access-Control-Allow-Headers"), Equals, "Authorization")
+ c.Check(resp.Header.Get("Access-Control-Allow-Origin"), Equals, "*")
+ }
+}
commit a5a99ae63eaae6b10b7718f6e685eecd99137495
Author: Tom Clegg <tom at curoverse.com>
Date: Sat Nov 22 04:49:53 2014 -0500
3781: Set CORS headers in API responses.
diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 4f0364f..eacd5f2 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -27,6 +27,7 @@ class ApplicationController < ActionController::Base
ERROR_ACTIONS = [:render_error, :render_not_found]
+ before_filter :set_cors_headers
before_filter :respond_with_json_by_default
before_filter :remote_ip
before_filter :load_read_auths
@@ -345,6 +346,13 @@ class ApplicationController < ActionController::Base
end
end
+ def set_cors_headers
+ response.headers['Access-Control-Allow-Origin'] = '*'
+ response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, PUT, POST, DELETE'
+ response.headers['Access-Control-Allow-Headers'] = 'Authorization'
+ response.headers['Access-Control-Max-Age'] = '86486400'
+ end
+
def respond_with_json_by_default
html_index = request.accepts.index(Mime::HTML)
if html_index.nil? or request.accepts[0...html_index].include?(Mime::JSON)
diff --git a/services/api/app/controllers/static_controller.rb b/services/api/app/controllers/static_controller.rb
index d624ea8..9c66f01 100644
--- a/services/api/app/controllers/static_controller.rb
+++ b/services/api/app/controllers/static_controller.rb
@@ -3,7 +3,7 @@ class StaticController < ApplicationController
skip_before_filter :find_object_by_uuid
skip_before_filter :render_404_if_no_object
- skip_before_filter :require_auth_scope, :only => [ :home, :login_failure ]
+ skip_before_filter :require_auth_scope, only: [:home, :empty, :login_failure]
def home
respond_to do |f|
@@ -20,4 +20,8 @@ class StaticController < ApplicationController
end
end
+ def empty
+ render text: "-"
+ end
+
end
diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index 3e79915..cdcb720 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -1,6 +1,7 @@
class UserSessionsController < ApplicationController
before_filter :require_auth_scope, :only => [ :destroy ]
+ skip_before_filter :set_cors_headers
skip_before_filter :find_object_by_uuid
skip_before_filter :render_404_if_no_object
diff --git a/services/api/config/routes.rb b/services/api/config/routes.rb
index 705822a..096a0a5 100644
--- a/services/api/config/routes.rb
+++ b/services/api/config/routes.rb
@@ -3,6 +3,9 @@ Server::Application.routes.draw do
# See http://guides.rubyonrails.org/routing.html
+ # OPTIONS requests just get an empty response with CORS headers.
+ match '*a', :to => 'static#empty', :via => 'OPTIONS'
+
namespace :arvados do
namespace :v1 do
resources :api_client_authorizations do
commit 106746eaa37489d6f4277ea49f04c897dcb8bd23
Author: Tom Clegg <tom at curoverse.com>
Date: Sat Nov 22 04:44:00 2014 -0500
3781: Remove js cruft from api server.
diff --git a/services/api/app/assets/javascripts/api_client_authorizations.js.coffee b/services/api/app/assets/javascripts/api_client_authorizations.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/api_client_authorizations.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/api_clients.js.coffee b/services/api/app/assets/javascripts/api_clients.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/api_clients.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/application.js b/services/api/app/assets/javascripts/application.js
deleted file mode 100644
index 37c7bfc..0000000
--- a/services/api/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-//= require jquery
-//= require jquery_ujs
-//= require_tree .
diff --git a/services/api/app/assets/javascripts/authorized_keys.js.coffee b/services/api/app/assets/javascripts/authorized_keys.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/authorized_keys.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/collections.js.coffee b/services/api/app/assets/javascripts/collections.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/collections.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/commit_ancestors.js.coffee b/services/api/app/assets/javascripts/commit_ancestors.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/commit_ancestors.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/commits.js.coffee b/services/api/app/assets/javascripts/commits.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/commits.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/groups.js.coffee b/services/api/app/assets/javascripts/groups.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/groups.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/humans.js.coffee b/services/api/app/assets/javascripts/humans.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/humans.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/job_tasks.js.coffee b/services/api/app/assets/javascripts/job_tasks.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/job_tasks.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/jobs.js.coffee b/services/api/app/assets/javascripts/jobs.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/jobs.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/keep_disks.js.coffee b/services/api/app/assets/javascripts/keep_disks.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/keep_disks.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/links.js.coffee b/services/api/app/assets/javascripts/links.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/links.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/logs.js.coffee b/services/api/app/assets/javascripts/logs.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/logs.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/nodes.js b/services/api/app/assets/javascripts/nodes.js
deleted file mode 100644
index a734426..0000000
--- a/services/api/app/assets/javascripts/nodes.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// -*- mode: javascript; js-indent-level: 4; indent-tabs-mode: nil; -*-
-// Place all the behaviors and hooks related to the matching controller here.
-// All this logic will automatically be available in application.js.
-
-var loaded_nodes_js;
-$(function(){
- if (loaded_nodes_js) return; loaded_nodes_js = true;
-
- $('[data-showhide-selector]').on('click', function(e){
- var x = $($(this).attr('data-showhide-selector'));
- if (x.css('display') == 'none')
- x.show();
- else
- x.hide();
- });
- $('[data-showhide-default]').hide();
-});
diff --git a/services/api/app/assets/javascripts/nodes.js.coffee b/services/api/app/assets/javascripts/nodes.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/nodes.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/pipeline_instances.js.coffee b/services/api/app/assets/javascripts/pipeline_instances.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/pipeline_instances.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/pipeline_templates.js.coffee b/services/api/app/assets/javascripts/pipeline_templates.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/pipeline_templates.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/repositories.js.coffee b/services/api/app/assets/javascripts/repositories.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/repositories.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/specimens.js.coffee b/services/api/app/assets/javascripts/specimens.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/specimens.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/traits.js.coffee b/services/api/app/assets/javascripts/traits.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/traits.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/services/api/app/assets/javascripts/virtual_machines.js.coffee b/services/api/app/assets/javascripts/virtual_machines.js.coffee
deleted file mode 100644
index 7615679..0000000
--- a/services/api/app/assets/javascripts/virtual_machines.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list