diff --git a/lib/cli.js b/lib/cli.js index 32e92ea0..7488af5e 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -500,6 +500,32 @@ program }) }) +program + .command('storage-plugin-image') + .description('start storage image plugin') + .option('-p, --port ' + , 'port (or $PORT)' + , Number + , process.env.PORT || 7100) + .option('-r, --storage-url ' + , 'URL to storage client' + , String) + .option('--cache-dir ' + , 'where to cache images' + , String + , os.tmpdir()) + .action(function(options) { + if (!options.storageUrl) { + this.missingArgument('--storage-url') + } + + require('./roles/storage/plugins/image')({ + port: options.port + , storageUrl: options.storageUrl + , cacheDir: options.cacheDir + }) + }) + program .command('migrate') .description('migrates the database to the latest version') diff --git a/lib/roles/storage/plugins/image/index.js b/lib/roles/storage/plugins/image/index.js new file mode 100644 index 00000000..8f7304e6 --- /dev/null +++ b/lib/roles/storage/plugins/image/index.js @@ -0,0 +1,47 @@ +var http = require('http') +var util = require('util') + +var express = require('express') +var Promise = require('bluebird') +var gm = require('gm') + +var logger = require('../../../../util/logger') + +var parseCrop = require('./param/crop') +var parseGravity = require('./param/gravity') +var get = require('./task/get') +var transform = require('./task/transform') + +module.exports = function(options) { + var log = logger.createLogger('storage:plugins:image') + , app = express() + , server = http.createServer(app) + + app.set('strict routing', true) + app.set('case sensitive routing', true) + app.set('trust proxy', true) + + app.get('/api/v1/resources/:id/*', function(req, res) { + get(req.params.id, options) + .then(function(stream) { + return transform(stream, { + crop: parseCrop(req.query.crop) + , gravity: parseGravity(req.query.gravity) + }) + }) + .then(function(out) { + res.status(200) + out.pipe(res) + }) + .catch(function(err) { + log.error('Unable to transform resource "%s"', req.params.id, err.stack) + res.status(500) + .json({ + success: false + }) + }) + }) + + server.listen(options.port) + log.info('Listening on port %d', options.port) +} diff --git a/lib/roles/storage/plugins/image/param/crop.js b/lib/roles/storage/plugins/image/param/crop.js new file mode 100644 index 00000000..f719850f --- /dev/null +++ b/lib/roles/storage/plugins/image/param/crop.js @@ -0,0 +1,14 @@ +var RE_CROP = /^([0-9]*)x([0-9]*)$/ + +module.exports = function(raw) { + var parsed + + if (raw && (parsed = RE_CROP.exec(raw))) { + return { + width: +parsed[1] || 0 + , height: +parsed[2] || 0 + } + } + + return null +} diff --git a/lib/roles/storage/plugins/image/param/gravity.js b/lib/roles/storage/plugins/image/param/gravity.js new file mode 100644 index 00000000..bb6ecb6f --- /dev/null +++ b/lib/roles/storage/plugins/image/param/gravity.js @@ -0,0 +1,21 @@ +var GRAVITY = { + northwest: 'NorthWest' +, north: 'North' +, northeast: 'NorthEast' +, west: 'West' +, center: 'Center' +, east: 'East' +, southwest: 'SouthWest' +, south: 'South' +, southeast: 'SouthEast' +} + +module.exports = function(raw) { + var parsed + + if (raw && (parsed = GRAVITY[raw])) { + return parsed + } + + return null +} diff --git a/lib/roles/storage/plugins/image/task/get.js b/lib/roles/storage/plugins/image/task/get.js new file mode 100644 index 00000000..32214dec --- /dev/null +++ b/lib/roles/storage/plugins/image/task/get.js @@ -0,0 +1,19 @@ +var util = require('util') +var http = require('http') + +var Promise = require('bluebird') + +module.exports = function(id, options) { + return new Promise(function(resolve, reject) { + http.get(util.format('%sapi/v1/resources/%s', options.storageUrl, id)) + .on('response', function(res) { + if (res.statusCode !== 200) { + reject(new Error(util.format('HTTP %d', res.statusCode))) + } + else { + resolve(res) + } + }) + .on('error', reject) + }) +} diff --git a/lib/roles/storage/plugins/image/task/transform.js b/lib/roles/storage/plugins/image/task/transform.js new file mode 100644 index 00000000..b87ccbbe --- /dev/null +++ b/lib/roles/storage/plugins/image/task/transform.js @@ -0,0 +1,26 @@ +var gm = require('gm') +var Promise = require('bluebird') + +module.exports = function(stream, options) { + return new Promise(function(resolve, reject) { + var transform = gm(stream) + + if (options.gravity) { + transform.gravity(options.gravity) + } + + if (options.crop) { + transform.geometry(options.crop.width, options.crop.height, '^') + transform.crop(options.crop.width, options.crop.height, 0, 0) + } + + transform.stream(function(err, stdout) { + if (err) { + reject(err) + } + else { + resolve(stdout) + } + }) + }) +} diff --git a/lib/util/storage.js b/lib/util/storage.js index 3ea23ebf..ec8fd7d1 100644 --- a/lib/util/storage.js +++ b/lib/util/storage.js @@ -14,14 +14,18 @@ util.inherits(Storage, events.EventEmitter) Storage.prototype.store = function(file) { var id = uuid.v4() + this.set(id, file) + return id +} +Storage.prototype.set = function(id, file) { this.files[id] = { timeout: 600000 , lastActivity: Date.now() , data: file } - return id + return file } Storage.prototype.remove = function(id) {