mozilla-packaged-webapp
Version:
Utility module for installing an app as preloaded & packaged app
294 lines (246 loc) • 7.21 kB
JavaScript
var achiver = require('archiver'),
readdirp = require('readdirp'),
fs = require('fs'),
fsPath = require('path'),
url = require('url'),
Webapps = require('./webapps').Webapps,
debug = require('debug')('packaged-webapp:install');
module.exports.webappDir = 'apps';
module.exports.webappsJSON = 'webapps.json';
module.exports.applicationZip = 'application.zip';
var MANIFEST = require('./webapps').manifest;
function validateAbsPath(name, input) {
if (!input || !fs.existsSync(input))
return new Error(name + '-> non-existant file path: ' + input);
return input;
}
function validateDomain(name, input) {
if (!input)
return new Error(name + ' is missing');
var parts = url.parse(input);
// when there is no protocol try to assign it app://
if (!parts.protocol) {
parts = url.parse('app://' + url.format(parts));
}
// no protocol or protocol is anything other then app
if (!parts.protocol || parts.protocol.indexOf('app') !== 0)
return new Error(name + ' has invalid origin ->' + input);
if (!parts.hostname)
return new Error(name + ' is missing hostname ' + input);
return parts;
}
var optionParse = {
// each is a required option
source: validateAbsPath,
origin: validateDomain
};
// creates or reuse the webapp target dir.
function createWebappTarget(target, callback) {
fs.exists(target, function(hasPath) {
if (hasPath) {
debug('webapp exists overriding', target);
return callback(null, target);
}
debug('create webapp', target);
// create the directory
fs.mkdir(target, callback);
});
}
// creates the application.zip file
function createApplicationZip(dest, source, callback) {
debug('creating application.zip', dest, source);
// archiver
var archive = achiver('zip');
// writable stream (will remove previous contents)
var output =
fs.createWriteStream(fsPath.join(dest, module.exports.applicationZip));
// saves contents of file to the zip archive
function append(data) {
debug('appending file to zip archive', data.path);
// create the readable stream and send it to be zipped
archive.append(
fs.createReadStream(data.fullPath),
{ name: data.path }
);
}
// close the zip archive and fire callback
function finalize() {
debug('finalizing zip');
archive.finalize(function(err, written) {
if (err) return callback(err);
debug('wrote bytes to zip', written);
callback();
});
}
archive.pipe(output);
// readdirp stream
var stream = readdirp({ root: source });
// abort
stream.on('error', function(err) {
return callback(err);
});
stream.on('warn', function(err) {
debug('zip warning', err);
});
stream.on('data', append);
stream.on('end', finalize);
}
// copies the manifest file from the app to the target dir
function copyManifest(dest, source, callback) {
debug('copying manifest', dest, source);
function handleError(err) {
callback && callback(err);
callback = null;
}
var readable = fs.createReadStream(fsPath.join(source, MANIFEST));
var writable = fs.createWriteStream(fsPath.join(dest, MANIFEST));
// handle read/write errors
readable.on('error', handleError);
writable.on('error', handleError);
readable.pipe(writable);
writable.on('close', callback);
}
// updates the global webapps manifest
function updateWebapps(originParsed, webapps, source, callback) {
var appManifest = fsPath.join(source, MANIFEST);
function finalize(manifest) {
var domain = originParsed.hostname;
debug('adding', domain, 'to webapps.json');
new Webapps(webapps).add(domain, manifest);
callback();
}
fs.readFile(appManifest, 'utf8', function(err, content) {
if (err) return callback(err);
var json;
try {
json = JSON.parse(content);
} catch (e) {
return callback(e);
}
finalize(json);
});
}
/**
* Creates a packaged app and installs it into the given profile.
*
* var options = {
* source: '/path/to/app',
* target: '/path/to/profile',
* origin: 'myapp.com'
* };
*
* require('packaged-web-app').install(
* options,
* function(err) {
* // yey installed!
* }
* );
*
*
* Options:
* - (String) source: absolute path where app is located.
* - (String) origin: the origin (domain) of the webapp.
*
* @param {Object} webapps in memory representation of webapps.json.
* @param {String} options for installation.
* @param {Function} callback [Error err].
*/
function stageAppInstall(webapps, profile, options, callback) {
debug('stage app', profile, options);
if (typeof options !== 'object')
throw new Error('options are required');
// local copy of options
var config = {};
// validate & parse install options
for (var name in optionParse) {
var opt = optionParse[name](name, options[name]);
if (opt instanceof Error) {
callback(opt);
return;
}
config[name] = opt;
}
// location in profile where files go (profile/webapps/HOSTNAME/
var webappTarget = fsPath.join(
profile, module.exports.webappDir, config.origin.hostname
);
createWebappTarget(webappTarget, function(err) {
if (err) return callback(err);
var pending = 3;
function next(err) {
if (err) {
callback && callback(err);
// remove callback reference so we don't fire twice
callback = null;
}
// when we are done doing the operation in parallel
if (--pending === 0) {
callback();
}
}
updateWebapps(
config.origin,
webapps,
config.source,
next
);
createApplicationZip(webappTarget, config.source, next);
copyManifest(webappTarget, config.source, next);
});
}
function installApp(profile, app, callback) {
installApps(profile, [app], callback);
}
function installApps(profile, apps, callback) {
// if there is less the one app skip
if (!apps || apps.length === 0)
return process.nextTick(callback);
// fetch the manifest
var webappsPath = fsPath.join(
profile,
module.exports.webappDir,
module.exports.webappsJSON
);
function writeWebappsJSON(webapps) {
debug('updating webapps.json');
fs.writeFile(
webappsPath,
JSON.stringify(webapps, null, 2),
callback
);
}
function updateApps(webapps) {
var pending = 0;
function next(err) {
if (err) {
callback && callback(err);
callback = null;
return;
}
if (--pending === 0) {
writeWebappsJSON(webapps);
}
}
apps.forEach(function(app) {
pending++;
stageAppInstall(webapps, profile, app, next);
});
}
// attempt to read the manifest (or use an empty object)
fs.readFile(webappsPath, 'utf8', function(err, content) {
if (err && err.code === 'ENOENT') {
err = null;
content = '{}';
}
if (err) return callback(err);
var json;
try {
json = JSON.parse(content);
} catch (e) {
return callback(e);
}
updateApps(json);
});
}
module.exports.installApp = installApp;
module.exports.installApps = installApps;