Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggable hosting #108

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function build(gyp, argv, callback) {
// turn back into command line options
Object.keys(opts).forEach(function(o) {
var val = opts[o];
if (val) {
if (val && typeof val !== 'object') {
command_line_args.push('--' + o + '=' + val);
}
})
Expand Down
47 changes: 6 additions & 41 deletions lib/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,16 @@ var fs = require('fs')
, path = require('path')
, zlib = require('zlib')
, log = require('npmlog')
, request = require('request')
, existsAsync = fs.exists || path.exists
, versioning = require('./util/versioning.js')
, compile = require('./util/compile.js')
, hosting = require('./util/hosting')
, testbinary = require('./testbinary.js')
, clean = require('./clean.js')

function download(uri,opts,callback) {
log.http('GET', uri)

var req = null
var requestOpts = {
uri: uri
, headers: {
'User-Agent': 'node-pre-gyp (node ' + process.version + ')'
}
}

var proxyUrl = opts.proxy
|| process.env.http_proxy
|| process.env.HTTP_PROXY
|| process.env.npm_config_proxy
if (proxyUrl) {
if (/^https?:\/\//i.test(proxyUrl)) {
log.verbose('download', 'using proxy url: "%s"', proxyUrl)
requestOpts.proxy = proxyUrl
} else {
log.warn('download', 'ignoring invalid "proxy" config setting: "%s"', proxyUrl)
}
}
try {
req = request(requestOpts)
} catch (e) {
return callback(e)
}
if (req) {
req.on('response', function (res) {
log.http(res.statusCode, uri)
})
}
return callback(null,req);
}

function place_binary(from,to,opts,callback) {
download(from,opts,function(err,req) {
function place_binary(to,opts,callback) {
hosting(opts).download(opts, function(err,req) {
if (err) return callback(err);
if (!req) return callback(new Error("empty req"));
var badDownload = false
Expand Down Expand Up @@ -135,7 +100,7 @@ function install(gyp, argv, callback) {
} catch (err) {
return callback(err);
}
var from = opts.hosted_tarball;

var to = opts.module_path;
var binary_module = path.join(to,opts.module_name + '.node');
if (existsAsync(binary_module,function(found) {
Expand All @@ -144,7 +109,7 @@ function install(gyp, argv, callback) {
if (err) {
console.error('['+package_json.name+'] ' + err.message);
log.error("Testing local pre-built binary failed, attempting to re-download");
place_binary(from,to,opts,function(err) {
place_binary(to,opts,function(err) {
if (err) {
if (should_do_fallback_build) {
log.http(err.message + ' (falling back to source compile with node-gyp)');
Expand All @@ -165,7 +130,7 @@ function install(gyp, argv, callback) {
});
} else {
log.info('check','checked for "' + binary_module + '" (not found)')
place_binary(from,to,opts,function(err) {
place_binary(to,opts,function(err) {
if (err && should_do_fallback_build) {
log.http(err.message + ' (falling back to source compile with node-gyp)');
return do_build(gyp,argv,callback);
Expand Down
43 changes: 5 additions & 38 deletions lib/publish.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,25 @@
var hosting = require('./util/hosting');

module.exports = exports = publish
module.exports = exports = publish;

exports.usage = 'Publishes pre-built binary (requires aws-sdk)'
exports.usage = 'Publishes pre-built binary (requires aws-sdk)';

var fs = require('fs')
, path = require('path')
, log = require('npmlog')
, versioning = require('./util/versioning.js')
, s3_setup = require('./util/s3_setup.js')
, mkdirp = require('mkdirp')
, existsAsync = fs.exists || path.exists
, url = require('url')
, config = require('rc')("node_pre_gyp",{acl:"public-read"});

function publish(gyp, argv, callback) {
var AWS = require("aws-sdk");
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
var tarball = opts.staged_tarball;
existsAsync(tarball,function(found) {
if (!found) {
return callback(new Error("Cannot publish because " + tarball + " missing: run `node-pre-gyp package` first"))
return callback(new Error("Cannot publish because " + tarball + " missing: run `node-pre-gyp package` first"));
}
s3_setup.detect(opts.hosted_path,config);
var key_name = url.resolve(config.prefix,opts.package_name)
AWS.config.update(config);
var s3 = new AWS.S3();
var s3_opts = { Bucket: config.bucket,
Key: key_name
};
s3.headObject(s3_opts, function(err, meta){
if (err && err.code == 'NotFound') {
// we are safe to publish because
// the object does not already exist
var s3 = new AWS.S3();
var s3_obj_opts = { ACL: config.acl,
Body: fs.createReadStream(tarball),
Bucket: config.bucket,
Key: key_name
};
s3.putObject(s3_obj_opts, function(err, resp){
if(err) return callback(err);
console.log('['+package_json.name+'] Success: published to https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key);
return callback();
});
} else if(err) {
return callback(err);
} else {
log.error('publish','Cannot publish over existing version');
log.error('publish',"Update the 'version' field in package.json and try again");
log.error('publish','If the previous version was published in error see:');
log.error('publish','\t node-pre-gyp unpublish');
return callback(new Error('Failed publishing to https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key));
}
});
hosting(opts).publish(opts, config, callback);
});
}
33 changes: 5 additions & 28 deletions lib/unpublish.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,18 @@
var hosting = require('./util/hosting');

module.exports = exports = unpublish
module.exports = exports = unpublish;

exports.usage = 'Unpublishes pre-built binary (requires aws-sdk)'
exports.usage = 'Unpublishes pre-built binary (requires aws-sdk)';

var fs = require('fs')
, path = require('path')
, log = require('npmlog')
, versioning = require('./util/versioning.js')
, s3_setup = require('./util/s3_setup.js')
, url = require('url')
, config = require('rc')("node_pre_gyp",{acl:"public-read"});

function unpublish(gyp, argv, callback) {
var AWS = require("aws-sdk");
var package_json = JSON.parse(fs.readFileSync('./package.json'));
var opts = versioning.evaluate(package_json, gyp.opts);
s3_setup.detect(opts.hosted_path,config);
AWS.config.update(config);
var key_name = url.resolve(config.prefix,opts.package_name)
var s3 = new AWS.S3();
var s3_opts = { Bucket: config.bucket,
Key: key_name
};
s3.headObject(s3_opts, function(err, meta) {
if (err && err.code == 'NotFound') {
console.log('['+package_json.name+'] Not found: https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key);
return callback();
} else if(err) {
return callback(err);
} else {
log.info(JSON.stringify(meta));
s3.deleteObject(s3_opts, function(err, resp) {
if (err) return callback(err);
log.info(JSON.stringify(resp));
console.log('['+package_json.name+'] Success: removed https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key);
return callback();
})
}
});

hosting(opts).unpublish(opts, config, callback);
}
126 changes: 126 additions & 0 deletions lib/util/hosting-s3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
var path = require('path');
var fs = require('fs');
var request = require('request');
var log = require('npmlog');
var url = require('url');
var s3_setup = require('../util/s3_setup.js');

/**
* Upload a tarball packaged by node-pre-gyp to AWS S3
*
* @param {Object} opts - A options object as return by node-pre-gyp's versioning.evaluate()
* @param {Object} config - A config object as built by node-pre-gyp using the rc configuration module
* @param {Function} callback - No particular return, just err or no err
*/
exports.publish = function(opts, config, callback) {
var AWS = require("aws-sdk");
s3_setup.detect(opts.hosted_path, config);
var key_name = url.resolve(config.prefix, opts.package_name);
AWS.config.update(config);
var s3 = new AWS.S3();
var s3_opts = {
Bucket: config.bucket,
Key: key_name
};
s3.headObject(s3_opts, function(err, meta) {
if (err && err.code == 'NotFound') {
// we are safe to publish because
// the object does not already exist
var s3 = new AWS.S3();
var s3_obj_opts = {
ACL: config.acl,
Body: fs.createReadStream(tarball),
Bucket: config.bucket,
Key: key_name
};
s3.putObject(s3_obj_opts, function(err, resp) {
if (err) return callback(err);
console.log('[' + package_json.name + '] Success: published to https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key);
return callback();
});
} else if (err) {
return callback(err);
} else {
log.error('publish', 'Cannot publish over existing version');
log.error('publish', "Update the 'version' field in package.json and try again");
log.error('publish', 'If the previous version was published in error see:');
log.error('publish', '\t node-pre-gyp unpublish');
return callback(new Error('Failed publishing to https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key));
}
});
};

/**
* Remove a tarball packaged by node-pre-gyp that was previously uploaded as an asset to a AWS S3
*
* @param {Object} opts - An options object as return by node-pre-gyp's versioning.evaluate()
* @param {Object} config - A config object as built by node-pre-gyp using the rc configuration module
* @param {Function} callback - No particular return, just err or no err
*/
exports.unpublish = function(opts, config, callback) {
var AWS = require("aws-sdk");
s3_setup.detect(opts.hosted_path, config);
AWS.config.update(config);
var key_name = url.resolve(config.prefix, opts.package_name);
var s3 = new AWS.S3();
var s3_opts = {
Bucket: config.bucket,
Key: key_name
};
s3.headObject(s3_opts, function(err, meta) {
if (err && err.code == 'NotFound') {
console.log('[' + package_json.name + '] Not found: https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key);
return callback();
} else if (err) {
return callback(err);
} else {
log.info(JSON.stringify(meta));
s3.deleteObject(s3_opts, function(err, resp) {
if (err) return callback(err);
log.info(JSON.stringify(resp));
console.log('[' + package_json.name + '] Success: removed https://' + s3_opts.Bucket + '.s3.amazonaws.com/' + s3_opts.Key);
return callback();
});
}
});
};

/**
* Download a tarball packaged by node-pre-gyp that was previously uploaded to AWS S3
*
* @param {Object} opts - An options object as return by node-pre-gyp's versioning.evaluate()
* @param {Function} callback - called with a request object (https://github.com/mikeal/request) that will be used as a stream by node-pre-gyp
*/
exports.download = function(opts, callback) {
var uri = opts.hosted_tarball;
log.http('GET', uri);

var req = null;
var requestOpts = {
uri: uri,
headers: {
'User-Agent': 'node-pre-gyp (node ' + process.version + ')'
}
};

var proxyUrl = opts.proxy || process.env.http_proxy || process.env.HTTP_PROXY || process.env.npm_config_proxy;
if (proxyUrl) {
if (/^https?:\/\//i.test(proxyUrl)) {
log.verbose('download', 'using proxy url: "%s"', proxyUrl);
requestOpts.proxy = proxyUrl;
} else {
log.warn('download', 'ignoring invalid "proxy" config setting: "%s"', proxyUrl);
}
}
try {
req = request(requestOpts);
} catch (e) {
return callback(e);
}
if (req) {
req.on('response', function(res) {
log.http(res.statusCode, uri);
});
}
return callback(null, req);
};
11 changes: 11 additions & 0 deletions lib/util/hosting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Fetch the right hosting implementation for given options
// first look for an implementation embedded into the project then in an outside module
module.exports = function(opts) {
var hosting;
try {
hosting = require('./hosting-' + opts.hosting.provider);
} catch (e) {
hosting = require('node-pre-gyp-hosting-' + opts.hosting.provider);
}
return hosting;
};
12 changes: 6 additions & 6 deletions lib/util/versioning.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ function get_node_abi(runtime, target) {

var required_parameters = [
'module_name',
'module_path',
'host'
'module_path'
];

function validate_config(package_json,callback) {
Expand All @@ -78,7 +77,7 @@ function validate_config(package_json,callback) {
if (missing.length >= 1) {
throw new Error(msg+"package.json must declare these properties: \n" + missing.join('\n'));
}
if (o) {
if (o && o.host) {
// enforce https over http
var protocol = url.parse(o.host).protocol;
if (protocol === 'http:') {
Expand Down Expand Up @@ -137,8 +136,9 @@ module.exports.evaluate = function(package_json,options) {
, arch: options.target_arch || process.arch
, target_arch: options.target_arch || process.arch
, module_main: package_json.main
, hosting: package_json.binary.hosting || {provider: 's3'}
}
opts.host = add_trailing_slash(eval_template(package_json.binary.host,opts));
opts.host = opts.host ? add_trailing_slash(eval_template(package_json.binary.host,opts)) : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why fallback to null here (and below)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because host is no longer a required option and add_trailing_slash fails if called on null or undefined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, makes sense. Can you share what an alternative hosting might look like so I can ponder whether the host related params might be moved around so things are more generic? I worry about having null params making this brittle.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see here for an example: https://github.com/albanm/node-addon-example/blob/master/package.json

I didn't move host around in order not to break compatibility, but following the logic of this PR it should probably be in a 'hosting' object in the 'binary' options. And the add_trailing_slash or other processing should be done in the specific hosting implementation.

opts.module_path = eval_template(package_json.binary.module_path,opts);
// now we resolve the module_path to ensure it is absolute so that binding.gyp variables work predictably
if (options.module_root) {
Expand All @@ -153,7 +153,7 @@ module.exports.evaluate = function(package_json,options) {
var package_name = package_json.binary.package_name ? package_json.binary.package_name : default_package_name;
opts.package_name = eval_template(package_name,opts);
opts.staged_tarball = path.join('build/stage',opts.remote_path,opts.package_name);
opts.hosted_path = url.resolve(opts.host,opts.remote_path);
opts.hosted_tarball = url.resolve(opts.hosted_path,opts.package_name);
opts.hosted_path = opts.host ? url.resolve(opts.host,opts.remote_path) : null;
opts.hosted_tarball = opts.host ? url.resolve(opts.hosted_path,opts.package_name) : null;
return opts;
}