linagora-rse
Version:
545 lines (457 loc) • 16.7 kB
JavaScript
;
var mongoose = require('mongoose');
var ObjectId = mongoose.Types.ObjectId;
var Application = mongoose.model('Application');
var zlib = require('zlib');
var tar = require('tar');
var fs = require('fs-extra');
var path = require('path');
var async = require('async');
var DEPLOYMENT_DIR = path.join(__dirname, '../../../apps');
function AwesomeAppManager(dependencies, moduleManager) {
this.logger = dependencies('logger');
this.storage = dependencies('filestore');
this.imageModule = dependencies('image');
this.communityModule = dependencies('community');
this.domainModule = dependencies('domain');
this.localPubsub = dependencies('pubsub').local;
this.esnconfig = dependencies('esn-config');
this.moduleManager = moduleManager;
}
AwesomeAppManager.prototype.getDeploymentDirForApplication = function() {
// For now for the loader, into apps should be modules of which the directory is named the same than their AwesomeModule
// they are installing. We remove then application.title and version from the path.
return path.join(DEPLOYMENT_DIR);
};
AwesomeAppManager.prototype.store = function(application, callback) {
if (!application) {
return callback(new Error('Cannot store application without data.'));
}
if (application.deployments) {
return callback(new Error('Cannot store application with deployments defined.'));
}
var app = new Application(application);
app.save(callback);
};
AwesomeAppManager.prototype.getById = function(id, callback) {
if (!id) {
return callback(new Error('Application id is required'));
}
return Application.findOne({_id: id}, callback);
};
AwesomeAppManager.prototype.get = function(query, callback) {
query = query || {};
return Application.find(query, callback);
};
AwesomeAppManager.prototype.delete = function(id, callback) {
if (!id) {
return callback(new Error('Application id is required'));
}
return Application.findByIdAndRemove(id, callback);
};
AwesomeAppManager.prototype.getAvatar = function(artifactId, query, callback) {
return this.imageModule.getAvatar(artifactId, query, callback);
};
AwesomeAppManager.prototype.updateAvatar = function(application, avatar, callback) {
if (!application) {
return callback(new Error('application is required'));
}
if (!avatar) {
return callback(new Error('avatar is required'));
}
application.avatar = avatar;
application.save(callback);
};
AwesomeAppManager.prototype.uploadAvatar = function(avatarId, mimetype, metadata, req, callback) {
// It should be done here.
// var avatarId = uuid.v1();
return this.imageModule.recordAvatar(avatarId, mimetype, metadata, req, callback);
};
AwesomeAppManager.prototype.getArtifact = function(artifactId, callback) {
return this.storage.getFileStream(artifactId, callback);
};
AwesomeAppManager.prototype.updateArtifact = function(application, artifactMetadata, callback) {
if (!application) {
return callback(new Error('application is required'));
}
if (!artifactMetadata) {
return callback(new Error('avatar is required'));
}
application.artifacts.push(artifactMetadata);
application.save(callback);
};
function getDuplicates(readable) {
var PassThrough = require('stream').PassThrough;
var p1 = new PassThrough();
var p2 = new PassThrough();
readable.pipe(p1);
readable.pipe(p2);
return [p1, p2];
}
AwesomeAppManager.prototype.uploadArtifact = function(application, contentType, metadata, stream, options, callback) {
var self = this;
var fileId = new ObjectId();
function returnFileMeta(err) {
if (err) {
return callback(err);
}
return self.storage.getMeta(fileId, callback);
}
function updateApplication(application, injection, moduleName, callback) {
var updates = Object.create(null);
var objectType = injection.objectType;
delete injection.objectType;
if (objectType === 'domain') {
updates = {$push: {domainInjections: injection}, moduleName: moduleName};
} else {
updates = {$push: {targetInjections: injection}, moduleName: moduleName};
}
var options = {};
return application.update(updates, options, callback);
}
function storeArtifact(contentType, metadata, stream, options, callback) {
return self.storage.store(fileId, contentType, metadata, stream, options, callback);
}
function extractInjectionJson(srcArchive) {
var stringInjection = '';
var stringModuleName = '';
return srcArchive
.pipe(zlib.Unzip())
.pipe(tar.Parse())
.on('entry', function(entry) {
var self = this;
if (/injection\.json$/.test(entry.path)) {
stringModuleName = entry.path.split('/')[0];
entry.on('data', function(chunk) {
stringInjection += chunk;
}).on('end', function() {
if (!stringInjection) {
self.emit('error', new Error('Unable to find a valid injection.json in the artifact'));
} else {
var metadata = {};
metadata.injection = JSON.parse(stringInjection);
metadata.moduleName = stringModuleName;
self.emit('metadata', metadata);
}
});
}
});
}
var injection = {};
var moduleName = {};
var streams = getDuplicates(stream);
extractInjectionJson(streams[0])
.on('metadata', function(metadata) {
injection = metadata.injection;
moduleName = metadata.moduleName;
})
.on('end', function() {
async.parallel([
updateApplication.bind(null, application, injection, moduleName),
storeArtifact.bind(null, contentType, metadata, streams[1], options)
],
returnFileMeta
);
})
.on('error', function(err) {
return callback(err);
});
};
AwesomeAppManager.prototype.deploy = function(application, deployData, callback) {
var self = this;
if (!deployData || !deployData.target || !deployData.target.id || !deployData.version) {
return callback(new Error('Deploy data is not correctly formatted.'));
}
if (!deployData.target.objectType || deployData.target.objectType !== 'domain') {
return callback(new Error('Unsupported deploy object.'));
}
var isApplicationAlreadyDeployed = application.deployments.filter(function(deployment) {
return (deployment.target.id + '' === deployData.target.id + '') && (deployment.version === deployData.version);
});
if (isApplicationAlreadyDeployed.length > 0) {
this.logger.debug('Application was not deployed because a previous deployment already existed.');
return callback(null, application);
}
var applicationPath = this.getDeploymentDirForApplication(application, deployData.version);
var deploy = deployData;
function extractArtifactToPath(applicationPath, artifact, callback) {
if (!artifact) {
return callback(new Error('Could not retrieve archive file.'));
}
artifact
.pipe(zlib.Unzip())
.pipe(tar.Extract({ path: applicationPath }))
.on('end', function() {
return callback(null);
}).on('error', function(err) {
return callback(err);
});
}
function getArtifactStream(artifact, callback) {
if (!artifact) {
return callback(new Error('Artifact is needed to retreive the associated stream.'));
}
self.getArtifact(artifact.id, callback);
}
function getArtifactMetadataFromVersion(application, version, callback) {
application.getArtifactFromVersion(version, function(err, artifact) {
if (err) {
return callback(err);
}
if (!artifact) {
return callback(new Error('Can not find artifact for version ' + version));
}
return callback(null, artifact);
});
}
// We register the application here to gain time while installing it
// TODO use applicationPath when we supports version in applicationPath
function registerAppIntoManager(application, applicationPath, callback) {
var modulePath = path.normalize(
path.join(DEPLOYMENT_DIR, application.moduleName)
);
var exists = fs.existsSync(modulePath);
if (exists) {
self.moduleManager.manager.registerModule(require(modulePath), false);
} else {
return callback(new Error('This application does not exist'));
}
return callback(null);
}
function registerAppIntoEsnConfig(application, callback) {
var configuration = self.esnconfig('injection');
configuration.get(function(err, injection) {
if (err) {
callback(err);
}
if (!injection) {
configuration.store({ modules: [application.moduleName] }, callback);
} else {
var newModules = injection.modules || [];
if (newModules.indexOf(application.moduleName) === -1) {
newModules.push(application.moduleName);
configuration.set('modules', newModules, callback);
} else {
callback(null);
}
}
});
}
async.waterfall([
getArtifactMetadataFromVersion.bind(null, application, deployData.version),
getArtifactStream,
extractArtifactToPath.bind(null, applicationPath),
registerAppIntoManager.bind(null, application, applicationPath),
registerAppIntoEsnConfig.bind(null, application)
], function(err) {
if (err) {
return callback(err);
}
application.deployments.push(deploy);
application.save(callback);
});
};
AwesomeAppManager.prototype.undeploy = function(application, target, callback) {
var self = this;
if (!application) {
return callback(new Error('Application is required.'));
}
if (!target) {
return callback(new Error('Target is required.'));
}
var otherTargetDeployments = application.deployments.filter(function(deployment) {
return deployment.target.id + '' !== target.id + '';
});
if (otherTargetDeployments.length === application.deployments.length) {
this.logger.debug('Application has no existing deployments for this target.');
return callback(null, application);
}
function unregisterAppIntoEsnConfig(application, callback) {
var configuration = self.esnconfig('injection');
configuration.get(function(err, injection) {
if (err) {
callback(err);
}
if (!injection) {
callback(null);
} else {
var modules = injection.modules || [];
var newModules = modules.filter(function(module) {
return module !== application.moduleName;
});
configuration.set('modules', newModules, callback);
}
});
}
function removeAppFromFs(application, callback) {
fs.remove(self.getDeploymentDirForApplication(application, ''), function(err) {
if (err) {
return callback(err);
}
application.deployments = otherTargetDeployments;
application.save(callback);
});
}
async.series([
unregisterAppIntoEsnConfig.bind(null, application),
removeAppFromFs.bind(null, application)
], function(err) {
if (err) {
return callback(err);
}
callback(null);
});
};
AwesomeAppManager.prototype.setDeployState = function(application, deployTarget, newState, callback) {
if (!application) {
return callback(new Error('Application object is required.'));
}
if (!application.deployments) {
return callback(new Error('Application does not have deployments.'));
}
var targettedDeploys = application.deployments.filter(function(deployment) {
return deployment.target.id + '' !== deployTarget + '';
});
if (targettedDeploys.length === 0) {
this.logger.debug('No deployment was found for this target.');
return callback(null, application);
}
targettedDeploys[0].state = newState;
application.save(callback);
};
AwesomeAppManager.prototype.install = function(application, target, callback) {
var self = this;
if (!application) {
return callback(new Error('Application is required.'));
}
if (!target || !target.id) {
return callback(new Error('Target is required.'));
}
if (!target.objectType || (target.objectType !== 'community' && target.objectType !== 'domain')) {
return callback(new Error('Unsupported install target.'));
}
function load(targetId, callback) {
if (target.objectType === 'community') {
self.communityModule.load(targetId, callback);
} else if (target.objectType === 'domain') {
self.domainModule.load(targetId, callback);
}
}
function addInstallsToComplicantDeploys(application, objectType, target, callback) {
var compliantDeploys = [];
if (objectType === 'community') {
compliantDeploys = application.deployments.filter(function(deploy) {
return self.communityModule.hasDomain(target, deploy.target.id);
});
} else if (objectType === 'domain') {
compliantDeploys = application.deployments.filter(function(deploy) {
return deploy.target.id === target.id;
});
}
if (compliantDeploys.length === 0) {
return callback(new Error('Cannot install application before it has been deployed.'));
}
var install = { objectType: objectType, id: target.id };
compliantDeploys[0].installs.push(install);
callback(null, compliantDeploys);
}
function saveApplication(application, compliantDeploys, callback) {
application.save(function(err, saved) {
if (err) {
return callback(err);
}
callback(null, compliantDeploys, saved);
});
}
function publishToLocalPubsub(compliantDeploys, savedApplication, callback) {
var installedApplication = {
application: savedApplication,
target: target,
domain: compliantDeploys[0].target
};
self.localPubsub.topic('appstore:application:installed').publish(installedApplication);
callback(null);
}
function startApplication(application, callback) {
self.moduleManager.manager.fire('start', application.moduleName).then(function() {
return callback(null);
}, function(err) {
return callback(new Error(err.message));
});
}
async.waterfall([
load.bind(null, target.id),
addInstallsToComplicantDeploys.bind(null, application, target.objectType),
saveApplication.bind(null, application),
publishToLocalPubsub,
startApplication.bind(null, application)
], function(err) {
if (err) {
return callback(err);
}
return callback(null);
});
};
AwesomeAppManager.prototype.uninstall = function(application, target, callback) {
var self = this;
if (!application) {
return callback(new Error('Application is required.'));
}
if (!target || !target.id) {
return callback(new Error('Target is required.'));
}
if (!target.objectType || target.objectType !== 'community') {
return callback(new Error('Unsupported install target.'));
}
function getDomainsRemovedFrom(application, community, callback) {
var compliantDeploys = application.deployments.filter(function(deploy) {
return self.communityModule.hasDomain(community, deploy.target.id);
});
if (compliantDeploys.length === 0) {
return callback(new Error('Cannot uninstall an application which it has not been deployed.'));
}
var installToRemove = target;
var domainsRemovedFrom = [];
compliantDeploys.forEach(function(deploy) {
var installs = deploy.installs.filter(function(install) {
return install.id === installToRemove.id;
});
if (installs.length > 0) {
deploy.installs = deploy.installs.filter(function(install) {
return install.id !== installToRemove.id;
});
domainsRemovedFrom.push(deploy.target);
}
});
callback(null, domainsRemovedFrom);
}
function saveApplication(application, domainsRemovedFrom, callback) {
application.save(function(err, saved) {
if (err) {
return callback(err);
}
callback(null, domainsRemovedFrom, saved);
});
}
function publishToLocalPubsub(domainsRemovedFrom, savedApplication, callback) {
var uninstalledApplication = {
application: savedApplication,
target: target,
domains: domainsRemovedFrom
};
self.localPubsub.topic('appstore:application:uninstalled').publish(uninstalledApplication);
callback(null);
}
async.waterfall([
self.communityModule.load.bind(null, target.id),
getDomainsRemovedFrom.bind(null, application),
saveApplication.bind(null, application),
publishToLocalPubsub
], function(err) {
if (err) {
return callback(err);
}
return callback(null);
});
};
module.exports.AwesomeAppManager = AwesomeAppManager;