webstore-upload
Version:
Automatically upload new versions of Chrome Extensions or Apps to the Chrome Webstore - integrated in your node project!
416 lines (370 loc) • 17.4 kB
JavaScript
var Q = require('q'),
https = require('https'),
path = require('path'),
url = require('url'),
fs = require('fs'),
http = require('http'),
util = require('util'),
open = require('open'),
_ = require('lodash'),
glob = require('glob'),
rp = require('request-promise'),
request = require('request');
module.exports = function webstore_upload(uploadOptions, loggerFn) {
var logger;
var errs = {
token: {
failedRefresh: 'WU_FAILED_TO_REFRESH_TOKEN',
failedGet: 'WU_FAILED_TO_GET_ACCOUNT_TOKEN',
failedRequest: 'WU_FAILED_TO_REQUEST_TOKEN'
},
upload: {
failed: 'WU_FAILED_TO_UPLOAD_EXTENSION'
},
publish: {
failed: 'WU_FAILED_TO_PUBLISH_EXTENSION'
}
};
if (!loggerFn || loggerFn === 'quiet') {
logger = require('./quietLogger.js');
logger('info', 'Loading quiet logger');
} else if (loggerFn === 'default') {
logger = require('./defaultLogger.js');
logger('info', 'Loading default logger');
} else if (typeof loggerFn === 'function') {
logger = loggerFn;
logger('info', 'Logger function was taken from arguments');
} else if (typeof loggerFn !== 'function') {
//logger('error', 'logger must be a function, "default" or "quiet"');
throw new Error('Logger must be a function, "default" or "quiet"');
}
var extensions = uploadOptions.extensions;
var accounts = uploadOptions.accounts;
var tasks = uploadOptions.uploadExtensions || [];
var extensionsToUpload = tasks.length ? _.pick(extensions, tasks) : extensions;
//calculate tasks for accounts that we want to use
var accountsTasksToUse = _.uniq( _.map( extensionsToUpload, function (extension) {
var name = (extension.account || 'default');
var tokenFn = (accounts[name].refresh_token !== undefined) ? refresh_account_token : get_account_token;
return tokenFn(name);
}) );
logger('log', 'Refreshing tokens for all accounts');
return Q.all(accountsTasksToUse)
.then(function(values){
var promises = [];
for (var extension in extensionsToUpload) {
promises.push(uploadExtension(extension));
}
logger('log', 'Uploading all extensions');
return Q.all(promises);
})
.then(function (result) {
return result;
})
.catch(function (err) {
logger('err', err);
return Q.reject(err);
});
// Get token for account
function get_account_token(accountName){
//prepare account for inner function
var account = accounts[ accountName ];
account['name'] = accountName;
var getTokenFn = account['cli_auth'] ? getTokenForAccountCli : getTokenForAccount;
logger('log', 'Getting token for account ' + accountName);
return getTokenFn(account)
.then(function (token) {
//set token for provided account
logger('info', 'New token for account ' + accountName + ': ' + token);
accounts[accountName].token = token;
return token;
})
.catch(function (err) {
var responseError = {name: accountName, nameType: 'account', appError: errs.token.failedGet, more: {error: error}};
logger('err', 'Failed to get new token for account: ' + JSON.stringify(responseError));
return Q.reject(err);
});
}
// Refresh token for account
function refresh_account_token(accountName){
//prepare account for inner function
var account = accounts[ accountName ];
account['name'] = accountName;
logger('log', 'Refreshing access token for account ' + accountName);
var post_data = util.format('client_id=%s' +
'&client_secret=%s' +
'&refresh_token=%s' +
'&grant_type=refresh_token',
account.client_id,
account.client_secret,
account.refresh_token);
return rp({
uri : 'https://accounts.google.com/o/oauth2/token',
method : 'POST',
headers : {
'Content-Type' : 'application/x-www-form-urlencoded'
},
body : post_data,
json : true
})
.then(function (body) {
logger('log', 'Refreshed access token for account ' + accountName);
accounts[accountName].token = body.access_token;
return Q(body.access_token);
})
.catch(function (err) {
var responseError = {name: accountName, nameType: 'account', appError: errs.token.failedRefresh, more: {statusCode: err.statusCode, error: err.error}};
logger('err', 'Failed to refresh token for account ' + accountName + '\n' + JSON.stringify(responseError));
return Q.reject(JSON.stringify(responseError));
});
}
// uploading with token
function uploadExtension(extensionName){
var promises = [];
var uploadExtensions = {};
if (extensionName) {
uploadExtensions[extensionName] = extensions[extensionName];
} else {
uploadExtensions = extensionsToUpload;
}
_.map(uploadExtensions, function (extension, extensionName) {
var uploadConfig = Object.create(extension);
var accountName = extension.account || 'default';
uploadConfig['name'] = extensionName;
uploadConfig['account'] = accounts[accountName];
var p = handleUpload(uploadConfig);
promises.push(p);
});
logger('log', 'Starting extensions upload');
return Q.allSettled(promises).then(function (results) {
var isError = false;
var values = [];
results.forEach(function (result) {
if (result.state === 'fulfilled') {
values.push( result.value );
} else {
isError = result.reason;
}
});
if ( !isError ) {
return values.length === 1 ? values[0] : values;
//return values;
} else {
var responseError = {name: extensionName, nameType: 'extension', appError: errs.upload.failed, more: {error: JSON.parse(isError)}};
logger('err', '================\n');
logger('err', 'Error while uploading: ', JSON.stringify(responseError), '\n');
return Q.reject(JSON.stringify(responseError));
}
});
}
//upload zip
function handleUpload(options){
var filePath, zip;
var doPublish = false;
if( typeof options.publish !== 'undefined' ){
doPublish = options.publish;
}else if( typeof options.account.publish !== 'undefined' ){
doPublish = options.account.publish;
}
//updating existing
logger('log', '================');
logger('log', ' ');
logger('log', 'Updating app ('+ options.name +'): ', options.appID);
logger('log', ' ');
zip = options.zip;
if( fs.statSync( zip ).isDirectory() ){
zip = getRecentFile( zip );
}
filePath = path.resolve(zip);
return Q.Promise(function(resolve, reject) {
logger('log', 'Sending the upload request to google api');
fs.createReadStream(filePath).pipe(request.put({
uri : 'https://www.googleapis.com/upload/chromewebstore/v1.1/items/' + options.appID,
headers : {
'Authorization' : 'Bearer ' + options.account.token,
'x-goog-api-version' : '2'
},
json : true
}))
.on('response', function(response) {
response.on('data', function(data) {
var dataString = data.toString('utf8');
var dataJson = JSON.parse(dataString);
if (dataJson.uploadState !== 'SUCCESS') {
return reject(dataString);
}
logger('log', '\nUploading done (' + options.name + ')', '\n');
var responseJson = {};
responseJson[options.name] = {
fileName : zip,
extensionName : options.name,
extensionId : options.appID,
published : doPublish
};
if (doPublish) {
return publishItem(options)
.then(function (result) {
return resolve(responseJson);
})
.catch(function (err) {
return reject(err);
});
} else {
return resolve(responseJson);
}
});
response.on('error', function(err) {
logger('err', err);
return reject(err);
});
})
.on('error', function(err) {
logger('err', err);
return reject(err);
});
});
}
//make item published
function publishItem(options){
return Q.Promise(function(resolve, reject) {
logger('log', 'Publishing ('+ options.name +') ' + options.appID + '..');
var url = util.format('/chromewebstore/v1.1/items/%s/publish%s', options.appID,
options.publishTarget ? '?publishTarget=' + options.publishTarget : '');
var responseError = {name: options.name, nameType: 'extension', appError: errs.publish.failed, more: {}};
request.post({
uri : 'https://www.googleapis.com' + url,
headers : {
'Authorization' : 'Bearer ' + options.account.token,
'x-goog-api-version' : '2',
'Content-Length' : '0'
},
json : true
})
.on('response', function (response) {
var returnResult;
var isError = false;
response.on('data', function(data) {
var dataString = data.toString('utf8');
var dataJson = JSON.parse(dataString);
if (dataJson.error) {
isError = true;
returnResult = dataJson.error;
}
});
response.on('error', function(err) {
responseError.more = err;
logger('err', responseError);
return reject(responseError);
});
response.on('end', function() {
if (isError) {
responseError.more = returnResult;
logger('err', 'Error while publishing: ' + JSON.stringify(responseError));
return reject(responseError);
} else {
logger('log', 'Publishing done ('+ options.name +')');
return resolve('Publishing done ('+ options.name +')');
}
});
})
.on('error', function (err) {
responseError.more = err;
logger('err', 'Error while publishing: ' + JSON.stringify(responseError));
return reject(responseError);
});
});
}
//return most recent changed file in directory
function getRecentFile( dirName ){
var files = glob.sync( dirName + '/*.zip', { nodir: true}),
mostRecentFile,
currentFile;
if( files.length ){
for( var i = 0; i < files.length; i++ ){
currentFile = files[i];
if( !mostRecentFile ){
mostRecentFile = currentFile;
}else{
if( fs.statSync( currentFile ).mtime > fs.statSync( mostRecentFile ).mtime ){
mostRecentFile = currentFile;
}
}
}
return mostRecentFile;
}else{
return false;
}
}
// Request access token from code
function requestToken( account, redirectUri, code ){
logger('info', 'code for account ' + account.name + ': ' + code);
var post_data = util.format('client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code&redirect_uri=%s', account.client_id, account.client_secret, code, redirectUri);
return rp({
uri : 'https://accounts.google.com/o/oauth2/token',
method : 'POST',
headers : {
'Content-Type' : 'application/x-www-form-urlencoded'
},
body : post_data,
json : true
})
.then(function (body) {
return Q(body.access_token);
})
.catch(function (err) {
logger('err', 'Failed to refresh token: ' + errs.token.failedRequest);
return Q.reject(errs.token.failedRequest);
});
}
// get OAuth token using ssh-friendly cli
function getTokenForAccountCli( account ){
return Q.Promise(function(resolve, reject) {
var redirectUri = 'urn:ietf:wg:oauth:2.0:oob';
var codeUrl = util.format('https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=%s&redirect_uri=%s', account.client_id, redirectUri);
var readline = require('readline');
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(util.format('Please open %s and enter code: ', codeUrl), function(code) {
rl.close();
return resolve(requestToken(account, redirectUri, code ));
});
});
}
//get OAuth token
function getTokenForAccount( account ){
return Q.Promise(function(resolve, reject) {
var port = 14809,
callbackURL = util.format('http://localhost:%s', port),
server = http.createServer(),
codeUrl = util.format('https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=%s&redirect_uri=%s', account.client_id, callbackURL);
logger('log', ' ');
logger('log', 'Authorization for account: ' + account.name);
logger('log', '================');
//due user interaction is required, we creating server to catch response and opening browser to ask user privileges
server.on('connection', function(socket) {
//reset Keep-Alive connections in order to quick close server
socket.setTimeout(1000);
});
server.on('request', function(req, res){
var code = url.parse(req.url, true).query['code']; //user browse back, so code in url string
if( code ){
res.end('Got it! Authorizations for account "' + account.name + '" done. Check your console for new details. Tab now can be closed.');
server.close(function () {
return resolve(requestToken(account, callbackURL, code ));
});
}else{
res.end('<a href="' + codeUrl + '">Please click here and allow access for account "' + account.name + '", to continue uploading..</a>');
}
});
server.listen( port, 'localhost' );
logger('log', ' ');
logger('log', 'Opening browser for authorization.. Please confirm privileges to continue..');
logger('log', ' ');
logger('log', util.format('If the browser didn\'t open within a minute, please visit %s manually to continue', callbackURL));
logger('log', ' ');
open(codeUrl);
});
}
};