A quick-and-dirty 'meteor publish' command.

It more or less works, but needs lots of cleanup.
This commit is contained in:
Emily Stark
2014-02-28 02:03:02 -08:00
parent d049bf7506
commit d50049777f
7 changed files with 317 additions and 7 deletions

View File

@@ -408,6 +408,84 @@ var fetchGalaxyOAuthInfo = function (galaxyName, timeout) {
}
};
// XXX De-dup with logInToGalaxy
// XXX make args options
var oauthFlow = function (conn, clientId, redirectUri,
domain, sessionType) {
var crypto = require('crypto');
var credentialToken = crypto.randomBytes(16).toString('hex');
var authCodeUrl = config.getOauthUrl() + "/authorize?" +
querystring.stringify({
state: credentialToken,
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri
});
// It's very important that we don't have request follow the
// redirect for us, but instead issue the second request ourselves,
// since request would pass our credentials along to the redirected
// URL. See comments in http-helpers.js.
try {
var codeResult = httpHelpers.request({
url: authCodeUrl,
method: 'POST',
strictSSL: true,
useAuthHeader: true
});
} catch (e) {
return { error: 'no-account-server' };
}
var response = codeResult.response;
if (response.statusCode !== 302 || ! response.headers.location) {
return { error: 'access-denied' };
}
if (url.parse(response.headers.location).hostname !==
url.parse(redirectUri).hostname) {
// If we didn't get an immediate redirect to the redirectUri then
// presumably the oauth server is trying to interact with us (make
// us log in, authorize the client, or something like that). We're
// not a web browser so we can't participate in such things.
return { error: 'access-denied' };
}
try {
var redirectResult = httpHelpers.request({
url: response.headers.location,
method: 'GET',
strictSSL: true
});
} catch (e) {
return { error: 'no-package-server' };
}
response = redirectResult.response;
// 'access-denied' isn't exactly right because it's possible that the server
// went down since our last request, but close enough.
if (response.statusCode !== 200) {
return { error: 'access-denied' };
}
// XXX tokenId???
var loginResult = conn.apply('login', [{
oauth: { credentialToken: credentialToken }
}], { wait: true });
if (loginResult.token && loginResult.id) {
var data = readSessionData();
var session = getSession(data, domain);
ensureSessionType(session, sessionType);
session.token = loginResult.token;
writeSessionData(data);
return 0;
} else {
process.stderr.write('Login failed');
return 1;
}
};
// Uses meteor accounts to log in to the specified galaxy. Returns an
// object with keys `token` and `tokenId` if the login was
// successful. If an error occurred, returns one of:
@@ -949,3 +1027,5 @@ exports.loggedInUsername = function () {
var data = readSessionData();
return loggedIn(data) ? currentUsername(data) : false;
};
exports.oauthFlow = oauthFlow;

View File

@@ -14,6 +14,9 @@ var config = require('./config.js');
var release = require('./release.js');
var Future = require('fibers/future');
var runLog = require('./run-log.js').runLog;
var packageClient = require('./package-client.js');
var utils = require('./utils.js');
var httpHelpers = require('./http-helpers.js');
// Given a site name passed on the command line (eg, 'mysite'), return
// a fully-qualified hostname ('mysite.meteor.com').
@@ -1294,6 +1297,100 @@ main.registerCommand({
});
});
///////////////////////////////////////////////////////////////////////////////
// publish a package
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'publish',
minArgs: 0,
maxArgs: 0,
options: {
versionString: { type: String, short: "v", required: true },
// XXX A temporary option to create the package, until we sync
// package metadata to the client.
create: { type: Boolean }
},
requiresPackage: true
}, function (options) {
var pkg = release.current.library.get(path.basename(
options.packageDir
));
var name = pkg.name;
var version = options.version;
var conn = packageClient.loggedInPackagesConnection();
if (! conn) {
process.stderr.write('Publish failed');
return 1;
}
// Create the package.
// XXX First sync package metadata and check if it exists.
if (options.create) {
process.stdout.write('Creating package...\n');
var packageId = conn.call('createPackage', {
name: pkg.name
});
}
process.stdout.write('Creating package version...\n');
var uploadInfo = conn.call('createPackageVersion', {
packageName: pkg.name,
version: options.versionString,
description: pkg.metadata.summary
});
process.stdout.write('Bundling source...\n');
var tempTarball = path.join(options.packageDir,
'source-' + utils.randomToken() + '.tgz');
files.createTarball(options.packageDir, tempTarball, {
ignoreDotFiles: true
});
var size = fs.statSync(tempTarball).size;
var crypto = require('crypto');
var hash = crypto.createHash('sha256');
hash.setEncoding('base64');
var rs = fs.createReadStream(tempTarball);
var fut = new Future();
rs.on('end', function () {
fut.return(hash.digest('base64'));
});
rs.pipe(hash, { end: false });
var tarballHash = fut.wait();
rs.close();
rs = fs.createReadStream(tempTarball);
process.stdout.write('Uploading source...\n');
httpHelpers.request({
method: 'PUT',
url: uploadInfo.uploadUrl,
headers: {
'content-length': size,
'content-type': 'application/octet-stream',
'x-amz-acl': 'public-read'
},
bodyStream: rs
});
// XXX Make sure this gets cleaned up even if we throw above
fs.unlinkSync(tempTarball);
// XXX Upload build tarball
process.stdout.write('Publishing package version...\n');
conn.call('publishPackageVersion', uploadInfo.uploadToken, tarballHash);
conn.close();
process.stdout.write('Published ' + pkg.name +
', version ' + options.versionString);
process.stdout.write('\nDone!\n');
return 0;
});
///////////////////////////////////////////////////////////////////////////////
// dummy

View File

@@ -127,6 +127,14 @@ _.extend(exports, {
return addScheme(host);
},
getPackageServerDomain: function () {
if (isLocalUniverse()) {
return localhostOffset(20);
} else {
return getUniverse();
}
},
// Return the domain name of the current Meteor Accounts server in
// use. This is used as a key for storing your Meteor Accounts
// login token.

View File

@@ -85,6 +85,18 @@ files.findAppDir = function (filepath) {
return findUpwards(isAppDir, filepath);
};
files.findPackageDir = function (filepath) {
var isPackageDir = function (filepath) {
try {
return fs.statSync(path.join(filepath, 'package.js')).isFile();
} catch (e) {
return false;
}
};
return findUpwards(isPackageDir, filepath);
};
// create a .gitignore file in dirPath if one doesn't exist. add
// 'entry' to the .gitignore on its own line at the bottom of the
// file, if the exact line does not already exist in the file.
@@ -404,17 +416,31 @@ files.extractTarGz = function (buffer, destPath) {
// Tar-gzips a directory, returning a stream that can then be piped as
// needed. The tar archive will contain a top-level directory named
// after dirPath.
files.createTarGzStream = function (dirPath) {
// options:
// - ignoreDotFiles: boolean
files.createTarGzStream = function (dirPath, options) {
var tar = require("tar");
var fstream = require('fstream');
var zlib = require("zlib");
return fstream.Reader({ path: dirPath, type: 'Directory' }).pipe(
tar.Pack()).pipe(zlib.createGzip());
var filter;
if (options.ignoreDotFiles) {
filter = function () {
return ! this.basename.match(/^\./);
};
}
var reader = fstream.Reader({
path: dirPath,
type: 'Directory',
filter: filter
});
return reader.pipe(tar.Pack()).pipe(zlib.createGzip());
};
// Tar-gzips a directory into a tarball on disk, synchronously.
// The tar archive will contain a top-level directory named after dirPath.
files.createTarball = function (dirPath, tarball) {
// options:
// - ignoreDotFiles: boolean
files.createTarball = function (dirPath, tarball, options) {
var future = new Future;
var out = fs.createWriteStream(tarball);
out.on('error', function (err) {
@@ -424,7 +450,7 @@ files.createTarball = function (dirPath, tarball) {
future.return();
});
files.createTarGzStream(dirPath).pipe(out);
files.createTarGzStream(dirPath, options).pipe(out);
future.wait();
};

View File

@@ -387,3 +387,7 @@ Grant a permission on an official service
Usage: meteor admin grant [XXX]
Not yet implemented
>>> publish
Publish a package
Usage: meteor publish --versionString <semver>

View File

@@ -515,13 +515,16 @@ Fiber(function () {
rawArgs.push(term);
}
// Figure out if we're running in a directory that is part of a
// Meteor application. Determine any additional directories to
// Figure out if we're running in a directory that is part of a Meteor
// application or package. Determine any additional directories to
// search for packages.
var appDir = files.findAppDir();
if (appDir)
appDir = path.resolve(appDir);
var packageDir = files.findPackageDir();
if (packageDir)
packageDir = path.resolve(packageDir);
var packageDirs = [];
if (appDir)
@@ -910,6 +913,22 @@ commandName + ": You're not in a Meteor project directory.\n" +
process.exit(1);
}
// Same check for commands that can only be run from a package dir.
var requiresPackage = command.requiresPackage;
if (typeof requiresPackage === "function") {
requiresPackage = requiresPackage(options);
}
if (packageDir) {
options.packageDir = packageDir;
}
if (requiresPackage && ! options.packageDir) {
process.stderr.write(
commandName + ": You're not in a Meteor package directory.\n");
process.exit(1);
}
if (command.requiresRelease && ! release.current) {
process.stderr.write(
"You must specify a Meteor version with --release when you work with this\n" +

76
tools/package-client.js Normal file
View File

@@ -0,0 +1,76 @@
var auth = require('./auth.js');
var config = require('./config.js');
var httpHelpers = require('./http-helpers.js');
var release = require('./release.js');
var Future = require('fibers/future');
var _ = require('underscore');
var getLoadedPackages = _.once(function () {
var unipackage = require('./unipackage.js');
return unipackage.load({
library: release.current.library,
packages: [ 'meteor', 'livedata', 'minimongo', 'mongo-livedata' ],
release: release.current.name
});
});
var openPackageServerConnection = function () {
var DDP = getLoadedPackages().livedata.DDP;
return DDP.connect(config.getPackageServerUrl(), {
headers: { 'User-Agent': httpHelpers.getUserAgent() }
});
};
// XXX onReconnect
exports.loggedInPackagesConnection = function () {
if (! auth.isLoggedIn()) {
auth.doUsernamePasswordLogin({ retry: true });
}
var conn = openPackageServerConnection();
var serviceConfigurations = new (getLoadedPackages()['meteor'].
Meteor.Collection)('meteor_accounts_loginServiceConfiguration', {
connection: conn
});
var fut = new Future();
var serviceConfigurationsSub = conn.subscribe(
'meteor.loginServiceConfiguration',
fut.resolver()
);
fut.wait();
var accountsConfiguration = serviceConfigurations.findOne({
service: 'meteor-developer'
});
if (! accountsConfiguration) {
return null;
}
var clientId = accountsConfiguration.clientId;
var loginResult;
if (! auth.getSessionToken(config.getPackageServerDomain())) {
// Since we passed retry: true, we shouldn't ever get to this point
// unless we are now logged in with the accounts server.
var redirectUri = config.getPackageServerUrl() +
'/_oauth/meteor-developer?close';
loginResult = auth.oauthFlow(conn, clientId, redirectUri,
config.getPackageServerDomain(),
'package-server');
if (! loginResult) {
conn.close();
return null;
}
} else {
loginResult = conn.apply('login', [{
resume: auth.getSessionToken(config.getPackageServerDomain())
}], { wait: true });
if (! loginResult || ! loginResult.token || ! loginResult.id) {
conn.close();
return null;
}
}
return conn;
};