diff --git a/lib/roles/app.js b/lib/roles/app.js index a5bc10c4..d040ed1a 100644 --- a/lib/roles/app.js +++ b/lib/roles/app.js @@ -420,6 +420,10 @@ module.exports = function(options) { .on('tx.cleanup', function(channel) { leaveChannel(channel) }) + .on('tx.punch', function(channel) { + joinChannel(channel) + socket.emit('tx.punch', channel) + }) .on('shell.command', function(channel, responseChannel, data) { joinChannel(responseChannel) push.send([ @@ -459,11 +463,14 @@ module.exports = function(options) { .on('storage.upload', function(channel, responseChannel, data) { joinChannel(responseChannel) request.postAsync({ - url: util.format('%sapi/v1/resources', options.storageUrl) + url: util.format( + '%sapi/v1/resources?channel=%s' + , options.storageUrl + , responseChannel + ) , json: true , body: { url: data.url - , channel: responseChannel } }) .catch(function(err) { diff --git a/lib/roles/storage/temp.js b/lib/roles/storage/temp.js index dad0d08d..5bf36fc5 100644 --- a/lib/roles/storage/temp.js +++ b/lib/roles/storage/temp.js @@ -3,6 +3,7 @@ var util = require('util') var fs = require('fs') var express = require('express') +var validator = require('express-validator') var formidable = require('formidable') var Promise = require('bluebird') var ApkReader = require('adbkit-apkreader') @@ -34,6 +35,9 @@ module.exports = function(options) { app.set('case sensitive routing', true) app.set('trust proxy', true) + app.use(express.json()) + app.use(validator()) + storage.on('timeout', function(id) { log.info('Cleaning up inactive resource "%s"', id) }) @@ -125,105 +129,114 @@ module.exports = function(options) { } app.post('/api/v1/resources', function(req, res) { - function handle(fields, files) { - var seq = 0 + var seq = 0 - function sendProgress(data, progress) { - if (fields.channel) { - push.send([ - fields.channel - , wireutil.envelope(new wire.TransactionProgressMessage( - options.id - , seq++ - , data - , progress - )) - ]) - } - } - - function sendDone(success, data, body) { - if (fields.channel) { - push.send([ - fields.channel - , wireutil.envelope(new wire.TransactionDoneMessage( - options.id - , seq++ - , success - , data - , body ? JSON.stringify(body) : null - )) - ]) - } - } - - if (files.file) { - return processFile(files.file) - .progressed(function(progress) { - sendProgress('processing', 0.9 * progress.percent) - }) - .then(function(manifest) { - sendProgress('storing', 90) - return storeFile(files.file) - .then(function(data) { - data.manifest = manifest - sendDone(true, 'success', data) - return data - }) - }) - .catch(function(err) { - sendDone(false, err.reportCode || 'fail') - return Promise.reject(err) - }) - } - else if (fields.url) { - return download(fields.url) - .progressed(function(progress) { - sendProgress('uploading', 0.7 * progress.percent) - }) - .then(function(file) { - return processFile(file) - .progressed(function(progress) { - sendProgress('processing', 70 + 0.2 * progress.percent) - }) - .then(function(manifest) { - sendProgress('storing', 90) - return storeFile(file) - .then(function(data) { - data.manifest = manifest - sendDone(true, 'success', data) - return data - }) - }) - }) - .catch(function(err) { - sendDone(false, err.reportCode || 'fail') - return Promise.reject(err) - }) - } - else { - throw new requtil.ValidationError('"file" or "url" is required') + function sendProgress(data, progress) { + if (req.query.channel) { + push.send([ + req.query.channel + , wireutil.envelope(new wire.TransactionProgressMessage( + options.id + , seq++ + , data + , progress + )) + ]) } } - var form = Promise.promisifyAll(new formidable.IncomingForm()) - form.parseAsync(req) - .spread(handle) + function sendDone(success, data, body) { + if (req.query.channel) { + push.send([ + req.query.channel + , wireutil.envelope(new wire.TransactionDoneMessage( + options.id + , seq++ + , success + , data + , body ? JSON.stringify(body) : null + )) + ]) + } + } + + requtil.validate(req, function() { + req.checkQuery('channel').notEmpty() + }) + .then(function() { + if (req.is('application/json')) { + return requtil.validate(req, function() { + req.checkBody('url').notEmpty() + }) + .then(function() { + return download(req.body.url) + .progressed(function(progress) { + sendProgress('uploading', 0.7 * progress.percent) + }) + }) + } + else { + var form = Promise.promisifyAll(new formidable.IncomingForm()) + function progressListener(received, expected) { + if (expected) { + sendProgress('uploading', 70 * (received / expected)) + } + } + sendProgress('uploading', 0) + form.on('progress', progressListener) + return form.parseAsync(req) + .finally(function() { + form.removeListener('progress', progressListener) + }) + .spread(function(fields, files) { + if (!files.file) { + throw new requtil.ValidationError('validation error', [ + { + "param": "file" + , "msg": "Required value" + } + ]) + } + return files.file + }) + } + }) + .then(function(file) { + return processFile(file) + .progressed(function(progress) { + sendProgress('processing', 70 + 0.2 * progress.percent) + }) + .then(function(manifest) { + sendProgress('storing', 90) + return storeFile(file) + .then(function(data) { + data.manifest = manifest + return data + }) + }) + }) .then(function(data) { + sendDone(true, 'success', data) data.success = true res.json(201, data) }) - .catch(requtil.ValidationError, function() { - res.json(400, { - success: false - , error: 'ValidationError' - }) + .catch(requtil.ValidationError, function(err) { + sendDone(false, err.reportCode || 'fail_validation') + res.status(400) + .json({ + success: false + , error: 'ValidationError' + , validationErrors: err.errors + }) }) .catch(function(err) { - log.error('Failed to save resource: ', err.stack) - res.json(500, { - success: false - }) + log.error('Unexpected error', err.stack) + sendDone(false, err.reportCode || 'fail') + res.status(500) + .json({ + success: false + , error: 'ServerError' + }) }) }) diff --git a/res/app/components/stf/control/control-service.js b/res/app/components/stf/control/control-service.js index 10fe5a89..a7cfdf1f 100644 --- a/res/app/components/stf/control/control-service.js +++ b/res/app/components/stf/control/control-service.js @@ -120,37 +120,21 @@ module.exports = function ControlServiceFactory( } this.uploadFile = function(files) { - // Let's fake it till we can make it - var result = new TransactionService.TransactionResult({ + if (files.length !== 1) { + throw new Error('Can only upload one file') + } + var tx = TransactionService.create({ id: 'storage' }) - return { - promise: + TransactionService.punch(tx.channel) + .then(function() { $upload.upload({ - url: '/api/v1/resources' + url: '/api/v1/resources?channel=' + tx.channel , method: 'POST' , file: files[0] }) - .then( - function(response) { - result.settled = true - result.progress = 100 - result.success = true - result.lastData = 'success' - result.data.push(result.lastData) - result.body = response.data - return result - } - , function(err) { - result.settled = true - result.progress = 100 - result.success = false - result.error = result.lastData = 'fail' - result.data.push(result.lastData) - return result - } - ) - } + }) + return tx } this.install = function(options) { diff --git a/res/app/components/stf/control/transaction-service.js b/res/app/components/stf/control/transaction-service.js index ccc872c8..96197c25 100644 --- a/res/app/components/stf/control/transaction-service.js +++ b/res/app/components/stf/control/transaction-service.js @@ -188,6 +188,25 @@ module.exports = function TransactionServiceFactory(socket) { } } + transactionService.punch = function(channel) { + var resolver = Promise.defer() + + function punchListener(someChannel) { + if (channel === someChannel) { + resolver.resolve(channel) + } + } + + socket.on('tx.punch', punchListener) + socket.emit('tx.punch', channel) + + return resolver.promise + .timeout(5000) + .finally(function() { + socket.removeListener('tx.punch', punchListener) + }) + } + transactionService.TransactionResult = TransactionResult return transactionService diff --git a/res/app/control-panes/dashboard/upload/upload-controller.js b/res/app/control-panes/dashboard/upload/upload-controller.js index 5997a4bf..40a1d26b 100644 --- a/res/app/control-panes/dashboard/upload/upload-controller.js +++ b/res/app/control-panes/dashboard/upload/upload-controller.js @@ -43,8 +43,15 @@ module.exports = function UploadCtrl($scope, $rootScope, SettingsService, gettex var upload = $rootScope.control.uploadFile($files) $scope.installation = null return upload.promise + .progressed(function (uploadResult) { + $scope.$apply(function () { + $scope.upload = uploadResult + }) + }) .then(function (uploadResult) { - $scope.upload = uploadResult + $scope.$apply(function () { + $scope.upload = uploadResult + }) if (uploadResult.success) { return $scope.maybeInstall(uploadResult.body) }