Source: index.js

//  .d888
// d88P'
// 888
// 888888.d8888b 88888b.d88b.        .d8888b .d88b. 888d888 .d88b.
// 888   88K     888 '888 '88b      d88P'   d88''88b888P'  d8P  Y8b
// 888   'Y8888b.888  888  888888888888     888  888888    88888888
// 888        X88888  888  888      Y88b.   Y88..88P888    Y8b.
// 888    88888P'888  888  888       'Y8888P 'Y88P' 888     'Y8888

import nodegit from 'nodegit';
import fs from 'fs-extra';
import jsonfile from 'jsonfile';
import rimraf from 'rimraf';
import debugStart from 'debug';
import validator from 'xsd-schema-validator';
import tmp from 'tmp';

let debug = debugStart('core');

/**
 * This module interfaces with a git repository to facilitate the persistence of SCXML files to be used in the fsm-engine module
 */
class Core {

    constructor(repositoryPath = __dirname + '/repo', name = 'default', email = 'None') {
        this.repositoryPath = repositoryPath;
        this.machinesDirPath = repositoryPath + '/machines';
        this.manifestPath = repositoryPath + '/manifest.json';
        this.configPath = repositoryPath + '/config.json';
        this.hasRemote = false;
        this.name = name;
        this.email = email;
        debug('Using path %s', repositoryPath);
    }

    /**
     * Initializes the repository connection
     * @method init
     * @memberOf Core
     * @returns {Promise} Repository connection
     */
    async init() {
        debug('Checking if there is a repository');
        let repo;
        try {
            repo = await nodegit.Repository.open(this.repositoryPath);
        } catch (err) {
            debug('Repository not found.');
            repo = await this._createRepository();
        }

        debug('Repository is ready');
        return repo;
    }

    /**
     * Initializes the repository connection using ssh password
     * @method initRemoteGitPlaintext
     * @memberOf Core
     * @param {String} cloneURL The URL of the remote repository
     * @param {String} user username to use to authenticate.
     * @param {String} password password to use to authenticate.
     * @returns {Promise} Repository connection
     */
    async initRemoteGitPlaintext(cloneURL, user, password) {

        // Simple object to store clone options.
        let cloneOptions = {};
        // This is a required callback for OS X machines.  There is a known issue
        // with libgit2 being able to verify certificates from GitHub.
        cloneOptions.fetchOpts = {
            callbacks: {
                certificateCheck: function () {
                    return 1;
                },
                credentials: function () {
                    nodegit.Cred.userpassPlaintextNew(user, password);
                }
            }
        };

        // Invoke the clone operation and store the returned Promise.
        let cloneRepository = nodegit.Clone(cloneURL, this.repositoryPath, cloneOptions);

        // If the repository already exists, the clone above will fail.  You can simply
        // open the repository in this case to continue execution.
        let errorAndAttemptOpen = () => {
            return nodegit.Repository.open(this.repositoryPath);
        };

        let repo = await cloneRepository.catch(errorAndAttemptOpen);
        this.hasRemote = true;
        this.user = user;
        this.password = password;

        //todo - Check the integrity of the repository
        if (!fs.existsSync(this.repositoryPath + '/machines')) {
            fs.mkdirSync(this.repositoryPath + '/machines');
        }

        debug('Repository is ready');
        return repo;
    }

    /**
     * Initializes the repository connection using ssh password
     * @method initRemoteGitSSH
     * @memberOf Core
     * @param {String} cloneURL The URL of the remote repository
     * @param {String} publicKey The public key of the credential.
     * @param {String} privateKey The private key of the credential.
     * @param {String} passphrase The passphrase of the credential.
     * @returns {Promise} Repository connection
     */
    async initRemoteGitSSH(cloneURL, publicKey, privateKey, passphrase) {

        console.log("ehe["+publicKey[0]+"]",publicKey);
        if(publicKey.startsWith("ssh-rsa")){
            let pubKey = tmp.fileSync();
            fs.writeFileSync(pubKey.name, publicKey);
            publicKey = pubKey.name;
            let priKey = tmp.fileSync();
            fs.writeFileSync(priKey.name, privateKey);
            privateKey = priKey.name;
        }


        debug('Initializing');
        // Simple object to store clone options.
        let cloneOptions = {};
        // This is a required callback for OS X machines.  There is a known issue
        // with libgit2 being able to verify certificates from GitHub.
        cloneOptions = {
            fetchOpts: {
                callbacks: {
                    certificateCheck: function () {
                        return 1;
                    },
                    credentials: function (url, userName) {
                        try{
                            return nodegit.Cred.sshKeyNew(
                                userName,
                                publicKey,
                                privateKey,
                                passphrase);
                        } catch(err){
                            debug(err);
                            throw err;
                        }
                    }
                }
            }
        };

        debug('Cloning');
        // Invoke the clone operation and store the returned Promise.
        let cloneRepository = nodegit.Clone(cloneURL, this.repositoryPath, cloneOptions);

        // If the repository already exists, the clone above will fail.  You can simply
        // open the repository in this case to continue execution.

        let errorAndAttemptOpen = async (err) => {
            debug(err);
            debug('Checking if the repository was already cloned');
            let repository = await nodegit.Repository.open(this.repositoryPath);
            await repository.fetchAll(cloneOptions.fetchOpts);
            return repository.mergeBranches('master', 'origin/master');
        };

        let repo = await cloneRepository.catch(errorAndAttemptOpen);

        debug('Cloned successfully');

        this.hasRemote = true;
        this.hasRemoteSSH = true;
        this.publicKey = publicKey;
        this.privateKey = privateKey;
        this.passphrase = passphrase;
        //todo - Check the integrity of the repository
        if (!fs.existsSync(this.repositoryPath + '/machines')) {
            fs.mkdirSync(this.repositoryPath + '/machines');
        }
        debug('Repository is ready');
        return repo;
    }

    /**
     * Recursively gather the paths of the files inside a directory path
     * @method _getFiles
     * @memberOf Core
     * @param {String} path The directory path to search
     * @returns {Array} An Array of file paths belonging to the directory path provided
     * @private
     */
    _getFiles(path) {
        let files = [];
        fs.readdirSync(path).forEach((file) => {
            let subpath = path + '/' + file;
            if (fs.lstatSync(subpath).isDirectory()) {
                let filesReturned = this._getFiles(subpath);
                files = files.concat(filesReturned);
            } else {
                files.push(path + '/' + file);
            }
        });
        return files;
    }

    /**
     * Commit to the repository
     * @method _commit
     * @memberOf Core
     * @param {Repository} repo The repository connection object
     * @param {Array} pathsToStage The array of file paths(relative to the repository) to be staged
     * @param {String} message The message to go along with this commit
     * @param {Array} pathsToUnstage The array of file paths(relative to the repository) to be un-staged
     * @returns {Array} An Array of file paths belonging to the directory path provided
     * @private
     */
    async _commit(repo, pathsToStage, message = null, pathsToUnstage = []) {
        repo = repo || (await nodegit.Repository.open(this.repositoryPath));
        debug('Adding files to the index');
        let index = await repo.refreshIndex(this.repositoryPath + '/.git/index');

        if (pathsToUnstage && pathsToUnstage.length && pathsToUnstage.length > 0) {
            for (let file of pathsToUnstage) {
                await index.removeByPath(file);
            }
            await index.write();
            await index.writeTree();
        }

        debug('Creating main files');
        let signature = nodegit.Signature.now(this.name, this.email);

        debug('Commiting');

        await repo.createCommitOnHead(pathsToStage, signature, signature, message || 'Automatic initialization');

        if (this.hasRemote) {

            debug('Pushing');
            if (this.hasRemoteSSH) {
                let remote = await repo.getRemote('origin');
                //
                remote.connect(nodegit.Enums.DIRECTION.PUSH);

                await remote.push(['refs/heads/master:refs/heads/master'], {
                    callbacks: {
                        certificateCheck: function () {
                            return 1;
                        },
                        credentials: (url, userName) => {
                            return nodegit.Cred.sshKeyNew(userName, this.publicKey, this.privateKey, this.passphrase);
                        }
                    },
                });

            } else {

                let remote = await repo.getRemote('origin');
                await remote.push(['refs/heads/master:refs/heads/master'], {
                    callbacks: {
                        certificateCheck: function () {
                            return 1;
                        },
                        credentials: (url, userName) => {
                            return NodeGit.Cred.userpassPlaintextNew(this.user, this.password);
                        }
                    },
                });

            }

            debug('Changes were pushed');

        }
    }

    /**
     * Create a new repository
     * @method _createRepository
     * @memberOf Core
     * @returns {Promise} Repository connection
     * @private
     */
    async _createRepository() {
        try {
            debug('Creating a new one');
            let repo = await nodegit.Repository.init(this.repositoryPath, 0);
            debug('Connection established');
            debug('Creating main files');
            await this._createManifest();
            await this._createConfig();
            fs.mkdirSync(this.machinesDirPath);
            await this._commit(repo, ['manifest.json', 'config.json']);
            debug('Repository was successfully created');
            return repo;
        } catch (err) {
            debug(err);
            debug('Nuking the repository');
            await new Promise((resolve, reject) => {
                rimraf(this.repositoryPath, () => {
                    resolve();
                });
            }).then();
            throw new Error(err);
        }
    }

    /**
     * Create the manifest file inside the repository
     * @method _createManifest
     * @memberOf Core
     * @returns {Promise}
     * @private
     */
    _createManifest() {
        let file = this.repositoryPath + '/manifest.json';
        let manifest = {
            machines: {}
        };

        return new Promise((resolve, reject) => {
            debug('Creating manifest file');
            jsonfile.writeFile(file, manifest, (err) => {
                if (err) {
                    debug('Failed to create the manifest file');
                    reject(err);
                    return;
                }
                resolve();
            });
        });
    }

    /**
     * Create the config file inside the repository
     * @method _createManifest
     * @memberOf Core
     * @returns {Promise}
     * @private
     */
    _createConfig() {
        let file = this.repositoryPath + '/config.json';
        let config = {
            simulation: false
        };
        return new Promise((resolve, reject) => {
            jsonfile.writeFile(file, config, (err) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve();
            });

        });
    }

    /**
     * Retrieve the repository path
     * @method getRepositoryPath
     * @memberOf Core
     * @returns {String} The path to the repository
     */
    getRepositoryPath() {
        return this.repositoryPath;
    }

    /**
     * Retrieve the repository manifest.json file as a JavasScript Object
     * @method getManifest
     * @memberOf Core
     * @returns {Object} The manifest Object
     */
    getManifest() {
        return jsonfile.readFileSync(this.manifestPath);
    }

    /**
     * Update the repository manifest.json file using a JavasScript Object
     * @method setManifest
     * @memberOf Core
     * @param {Object} manifest The manifest Object to save
     * @param {String} message If supplied it is used as the message for the commit
     * @returns {Promise}
     */
    async setManifest(manifest, message = null) {
        jsonfile.writeFileSync(this.manifestPath, manifest, {spaces: 2});
        return await this._commit(null, ['manifest.json'],
            message || 'Changed the manifest file');
    }

    /**
     * Retrieve the repository config.json file as a JavasScript Object
     * @method getConfig
     * @memberOf Core
     * @returns {Object} The config Object
     */
    getConfig() {
        return jsonfile.readFileSync(this.configPath);
    }

    /**
     * Update the repository config.json file using a JavasScript Object
     * @method setConfig
     * @memberOf Core
     * @param {Object} config The config Object to save
     * @param {String} message If supplied it is used as the message for the commit
     * @returns {Promise}
     */
    async setConfig(config, message = null) {
        jsonfile.writeFileSync(this.configPath, config, {spaces: 2});
        return await this._commit(null, ['config.json'],
            message || 'Changed the config file');
    }

    /**
     * Get the names of all of the machines in the repository
     * @method getMachinesNames
     * @memberOf Core
     * @returns {Array} An array with all the machine's names
     */
    getMachinesNames() {
        let manifest = this.getManifest();
        return Object.keys(manifest.machines);
    }

    /**
     * Add a new machine to the repository
     * @method addMachine
     * @memberOf Core
     * @param {String} name The name of the new machine
     * @returns {Promise}
     */
    async addMachine(name) {
        debug('Adding a new machine with the name "%s"', name);
        let manifest = this.getManifest();

        if (manifest.machines[name]) {
            debug('Machine already exists');
            throw new Error('Machine already exists');
        }

        manifest.machines[name] = {
            route: 'machines/' + name,
            'versions': {
                'version1': {
                    'route': 'machines/' + name + '/versions/version1',
                    'instances': {}
                }
            }
        };

        let machineDirPath = 'machines/' + name;
        let machineVersionsDirPath = machineDirPath + '/versions';
        let version1DirPath = machineVersionsDirPath + '/version1';
        let version1InstancesDirPath = version1DirPath + '/instances';
        let modelFile = version1DirPath + '/model.scxml';
        let infoFile = version1DirPath + '/info.json';

        debug('Creating the directories');
        fs.mkdirSync(this.repositoryPath + '/' + machineDirPath);
        fs.mkdirSync(this.repositoryPath + '/' + machineVersionsDirPath);
        fs.mkdirSync(this.repositoryPath + '/' + version1DirPath);
        fs.mkdirSync(this.repositoryPath + '/' + version1InstancesDirPath);

        debug('Creating the base.scxml file');
        fs.copySync(__dirname + '/base.scxml', this.repositoryPath + '/' + modelFile);

        debug('Creating the version info.json file');
        let infoVersion1 = {'isSealed': false};
        jsonfile.writeFileSync(this.repositoryPath + '/' + infoFile, infoVersion1);

        debug('Setting the manifest');
        await this.setManifest(manifest);

        await this._commit(null, ['manifest.json', modelFile, infoFile], 'Added "' + name + '" machine');
        debug('A new machine with the name "%s" was successfully added', name);
    }

    /**
     * Remove a machine from the repository
     * @method removeMachine
     * @memberOf Core
     * @param {String} name The name of the machine
     * @returns {Promise}
     */
    async removeMachine(name) {
        debug('Removing the machine');
        let manifest = this.getManifest();

        if (!manifest.machines[name]) {
            debug('Machine doesn\'t exists');
            return;
        }

        let machinePath = this.machinesDirPath + '/' + name;
        let removedFileNames = this._getFiles(machinePath).map((f) => f.substring(this.repositoryPath.length + 1));

        delete manifest.machines[name];
        await new Promise((resolve, reject) => {
            rimraf(this.machinesDirPath + '/' + name, () => {
                resolve();
            });
        }).then();

        debug('Setting the manifest');
        await this.setManifest(manifest);

        await this._commit(null, ['manifest.json'], 'Removed "' + name + '"  machine.', removedFileNames);

        return Object.keys(manifest.machines);

    }

    /**
     * Get the keys of all of the versions of machine in the repository
     * @method getVersionsKeys
     * @memberOf Core
     * @param {String} machineName The name of the machine to get the version's keys
     * @returns {Array} An array with all the version's keys of the machine
     */
    getVersionsKeys(machineName) {
        let manifest = this.getManifest();
        if (!manifest.machines[machineName]) {
            throw new Error('Machine does not exists');
        }
        return Object.keys(manifest.machines[machineName].versions);
    }

    /**
     * Retrieve the version directory path
     * @method getVersionRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {String} The route
     */
    getVersionRoute(machineName, versionKey) {
        let manifest = this.getManifest();

        if (!manifest.machines[machineName]) {
            throw new Error('Machine does not exists');
        }

        if (!manifest.machines[machineName].versions[versionKey]) {
            throw new Error('Version does not exists');
        }

        return manifest.machines[machineName].versions[versionKey].route;
    }

    /**
     * Retrieve the version's info.json file path
     * @method getVersionInfoRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {String} The route
     */
    getVersionInfoRoute(machineName, versionKey) {
        return this.getVersionRoute(machineName, versionKey) + '/info.json';
    }

    /**
     * Retrieve the version's model.scxml file path
     * @method getVersionModelRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {String} The route
     */
    getVersionModelRoute(machineName, versionKey) {
        return this.getVersionRoute(machineName, versionKey) + '/model.scxml';
    }

    /**
     * Retrieve the version info.json file as a JavasScript Object
     * @method getVersionInfo
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {Object} The info Object
     */
    getVersionInfo(machineName, versionKey) {
        let route = this.getVersionInfoRoute(machineName, versionKey);
        return jsonfile.readFileSync(this.repositoryPath + '/' + route);
    }

    /**
     * Update the version info.json file using a JavasScript Object
     * @method setVersionInfo
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {Object} info The info Object to save
     * @param {String} message If supplied it is used as the message for the commit
     * @returns {Promise}
     */
    async setVersionInfo(machineName, versionKey, info, message = null) {

        let route = this.getVersionInfoRoute(machineName, versionKey);
        let previousInfo = jsonfile.readFileSync(this.repositoryPath + '/' + route);
        if (previousInfo.isSealed) {
            throw new Error('Cannot change the version SCXML because the version is sealed.')
        }

        jsonfile.writeFileSync(this.repositoryPath + '/' + route, info, {spaces: 2});

        return await this._commit(null, [route],
            message || 'Changed the info for the ' + versionKey + ' of the "' + machineName + '" machine');

    }


    /**
     * Add a new version to a machine
     * @method addVersion
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @returns {Promise} The version key
     */
    async addVersion(machineName) {

        debug('Adding a new version to the "' + machineName + '" machine');
        let manifest = this.getManifest();

        if (!manifest.machines[machineName]) {
            throw new Error('Machine does not exists');
        }

        let versions = manifest.machines[machineName].versions;
        let versionKeys = Object.keys(versions);
        let lastVersionKey = versionKeys[versionKeys.length - 1];
        let lastVersion = versions[lastVersionKey];
        let lastVersionInfoFile = lastVersion.route + '/info.json';
        let lastVersionInfo = jsonfile.readFileSync(this.repositoryPath + '/' + lastVersionInfoFile);
        let lastVersionModelFile = lastVersion.route + '/model.scxml';

        if (!lastVersionInfo.isSealed) {
            throw new Error('The last versions is not sealed yet');
        }

        let newVersionKey = 'version' + (versionKeys.length + 1);
        let versionDirPath = manifest.machines[machineName].route + '/versions/' + newVersionKey;
        manifest.machines[machineName].versions[newVersionKey] = {
            'route': versionDirPath,
            'instances': {}
        };

        let versionInstancesDirPath = versionDirPath + '/instances';
        let modelFile = versionDirPath + '/model.scxml';
        let infoFile = versionDirPath + '/info.json';

        debug('Creating the directories');
        fs.mkdirSync(this.repositoryPath + '/' + versionDirPath);
        fs.mkdirSync(this.repositoryPath + '/' + versionInstancesDirPath);

        debug('Copying the previous version\'s model.scxml');
        fs.copySync(this.repositoryPath + '/' + lastVersionModelFile, this.repositoryPath + '/' + modelFile);

        debug('Creating the version info.json file');
        let infoVersion = {'isSealed': false};
        jsonfile.writeFileSync(this.repositoryPath + '/' + infoFile, infoVersion);

        debug('Setting the manifest');
        await this.setManifest(manifest);
        await this._commit(null, ['manifest.json', modelFile, infoFile],
            'Created the ' + newVersionKey + ' for the "' + machineName + '" machine');

        return newVersionKey;


    }

    /**
     * Seal a version of a machine
     * @method addMachine
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {Promise}
     */
    async sealVersion(machineName, versionKey) {
        debug('Attempting to seal the version "%s" of the machine "%s"', versionKey, machineName);
        let info = await this.getVersionInfo(machineName, versionKey);
        if (info.isSealed) {
            throw new Error('Version it already sealed');
        }

        debug('Getting manifest');
        let manifest = this.getManifest();
        let model = this.getVersionSCXML(machineName, versionKey);
        let isValid = await this.isSCXMLValid(model);

        if (!isValid) {
            throw new Error('The model is not valid.');
        }

        info.isSealed = true;
        await this.setVersionInfo(machineName, versionKey, info, true, "Sealed the version " +
            versionKey + "of the machine " + machineName + ".");
        debug('The version "%s" of the machine "%s" was sealed successfully', versionKey, machineName);

    }


    /**
     * Retrieve the version model.scxml file as a String
     * @method getVersionSCXML
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {String} The model
     */
    getVersionSCXML(machineName, versionKey) {
        let route = this.getVersionModelRoute(machineName, versionKey);
        return fs.readFileSync(this.repositoryPath + '/' + route).toString('utf8');
    }

    /**
     * Update the version model.scxml file using a String
     * @method setVersionSCXML
     * @memberOf Core
     * @param {String} model The model
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} message If supplied it is used as the message for the commit
     * @returns {Promise}
     */
    async setVersionSCXML(machineName, versionKey, model, message = null) {

        let route = this.getVersionInfoRoute(machineName, versionKey);
        let previousInfo = jsonfile.readFileSync(this.repositoryPath + '/' + route);
        if (previousInfo.isSealed) {
            throw new Error('Cannot change the version SCXML because the version is sealed.')
        }
        let modelRoute = this.getVersionModelRoute(machineName, versionKey);
        fs.writeFileSync(this.repositoryPath + '/' + modelRoute, model);

        return await this._commit(null, [modelRoute],
            message || 'Changed the model.scxml for the ' + versionKey + ' of the "' + machineName + '" machine');
    }

    /**
     * Validates SCXML markup as a string
     * @method isSCXMLValid
     * @memberOf Core
     * @param {String} model A string with the SCXML document to validate
     * @returns {Promise} True if the SCXML is valid false otherwise
     */
    isSCXMLValid(model) {
        return new Promise((resolve, reject) => {
            if (model === '') {
                reject('Model is empty');
                return;
            }

            validator.validateXML(model, __dirname + '/xmlSchemas/scxml.xsd', (err, result) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(result.valid);
            })
        });
    }

    /**
     * Gets the keys of all of the instances of a version of the machine in the repository
     * @method getInstancesKeys
     * @memberOf Core
     * @param {String} machineName The name of the machine to get the instances's keys
     * @param {String} versionKey The key of the version to get the instances's keys
     * @returns {Array} An array with all the instance's keys of the the version
     */
    getInstancesKeys(machineName, versionKey) {

        let manifest = this.getManifest();

        let machine = manifest.machines[machineName];
        if (!machine) {
            throw new Error('Machine does not exists');
        }

        let version = machine.versions[versionKey];
        if (!version) {
            throw new Error('Version does not exists');
        }

        return Object.keys(version.instances);
    }

    /**
     * Retrieve the instance's directory path
     * @method getInstanceRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @returns {String} The route
     */
    getInstanceRoute(machineName, versionKey, instanceKey) {

        let manifest = this.getManifest();

        let machine = manifest.machines[machineName];
        if (!machine) {
            throw new Error('Machine does not exists');
        }

        let version = machine.versions[versionKey];
        if (!version) {
            throw new Error('Version does not exists');
        }

        let instance = version.instances[instanceKey];
        if (!instance) {
            throw new Error('Instance does not exists');
        }

        return instance.route;
    }

    /**
     * Retrieve the instance's info.json path
     * @method getInstanceInfoRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @returns {String} The route
     */
    getInstanceInfoRoute(machineName, versionKey, instanceKey) {
        return this.getInstanceRoute(machineName, versionKey, instanceKey) + '/info.json';
    }

    /**
     * Retrieve the instance's info.json file as a JavasScript Object
     * @method getInstanceInfo
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @returns {Object} The info Object
     */
    getInstanceInfo(machineName, versionKey, instanceKey) {
        let route = this.getInstanceInfoRoute(machineName, versionKey, instanceKey);
        return jsonfile.readFileSync(this.repositoryPath + '/' + route);
    }

    /**
     * Update the instance's info.json file using a JavasScript Object
     * @method setInstanceInfo
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @param {Object} info The info Object to save
     * @param {String} message If supplied it is used as the message for the commit
     * @returns {Promise}
     */
    async setInstanceInfo(machineName, versionKey, instanceKey, info, message = null) {

        let route = this.getInstanceInfoRoute(machineName, versionKey, instanceKey);
        jsonfile.writeFileSync(this.repositoryPath + '/' + route, info, {spaces: 2});

        return await this._commit(null, [route],
            message || 'Changed the info for the ' + instanceKey + ' of the ' +
            versionKey + ' of the "' + machineName + '" machine');

    }

    /**
     * Add a new instance to a version of a machine
     * @method addInstance
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @returns {Promise} The instance key
     */
    async addInstance(machineName, versionKey) {

        debug('Adding a new instance to the ' + versionKey + ' of the "' + machineName + '" machine');
        let manifest = this.getManifest();

        let machine = manifest.machines[machineName];
        if (!machine) {
            throw new Error('Machine does not exists');
        }

        let version = machine.versions[versionKey];
        if (!version) {
            throw new Error('Version does not exists');
        }

        let versionInfo = this.getVersionInfo(machineName, versionKey);
        if (!versionInfo.isSealed) {
            throw new Error('The version is not sealed yet');
        }

        let newInstanceKey = 'instance' + (Object.keys(version.instances).length + 1);
        let instanceDirPath = version.route + '/instances/' + newInstanceKey;
        let instanceSnapshotsDirPath = instanceDirPath + '/snapshots';
        version.instances[newInstanceKey] = {
            'route': instanceDirPath,
            'snapshots': {}
        };

        let infoFile = instanceDirPath + '/info.json';

        debug('Creating the directories');
        if (!fs.existsSync(this.repositoryPath + "/" + version.route + '/instances')) {
            fs.mkdirSync(this.repositoryPath + "/" + version.route + '/instances');
        }
        fs.mkdirSync(this.repositoryPath + '/' + instanceDirPath);
        fs.mkdirSync(this.repositoryPath + '/' + instanceSnapshotsDirPath);

        debug('Creating the instance info.json file');
        let info = {
            'hasStarted': false,
            'hasEnded': false
        };
        jsonfile.writeFileSync(this.repositoryPath + '/' + infoFile, info);

        debug('Setting the manifest');
        await this.setManifest(manifest);
        await this._commit(null, ['manifest.json', infoFile],
            'Created the ' + newInstanceKey + ' for the ' + versionKey + ' of the "' + machineName + '" machine');

        return newInstanceKey;
    }

    /**
     * Gets the keys of all of the snapshots of the instance of a version of the machine in the repository
     * @method getSnapshotsKeys
     * @memberOf Core
     * @param {String} machineName The name of the machine to get the snapshots's keys
     * @param {String} versionKey The key of the version to get the snapshots's keys
     * @param {String} instanceKey The key of the instance to get the snapshot's keys
     * @returns {Array} An array with all the snapshot's keys of the instance
     */
    getSnapshotsKeys(machineName, versionKey, instanceKey) {

        let manifest = this.getManifest();

        let machine = manifest.machines[machineName];
        if (!machine) {
            throw new Error('Machine does not exists');
        }

        let version = machine.versions[versionKey];
        if (!version) {
            throw new Error('Version does not exists');
        }

        let instance = version.instances[instanceKey];
        if (!instance) {
            throw new Error('Instance does not exists');
        }

        return Object.keys(instance.snapshots);
    }

    /**
     * Retrieve the snapshot's directory path
     * @method getSnapshotRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @param {String} snapshotKey The key of the snapshot
     * @returns {String} The route
     */
    getSnapshotRoute(machineName, versionKey, instanceKey, snapshotKey) {

        let manifest = this.getManifest();

        let machine = manifest.machines[machineName];
        if (!machine) {
            throw new Error('Machine does not exists');
        }

        let version = machine.versions[versionKey];
        if (!version) {
            throw new Error('Version does not exists');
        }

        let instance = version.instances[instanceKey];
        if (!instance) {
            throw new Error('Instance does not exists');
        }

        let snapshot = instance.snapshots[snapshotKey];
        if (!snapshot) {
            throw new Error('Snapshot does not exists');
        }

        return snapshot.route;
    }

    /**
     * Retrieve the snapshot's info.json path
     * @method getSnapshotInfoRoute
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @param {String} snapshotKey The key of the snapshot
     * @returns {String} The route
     */
    getSnapshotInfoRoute(machineName, versionKey, instanceKey, snapshotKey) {
        return this.getSnapshotRoute(machineName, versionKey, instanceKey, snapshotKey) + '/info.json';
    }

    /**
     * Retrieve the snapshot's info.json file as a JavasScript Object
     * @method getSnapshotInfo
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @param {String} snapshotKey The key of the snapshot
     * @returns {Object} The info Object
     */
    getSnapshotInfo(machineName, versionKey, instanceKey, snapshotKey) {
        let route = this.getSnapshotInfoRoute(machineName, versionKey, instanceKey, snapshotKey);
        return jsonfile.readFileSync(this.repositoryPath + '/' + route);
    }

    /**
     * Add a new snapshot to an instance of a version of a machine
     * @method addSnapshot
     * @memberOf Core
     * @param {String} machineName The name of the machine
     * @param {String} versionKey The key of the version
     * @param {String} instanceKey The key of the instance
     * @param {Object} info The info Object
     * @returns {Promise} The instance key
     */
    async addSnapshot(machineName, versionKey, instanceKey, info) {

        debug('Adding a new snapshot to the ' + instanceKey + ' of the ' + versionKey + ' of the "' + machineName + '" machine');
        let manifest = this.getManifest();

        let machine = manifest.machines[machineName];
        if (!machine) {
            throw new Error('Machine does not exists');
        }

        let version = machine.versions[versionKey];
        if (!version) {
            throw new Error('Version does not exists');
        }

        let instance = version.instances[instanceKey];
        if (!instance) {
            throw new Error('Instance does not exists');
        }

        let newSnapshotKey = 'snapshot' + (Object.keys(instance.snapshots).length + 1);
        let snapshotDirPath = instance.route + '/snapshots/' + newSnapshotKey;
        instance.snapshots[newSnapshotKey] = {
            'route': snapshotDirPath
        };

        let infoFile = snapshotDirPath + '/info.json';

        debug('Creating the directories');

        if (!fs.existsSync(this.repositoryPath + "/" + instance.route + '/snapshots')) {
            fs.mkdirSync(this.repositoryPath + "/" + instance.route + '/snapshots');
        }
        fs.mkdirSync(this.repositoryPath + '/' + snapshotDirPath);

        debug('Creating the snapshot info.json file');
        jsonfile.writeFileSync(this.repositoryPath + '/' + infoFile, info);

        debug('Setting the manifest');
        await this.setManifest(manifest);
        await this._commit(null, ['manifest.json', infoFile],
            'Created the ' + newSnapshotKey + ' for the ' + instanceKey + ' of the ' +
            versionKey + ' of the "' + machineName + '" machine');

        return newSnapshotKey;
    }
}

export default Core;