Implement APK uploads using the new storage system. Installation from URL still does not work, and dropping the file on the screen may not work either.

This commit is contained in:
Simo Kinnunen
2014-05-22 13:33:38 +09:00
parent 1db48e9fcb
commit 41ed33f5c4
17 changed files with 389 additions and 336 deletions

View File

@@ -73,13 +73,21 @@ module.exports = function(options) {
, authUrl: options.authUrl
}))
// Proxied requests must come before any body parsers
// Proxied requests must come before any body parsers. These proxies are
// here mainly for convenience, they should be replaced with proper reverse
// proxies in production.
app.all('/api/v1/s/image/*', function(req, res) {
proxy.web(req, res, {
target: options.storagePluginImageUrl
})
})
app.all('/api/v1/s/apk/*', function(req, res) {
proxy.web(req, res, {
target: options.storagePluginApkUrl
})
})
app.all('/api/v1/s/*', function(req, res) {
proxy.web(req, res, {
target: options.storageUrl
@@ -507,7 +515,11 @@ module.exports = function(options) {
channel
, wireutil.transaction(
responseChannel
, new wire.InstallMessage(data)
, new wire.InstallMessage(
data.href
, data.launch === true
, JSON.stringify(data.manifest)
)
)
])
})

216
lib/roles/cache/apk.js vendored
View File

@@ -1,216 +0,0 @@
var http = require('http')
var util = require('util')
var fs = require('fs')
var express = require('express')
var validator = require('express-validator')
var Promise = require('bluebird')
var ApkReader = require('adbkit-apkreader')
var request = require('request')
var progress = require('request-progress')
var temp = require('temp')
var zmq = require('zmq')
var logger = require('../../util/logger')
var requtil = require('../../util/requtil')
var Storage = require('../../util/storage')
var wireutil = require('../../wire/util')
module.exports = function(options) {
var log = logger.createLogger('cache-apk')
, app = express()
, server = http.createServer(app)
, storage = new Storage()
// Output
var push = zmq.socket('push')
options.endpoints.push.forEach(function(endpoint) {
log.info('Sending output to %s', endpoint)
push.connect(endpoint)
})
app.set('strict routing', true)
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)
})
function processFile(file) {
var resolver = Promise.defer()
log.info('Processing file "%s"', file.path)
resolver.progress({
percent: 0
})
process.nextTick(function() {
try {
var reader = ApkReader.readFile(file.path)
var manifest = reader.readManifestSync()
resolver.resolve(manifest)
}
catch (err) {
err.reportCode = 'fail_invalid_app_file'
resolver.reject(err)
}
})
return resolver.promise
}
function storeFile(file) {
var id = storage.store(file)
return Promise.resolve({
id: id
, url: util.format(
'http://%s:%s/api/v1/resources/%s'
, options.publicIp
, options.port
, id
)
})
}
function download(url) {
var resolver = Promise.defer()
var path = temp.path({
dir: options.saveDir
})
log.info('Downloading "%s" to "%s"', url, path)
function errorListener(err) {
err.reportCode = 'fail_download'
resolver.reject(err)
}
function progressListener(state) {
resolver.progress(state)
}
function closeListener() {
resolver.resolve({
path: path
})
}
resolver.progress({
percent: 0
})
try {
var req = progress(request(url), {
throttle: 100 // Throttle events, not upload speed
})
.on('progress', progressListener)
var save = req.pipe(fs.createWriteStream(path))
.on('error', errorListener)
.on('close', closeListener)
}
catch (err) {
err.reportCode = 'fail_invalid_url'
resolver.reject(err)
}
return resolver.promise.finally(function() {
req.removeListener('progress', progressListener)
save.removeListener('error', errorListener)
save.removeListener('close', closeListener)
})
}
app.post('/api/v1/cache', function(req, res) {
var reply = wireutil.reply(options.id)
function sendProgress(data, progress) {
if (req.query.channel) {
push.send([
req.query.channel
, reply.progress(data, progress)
])
}
}
function sendDone(success, data, body) {
if (req.query.channel) {
push.send([
req.query.channel
, reply.okay(data, body)
])
}
}
requtil.validate(req, function() {
req.checkQuery('channel').notEmpty()
})
.then(function() {
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)
})
})
})
.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(err) {
sendDone(false, err.reportCode || 'fail_validation')
res.status(400)
.json({
success: false
, error: 'ValidationError'
, validationErrors: err.errors
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
sendDone(false, err.reportCode || 'fail')
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.get('/api/v1/cache/:id', function(req, res) {
var file = storage.retrieve(req.params.id)
if (file) {
res.set('Content-Type', file.type)
res.sendfile(file.path)
}
else {
res.send(404)
}
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -1,4 +1,6 @@
var stream = require('stream')
var url = require('url')
var util = require('util')
var syrup = require('syrup')
var request = require('request')
@@ -17,7 +19,7 @@ module.exports = syrup.serial()
var log = logger.createLogger('device:plugins:install')
router.on(wire.InstallMessage, function(channel, message) {
log.info('Installing "%s"', message.url)
log.info('Installing "%s"', message.href)
var reply = wireutil.reply(options.serial)
@@ -30,7 +32,7 @@ module.exports = syrup.serial()
function pushApp() {
var req = request({
url: message.url
url: url.resolve(options.storageUrl, message.href)
})
// We need to catch the Content-Length on the fly or we risk
@@ -104,16 +106,30 @@ module.exports = syrup.serial()
.timeout(30000)
})
.then(function() {
if (message.launchActivity) {
log.info(
'Launching activity with action "%s" on component "%s"'
, message.launchActivity.action
, message.launchActivity.component
)
// Progress 90%
sendProgress('launching_app', 90)
return adb.startActivity(options.serial, message.launchActivity)
.timeout(15000)
if (message.launch) {
var manifest = JSON.parse(message.manifest)
if (manifest.application.launcherActivities.length) {
var launchActivity = {
action: 'android.intent.action.MAIN'
, component: util.format(
'%s/%s'
, manifest.package
, manifest.application.launcherActivities[0].name
)
, category: ['android.intent.category.LAUNCHER']
, flags: 0x10200000
}
log.info(
'Launching activity with action "%s" on component "%s"'
, launchActivity.action
, launchActivity.component
)
// Progress 90%
sendProgress('launching_app', 90)
return adb.startActivity(options.serial, launchActivity)
.timeout(15000)
}
}
})
.then(function() {

View File

@@ -0,0 +1,54 @@
var http = require('http')
var url = require('url')
var express = require('express')
var httpProxy = require('http-proxy')
var logger = require('../../../../util/logger')
var download = require('../../../../util/download')
var manifest = require('./task/manifest')
module.exports = function(options) {
var log = logger.createLogger('storage:plugins:apk')
, app = express()
, server = http.createServer(app)
, proxy = httpProxy.createProxyServer()
proxy.on('error', function(err) {
log.error('Proxy had an error', err.stack)
})
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.set('trust proxy', true)
app.get('/api/v1/s/apk/:id/*/manifest', function(req, res) {
download(url.resolve(options.storageUrl, req.url), {
dir: options.cacheDir
})
.then(manifest)
.then(function(data) {
res.status(200)
.json({
success: true
, manifest: data
})
})
.catch(function(err) {
log.error('Unable to read manifest of "%s"', req.params.id, err.stack)
res.status(500)
.json({
success: false
})
})
})
app.get('/api/v1/s/apk/:id/*', function(req, res) {
proxy.web(req, res, {
target: options.storageUrl
})
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -0,0 +1,19 @@
var Promise = require('bluebird')
var ApkReader = require('adbkit-apkreader')
module.exports = function(file) {
var resolver = Promise.defer()
process.nextTick(function() {
try {
var reader = ApkReader.readFile(file.path)
var manifest = reader.readManifestSync()
resolver.resolve(manifest)
}
catch (err) {
resolver.reject(err)
}
})
return resolver.promise
}

View File

@@ -3,11 +3,14 @@ var util = require('util')
var path = require('path')
var express = require('express')
var validator = require('express-validator')
var formidable = require('formidable')
var Promise = require('bluebird')
var logger = require('../../util/logger')
var Storage = require('../../util/storage')
var requtil = require('../../util/requtil')
var download = require('../../util/download')
module.exports = function(options) {
var log = logger.createLogger('storage:temp')
@@ -19,19 +22,77 @@ 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)
})
app.post('/api/v1/s/:type/download', function(req, res) {
requtil.validate(req, function() {
req.checkBody('url').notEmpty()
})
.then(function() {
return download(req.body.url, {
dir: options.cacheDir
})
})
.then(function(file) {
return {
id: storage.store(file)
, name: file.name
}
})
.then(function(file) {
res.status(201)
.json({
success: true
, resource: {
date: new Date()
, type: req.params.type
, id: file.id
, name: file.name
, href: util.format(
'/api/v1/s/%s/%s%s'
, req.params.type
, file.id
, file.name
? util.format('/%s', path.basename(file.name))
: ''
)
}
})
})
.catch(requtil.ValidationError, function(err) {
res.status(400)
.json({
success: false
, error: 'ValidationError'
, validationErrors: err.errors
})
})
.catch(function(err) {
log.error('Error storing resource', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.post('/api/v1/s/:type', function(req, res) {
var form = new formidable.IncomingForm()
Promise.promisify(form.parse, form)(req)
.spread(function(fields, files) {
return Object.keys(files).map(function(field) {
var file = files[field]
log.info('Uploaded "%s" to "%s"', file.name, file.path)
return {
field: field
, id: storage.store(files[field])
, name: files[field].name
, id: storage.store(file)
, name: file.name
}
})
})