diff --git a/lib/drivers/fs.js b/lib/drivers/fs.js new file mode 100644 index 000000000..f3d8bda65 --- /dev/null +++ b/lib/drivers/fs.js @@ -0,0 +1,55 @@ +var fs = require('fs'); +var Path = require('path'); + +function make_directories(dest, cb) { + var dir = Path.dirname(dest); + if (dir === '.' || dir === '..') return cb(); + fs.mkdir(dir, function(err) { + if (err && err.code === 'ENOENT') { + make_directories(dir, function() { + fs.mkdir(dir, cb); + }) + } else { + cb(); + } + }); +} + +function write_file(dest, data, cb) { + var safe_write = function(cb) { + fs.writeFile(dest, data, cb); + } + + safe_write(function(err) { + if (err && err.code === 'ENOENT') { + make_directories(dest, function() { + safe_write(cb); + }) + } else { + cb(err); + } + }); +} + +function create(name, contents, callback) { + fs.exists(name, function(exists) { + if (exists) return callback(new Error({code: 'EEXISTS'})); + write_file(name, contents, callback); + }); +} + +function update(name, contents, callback) { + fs.exists(name, function(exists) { + if (!exists) return callback(new Error({code: 'ENOENT'})); + write_file(name, contents, callback); + }); +} + +function read(name, callback) { + fs.readFile(name, callback); +} + +module.exports.read = read; +module.exports.create = create; +module.exports.update = update; + diff --git a/lib/drivers/memory.js b/lib/drivers/memory.js new file mode 100644 index 000000000..1d9bc9821 --- /dev/null +++ b/lib/drivers/memory.js @@ -0,0 +1,32 @@ +var store = {}; + +function create(name, contents, callback) { + if (store[name] != null) { + return callback(new Error({code: 'EEXISTS'})); + } + store[name] = contents; + callback(); +} + +function update(name, contents, callback) { + if (store[name] == null) { + return callback(new Error({code: 'ENOENT'})); + } + store[name] = contents; + callback(); +} + +function read(name, callback) { + if (store[name] == null) { + return callback(new Error({code: 'ENOENT'})); + } + callback(null, store[name]); +} + +module.exports.read_json = read; +module.exports.read = read; +module.exports.create_json = create; +module.exports.create = create; +module.exports.update_json = update; +module.exports.update = update; + diff --git a/lib/plugins/mongodb.js b/lib/drivers/mongodb.js similarity index 100% rename from lib/plugins/mongodb.js rename to lib/drivers/mongodb.js diff --git a/lib/error.js b/lib/error.js new file mode 100644 index 000000000..e62da0b26 --- /dev/null +++ b/lib/error.js @@ -0,0 +1,51 @@ +var util = require('util'); + +function parse_error_params(params, status, msg) { + if (typeof(params) === 'string') { + return { + msg: params, + status: status, + }; + } else if (typeof(params) === 'number') { + return { + msg: msg, + status: params, + }; + } else if (typeof(params) === 'object' && params != null) { + if (params.msg == null) params.msg = msg; + if (params.status == null) params.status = status; + return params; + } else { + return { + msg: msg, + status: status, + }; + } +} + +/* + * Errors caused by malfunctioned code + */ +var AppError = function(params, constr) { + Error.captureStackTrace(this, constr || this); + params = parse_error_params(params, 500, 'Internal server error'); + this.msg = params.msg; + this.status = params.status; +}; +util.inherits(AppError, Error); +AppError.prototype.name = 'Application Error'; + +/* + * Errors caused by wrong request + */ +var UserError = function(params, constr) { + params = parse_error_params(params, 404, 'The requested resource was not found'); + this.msg = params.msg; + this.status = params.status; +}; +util.inherits(UserError, Error); +UserError.prototype.name = 'User Error'; + +module.exports.AppError = AppError; +module.exports.UserError = UserError; + diff --git a/lib/index.js b/lib/index.js index e3db6ea31..d019fa559 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,18 +9,42 @@ function validate_name(req, res, next, value, name) { req.params.package = String(req.params.package); next(); } else { - res.status(403); - return res.send({ - error: 'invalid package name' - }); + next(new Error({ + status: 403, + msg: 'invalid package name', + })); } }; +function media(expect) { + return function(req, res, next) { + if (req.headers['content-type'] !== expect) { + next(new Error({ + status: 415, + msg: 'wrong content-type, we expect '+expect, + })); + } else { + next(); + } + } +} + +function expect_json(req, res, next) { + if (typeof(req.body) !== 'object') { + return next({ + status: 400, + msg: 'can\'t parse incoming json', + }); + } + next(); +} + module.exports = function(settings) { var app = express(); app.use(express.logger()); app.use(express.bodyParser()); app.param('package', validate_name); + app.param('filename', validate_name); /* app.get('/', function(req, res) { res.send({ @@ -28,16 +52,24 @@ module.exports = function(settings) { }); });*/ - app.get('/:package', function(req, res) { + app.get('/:package', function(req, res, next) { storage.get_package(req.params.package, function(err, info) { if (err) return next(err); - if (!info) { - res.status(404); - return res.send({ - error: 'package not found' + res.send(info); + }); + }); + + app.get('/:package/-/:filename', function(req, res, next) { + storage.get_tarball(req.params.package, req.params.filename, function(err, stream) { + if (err) return next(err); + if (!stream) { + return next({ + status: 404, + msg: 'package not found' }); } - res.send(info); + res.header('content-type', 'application/octet-stream'); + res.send(stream); }); }); @@ -56,77 +88,67 @@ module.exports = function(settings) { }); // publishing a package - app.put('/:package', function(req, res, next) { + app.put('/:package', media('application/json'), expect_json, function(req, res, next) { var name = req.params.package; - if (req.headers['content-type'] !== 'application/json') { - res.status(415); - return res.send({ - error: 'wrong content-type, we expect application/json', - }); - } - if (typeof(req.body) !== 'object') { - res.status(400); - return res.send({ - error: 'can\'t parse incoming json', + try { + var metadata = utils.validate_metadata(req.body, name); + } catch(err) { + return next({ + status: 422, + msg: 'bad incoming package data', }); } - storage.create_package(name, req.body, function(err, created) { + storage.add_package(name, metadata, function(err) { if (err) return next(err); - if (created) { - res.status(201); - return res.send({ - ok: 'created new package' - }); - } else { - res.status(409); - return res.send({ - error: 'package already exists' - }); - } + res.status(201); + return res.send({ + ok: 'created new package' + }); }); }); // uploading package tarball - app.put('/:package/-/:filename/*', function(req, res, next) { - res.status(201); - return res.send({ - ok: 'tarball uploaded successfully' + app.put('/:package/-/:filename/*', media('application/octet-stream'), function(req, res, next) { + var name = req.params.package; + + storage.add_tarball(name, req.params.filename, req, function(err) { + if (err) return next(err); + res.status(201); + return res.send({ + ok: 'tarball uploaded successfully' + }); }); }); // adding a version - app.put('/:package/:version/-tag/:tag', function(req, res, next) { + app.put('/:package/:version/-tag/:tag', media('application/json'), expect_json, function(req, res, next) { var name = req.params.package; - if (req.headers['content-type'] !== 'application/json') { - res.status(415); - return res.send({ - error: 'wrong content-type, we expect application/json', - }); - } - if (typeof(req.body) !== 'object') { - res.status(400); - return res.send({ - error: 'can\'t parse incoming json', - }); - } + var version = req.params.version; + var tag = req.params.tag; - storage.add_version(req.params.package, req.params.version, req.body, function(err, created) { + storage.add_version(name, version, req.body, tag, function(err) { if (err) return next(err); - if (created) { - res.status(201); - return res.send({ - ok: 'package published' - }); - } else { - res.status(409); - return res.send({ - error: 'this version already exists' - }); - } + res.status(201); + return res.send({ + ok: 'package published' + }); }); }); + app.use(app.router); + app.use(function(err, req, res, next) { + if (err.status && err.msg && err.status >= 400 && err.status < 600) { + res.status(err.status); + res.send({error: err.msg}); + } else { + console.log(err); + console.log(err.stack); + res.status(500); + res.send({error: 'internal server error'}); + } + }); + return app; }; diff --git a/lib/plugins/fs.js b/lib/plugins/fs.js deleted file mode 100644 index 88f44e580..000000000 --- a/lib/plugins/fs.js +++ /dev/null @@ -1,5 +0,0 @@ - -module.exports.add_package = function(name, version, tarball) { - -} - diff --git a/lib/storage.js b/lib/storage.js index 7c61cde7a..0b1cdb5e3 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -1,27 +1,100 @@ -var packages = {}; +var storage = wrap(require('./drivers/fs')); +var UError = require('./error').UserError; +var info_file = '.package.json'; -module.exports.create_package = function(name, metadata, callback) { - if (packages[name] == null) { - packages[name] = { - meta: metadata, - versions: {}, +function wrap(driver) { + if (typeof(driver.create_json) !== 'function') { + driver.create_json = function(name, value, cb) { + driver.create(name, JSON.stringify(value), cb); }; - callback(null, true); - } else { - callback(null, false); } + if (typeof(driver.update_json) !== 'function') { + driver.update_json = function(name, value, cb) { + driver.update(name, JSON.stringify(value), cb); + }; + } + if (typeof(driver.read_json) !== 'function') { + driver.read_json = function(name, cb) { + driver.read(name, function(err, res) { + if (err) return cb(err); + cb(null, JSON.parse(res)); + }); + }; + } + return driver; } -module.exports.add_version = function(name, version, metadata, callback) { - if (packages[name] == null) { - callback(null, false); - } else { - packages[name].versions[version] = metadata; - callback(null, true); - } +module.exports.add_package = function(name, metadata, callback) { + storage.create_json(name + '/' + info_file, metadata, function(err) { + if (err && err.code === 'EEXISTS') { + return callback(new UError({ + status: 409, + msg: 'this package is already present' + })); + } + callback(); + }); +} + +module.exports.add_version = function(name, version, metadata, tag, callback) { + storage.read_json(name + '/' + info_file, function(err, data) { + // TODO: race condition + if (err) return callback(err); + + if (data.versions[version] != null) { + return callback(new UError({ + status: 409, + msg: 'this version already present' + })); + } + data.versions[version] = metadata; + data['dist-tags'][tag] = version; + storage.update_json(name + '/' + info_file, data, callback); + }); +} + +module.exports.add_tarball = function(name, filename, stream, callback) { + var data = new Buffer(0); + stream.on('data', function(d) { + var tmp = data; + data = new Buffer(tmp.length+d.length); + tmp.copy(data, 0); + d.copy(data, tmp.length); + }); + stream.on('end', function(d) { + storage.create(name + '/' + filename, data, function(err) { + if (err && err.code === 'EEXISTS') { + return callback(new UError({ + status: 409, + msg: 'this tarball is already present' + })); + } + callback.apply(null, arguments); + }); + }); +} + +module.exports.get_tarball = function(name, filename, callback) { + storage.read(name + '/' + filename, function(err) { + if (err && err.code === 'ENOENT') { + return callback(new UError({ + status: 404, + msg: 'no such package available' + })); + } + callback.apply(null, arguments); + }); } module.exports.get_package = function(name, callback) { - callback(null, packages[name]); + storage.read_json(name + '/' + info_file, function(err) { + if (err && err.code === 'ENOENT') { + return callback(new UError({ + status: 404, + msg: 'no such package available' + })); + } + callback.apply(null, arguments); + }); } diff --git a/lib/utils.js b/lib/utils.js index 59aea551d..8f5c6f4a0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,4 @@ +var assert = require('assert'); // from normalize-package-data/lib/fixer.js module.exports.validate_name = function(name) { @@ -14,3 +15,23 @@ module.exports.validate_name = function(name) { } } +function is_object(obj) { + return typeof(obj) === 'object' && !Array.isArray(obj); +} + +module.exports.validate_metadata = function(object, name) { + assert(is_object(object)); + assert.equal(object._id, name); + assert.equal(object.name, name); + + if (!is_object(object['dist-tags'])) { + object['dist-tags'] = {}; + } + + if (!is_object(object['versions'])) { + object['versions'] = {}; + } + + return object; +} +