1 var app = angular.module('Workbench', ['Arvados']);
2 app.controller('UploadToCollection', UploadToCollection);
3 app.directive('arvUuid', arvUuid);
6 // Copy the given uuid into the current $scope.
9 link: function(scope, element, attributes) {
10 scope.uuid = attributes.arvUuid;
15 UploadToCollection.$inject = ['$scope', '$filter', '$q', '$timeout',
16 'ArvadosClient', 'arvadosApiToken'];
17 function UploadToCollection($scope, $filter, $q, $timeout,
18 ArvadosClient, arvadosApiToken) {
21 uploader: new QueueUploader(),
22 addFilesToQueue: function(files) {
23 // Angular binding doesn't work its usual magic for file
24 // inputs, so we need to $scope.$apply() this update.
25 $scope.$apply(function(){
28 for (insertAt=0; (insertAt<$scope.uploadQueue.length &&
29 $scope.uploadQueue[insertAt].state != 'Done');
31 for (i=0; i<files.length; i++) {
32 $scope.uploadQueue.splice(insertAt+i, 0,
33 new FileUploader(files[i]));
41 $scope.uploader.stop();
43 removeFileFromQueue: function(index) {
44 var wasRunning = $scope.uploader.running;
45 $scope.uploadQueue[index].stop();
46 $scope.uploadQueue.splice(index, 1);
50 countDone: function() {
52 for (var i=0; i<$scope.uploadQueue.length; i++) {
53 if ($scope.uploadQueue[i].state == 'Done') {
60 // TODO: watch uploadQueue, abort uploads if entries disappear
64 function SliceReader(_slice) {
69 ////////////////////////////////
73 // Return a promise, which will be resolved with the
74 // requested slice data.
75 _deferred = $.Deferred();
76 _reader = new FileReader();
77 _reader.onload = resolve;
78 _reader.onerror = _deferred.reject;
79 _reader.onprogress = _deferred.notify;
80 _reader.readAsArrayBuffer(_slice.blob);
81 return _deferred.promise();
84 if (that._reader.result.length != that._slice.size) {
85 // Sometimes we get an onload event even if the read
86 // did not return the desired number of bytes. We
87 // treat that as a fail.
90 "Short read: wanted " + _slice.size +
91 ", received " + _reader.result.length);
94 return _deferred.resolve(_reader.result);
98 function SliceUploader(_label, _data, _dataSize) {
103 ////////////////////////////////
110 // Send data to the Keep proxy. Retry a few times on
111 // fail. Return a promise that will get resolved with
112 // resolve(locator) when the block is accepted by the
114 _deferred = $.Deferred();
116 return _deferred.promise();
122 textStatus: 'stopped',
123 err: 'interrupted at slice '+_label
132 'Authorization': 'OAuth2 '+arvadosApiToken,
133 'Content-Type': 'application/octet-stream',
134 'X-Keep-Desired-Replicas': '2'
137 // Make an xhr that reports upload progress
138 var xhr = $.ajaxSettings.xhr();
140 xhr.upload.onprogress = onSendProgress;
147 _jqxhr.then(onSendResolve, onSendReject);
149 function onSendProgress(xhrProgressEvent) {
150 _deferred.notify(xhrProgressEvent.loaded, _dataSize);
152 function onSendResolve(data, textStatus, jqxhr) {
153 _deferred.resolve(data, _dataSize);
155 function onSendReject(xhr, textStatus, err) {
156 if (++_failCount < _failMax) {
157 // TODO: nice to tell the user that retry is happening.
158 console.log('slice ' + _label + ': ' +
159 textStatus + ', retry ' + _failCount);
163 {xhr: xhr, textStatus: textStatus, err: err});
166 function proxyUriBase() {
167 return ((keepProxy.service_ssl_flag ? 'https' : 'http') +
168 '://' + keepProxy.service_host + ':' +
169 keepProxy.service_port + '/');
173 function FileUploader(file) {
179 state: 'Queued', // Queued, Uploading, Paused, Done
182 stop: stop // User wants to stop.
184 ////////////////////////////////
186 var _currentUploader;
189 var _maxBlobSize = Math.pow(2,26);
191 var _queueTime = Date.now();
195 var _readPos = 0; // number of bytes confirmed uploaded
198 _deferred.reject({textStatus: 'restarted'});
199 _deferred = $q.defer();
200 that.state = 'Uploading';
201 _startTime = Date.now();
202 _startByte = _readPos;
205 return _deferred.promise;
209 that.state = 'Paused';
210 _deferred.reject({textStatus: 'stopped', err: 'interrupted'});
212 if (_currentUploader) {
213 _currentUploader.stop();
214 _currentUploader = null;
218 // Ensure this._deferred gets resolved or rejected --
219 // either right here, or when a new promise arranged right
220 // here is fulfilled.
221 _currentSlice = nextSlice();
222 if (!_currentSlice) {
224 setProgress(_readPos);
225 _currentUploader = null;
226 _deferred.resolve([that]);
229 _currentUploader = new SliceUploader(
233 _currentUploader.go().then(
238 function onUploaderResolve(locator, dataSize) {
239 if (!locator || _currentSlice.size != dataSize) {
240 console.log("onUploaderResolve but locator=" + locator +
241 " and " + _currentSlice.size + " != " + dataSize);
242 return onUploaderReject({
244 err: "Bad response from slice upload"
247 that.locators.push(locator);
248 _readPos += dataSize;
249 _currentUploader = null;
252 function onUploaderReject(reason) {
253 that.state = 'Paused';
254 setProgress(_readPos);
255 _currentUploader = null;
256 _deferred.reject(reason);
258 function onUploaderProgress(sliceDone, sliceSize) {
259 setProgress(_readPos + sliceDone);
261 function nextSlice() {
264 that.file.size - _readPos);
265 setProgress(_readPos);
269 var blob = that.file.slice(
270 _readPos, _readPos+size,
271 'application/octet-stream; charset=x-user-defined');
272 return {blob: blob, size: size};
274 function setProgress(bytesDone) {
276 that.progress = Math.min(100, 100 * bytesDone / that.file.size)
277 if (bytesDone > _startByte) {
278 kBps = (bytesDone - _startByte) /
279 (Date.now() - _startTime);
281 '' + $filter('number')(bytesDone/1024, '0') + 'K ' +
282 'at ~' + $filter('number')(kBps, '0') + 'K/s')
283 if (that.state == 'Paused') {
284 that.statistics += ', paused';
285 } else if (that.state == 'Uploading') {
286 that.statistics += ', ETA ' +
289 Date.now() + (that.file.size - bytesDone) / kBps),
292 that.statistics += ', finished ' +
293 $filter('date')(Date.now(), 'shortTime');
294 _finishTime = Date.now();
297 that.statistics = that.state;
303 function QueueUploader() {
311 ////////////////////////////////
315 if (that.state == 'Running') return _deferred.promise;
316 _deferred = $.Deferred();
317 that.state = 'Running';
318 ArvadosClient.apiPromise(
319 'keep_services', 'list',
320 {filters: [['service_type','=','proxy']]}).
321 then(doQueueWithProxy);
323 return _deferred.promise();
326 for (var i=0; i<$scope.uploadQueue.length; i++)
327 $scope.uploadQueue[i].stop();
329 function doQueueWithProxy(data) {
330 keepProxy = data.items[0];
332 that.state = 'Failed';
334 'There seems to be no Keep proxy service available.';
335 _deferred.reject(null, 'error', that.stateReason);
338 return doQueueWork();
340 function doQueueWork() {
342 that.state = 'Running';
343 that.stateReason = null;
344 // Push the done things to the bottom of the queue.
345 for (i=0; (i<$scope.uploadQueue.length &&
346 $scope.uploadQueue[i].state == 'Done'); i++);
348 $scope.uploadQueue.push.apply($scope.uploadQueue, $scope.uploadQueue.splice(0, i));
349 // If anything is not-done, do it.
350 if ($scope.uploadQueue.length > 0 &&
351 $scope.uploadQueue[0].state != 'Done') {
352 return $scope.uploadQueue[0].go().
353 then(appendToCollection, null, onQueueProgress).
354 then(doQueueWork, onQueueReject);
356 // If everything is done, resolve the promise and clean up.
357 return onQueueResolve();
359 function onQueueReject(reason) {
360 that.state = 'Failed';
362 (reason.textStatus || 'Error') +
363 (reason.xhr && reason.xhr.options
364 ? (' (from ' + reason.xhr.options.url + ')')
368 if (reason.xhr && reason.xhr.responseText)
369 that.stateReason += ' -- ' + reason.xhr.responseText;
370 _deferred.reject(reason);
373 function onQueueResolve() {
375 that.stateReason = 'Done!';
379 function onQueueProgress() {
380 // Ensure updates happen after FileUpload promise callbacks.
381 $timeout(function(){$scope.$apply();});
383 function appendToCollection(uploads) {
384 var deferred = $q.defer();
385 return ArvadosClient.apiPromise(
386 'collections', 'get',
387 { uuid: $scope.uuid }).
388 then(function(collection) {
389 var manifestText = '';
391 for (i=0; i<uploads.length; i++) {
393 filename = ArvadosClient.uniqueNameForManifest(
394 collection.manifest_text,
395 '.', upload.file.name);
396 collection.manifest_text += '. ' +
397 upload.locators.join(' ') +
398 ' 0:' + upload.file.size.toString() + ':' +
402 return ArvadosClient.apiPromise(
403 'collections', 'update',
407 collection.manifest_text }
409 then(deferred.resolve);
410 }, onQueueReject).then(function() {
412 for(i=0; i<uploads.length; i++) {
413 uploads[i].committed = true;
416 return deferred.promise.then(doQueueWork);