dodekeract/symlinker

View on GitHub
symlinker.js

Summary

Maintainability
F
4 days
Test Coverage
var mkdirp = require('mkdirp');

var path = require('path');

var fs = require('fs');

/* basic functions */

function clone (obj){
    if(obj == null || typeof(obj) != 'object')
        return obj;

    var temp = obj.constructor();

    for(var key in obj)
        temp[key] = clone(obj[key]);
    return temp;
}

function open (options) {
    var rawFile;
    // options:
    // -path
    // -type
    // -(sourcePath)
    // -(destinationPath)
    // -(options.forceCreation)

    /* Validating */
    try {
        rawFile = fs.readFileSync(options.path, 'utf8');
    } catch (err) {
        console.log("Could not read the provided Symlinker file.\n" + options.path);
        process.exit(1);
    }

    /* Parsing Symlinker File */

    var task = new Array();

    switch (options.type) {
        case "text-newline":
            try {
                task = [{
                    'input': options.sourcePath,
                    'items': rawFile.replace(/(\r)/gm,"").split("\n"),
                    'output': options.destinationPath
                }];
            } catch (err) {
                console.log("Could not parse the provided Symlinker file.\n" + options.path);
                process.exit(1);
            }
        break;
        case "json":
            try {
                task = [{
                    'input': options.sourcePath,
                    'items': JSON.parse(rawFile),
                    'output': options.destinationPath
                }];
            } catch (err) {
                console.log("Could not parse the provided Symlinker file.\n" + options.path);
                process.exit(1);
            }
        break;
        case "advanced-newline":
            try {

                var temp = rawFile.replace(/(\r)/gm,"").split("\n");

                var queue = new Object();

                for (var i = 0; i < temp.length; i++) {

                    var content = temp[i].slice(1);

                    switch(temp[i][0]) {
                        case '$':
                            queue = {'input': content,'items':[]};
                        break;
                        case '-':
                            queue.items.push(content);
                        break;
                        case '>':
                            queue.output = content;
                            task.push(clone(queue));
                        break;
                        case undefined: break;
                        case '#': break;
                        default:
                            console.log("invalid file syntax " + JSON.stringify(temp[i]) + " in line " + i);
                    }
                }
            } catch (err) {
                console.error("Could not parse the provided Symlinker file.\n" + options.path);
                process.exit(1);
            }
        break;
        default:
            console.error("Invalid value for Symlinker-file type.");
            process.exit(1);
    }
    runTasks(task, options);
}

function runTasks (task, options) {
    for (var i = 0; i < task.length; i++) {
        var q = task[i];

        // checking if source folder is valid
        var valid = fs.existsSync(q.input);

        if (!valid && !options.forceCreation) {
            console.error('Path isn\'t valid: ' + q.input)
        } else {
            console.log('\n$' + q.input);
            for (var j = 0; j < q.items.length; j++) {
                try {
                    var outputPath = q.output + '/' + q.items[j];
                    var inputPath = q.input + '/' + q.items[j];
                    var dirs = outputPath.slice(0,outputPath.lastIndexOf('/'));
                    mkdirp.sync(dirs);
                    try {
                        if (fs.lstatSync(outputPath).isSymbolicLink()) {
                            if (!options.forceRecreation) {
                                console.log("Symbolic link " + outputPath  + " already exists.\nUse -r to force symbolic link recreation.");
                                process.exit(1);
                            } else {
                                fs.unlinkSync(outputPath);
                            }
                        }
                    } catch (err) {
                        // file doesn't exist
                    }
                    var stats = fs.lstatSync(inputPath);
                    if (stats.isDirectory()) {
                        fs.symlinkSync(inputPath, outputPath, 'dir');
                    } else if (stats.isFile()) {
                        fs.symlinkSync(inputPath, outputPath, 'file');
                    } else if (!options.isIgnoring) {
                        console.log("Could not create symbolic link. " + inputPath + " is neither a file nor a folder. Use -i to allow Symlinker to continue after this error.");
                        process.exit(1);
                    }
                    console.log('-' + q.items[j]);
                } catch (err) {
                    console.error(err);
                    console.error('Error creating symlink ' + q.input + ' -> ' + q.output);
                    process.exit(1);
                }
            }
            console.log('>' + q.output);
        }
    }
}

function basic (sourcePath, destinationPath, options, callback) {
    try {
        if (fs.lstatSync(destinationPath).isSymbolicLink()) {
            if (!options.recreateSymbolicLinks) {
                process.nextTick(function () {
                    return callback(new Error('Could not create symbolic link. Destination is already a symbolic link.'));
                });
            } else {
                fs.unlinkSync(destinationPath);
            }
        }
    } catch (err) {
        // file doesn't exist
    }

    if (fs.existsSync(sourcePath)) { // source path is valid
        mkdirp.sync(path.dirname(destinationPath));
        var stats = fs.lstatSync(sourcePath);
        if (stats.isDirectory()) {
            fs.symlinkSync(sourcePath, destinationPath, 'dir');
            process.nextTick(function () {
                return callback(null, true);
            });
        } else if (stats.isFile()) {
            fs.symlinkSync(sourcePath, destinationPath, 'file');
            process.nextTick(function () {
                return callback(null, true);
            });
        } else {
            process.nextTick(function () {
                return callback(new Error('Could not create symbolic link. Source is neither a file nor a folder.'));
            });
        }
    } else { // source path is invalid
        process.nextTick(function () {
            return callback(new Error('Could not create symbolic link. Source doesn\'t exist.'));
        });
    }
}

function removeBasic (path, callback) {
    try {
        if (fs.lstatSync(path).isSymbolicLink()) {
            fs.unlinkSync(path);
            process.nextTick(function () {
                return callback(null, true);
            });
        } else {
            process.nextTick(function () {
                return callback(new Error('File is no symbolic link'));
            });
        }
    } catch (err) {
        process.nextTick(function () {
            return callback(new Error('Symbolic link not found.'));
        });
    }
}

var queue = [];
var queueRunning;

function advanced (task, options, callback) {
    // task {source, destination, files:[{path, name}, *]}

    queue.push({
        action: 'link',
        source: task.source,
        destination: task.destination,
        files: task.files,
        options: options,
        callback: callback
    });

    if (!queueRunning) process.nextTick(runQueue);
}

function removeAdvanced (task, options, callback) {
    // task {destination, files:[{path, name}, *]}

    console.log('removeAdvanced');

    queue.push({
        action: 'unlink',
        destination: task.destination,
        files: task.files,
        options: options,
        callback: callback
    });

    if (!queueRunning) process.nextTick(runQueue);
}

function runQueue () {
    queueRunning = true;

    console.log('runQueue: starting with ' + queue.length + ' tasks.');

    while (queue.length) {
        console.log('runQueue: processing task');
        // {source, destination, files:[{path, name}, *], callback}
        var currentTask = queue.shift(); // grab a task

        /*--------------------[link]--------------------*/

        if (currentTask.action == 'link') {
            // get paths
            var sourcePath = path.normalize(currentTask.source);
            var destinationPath = path.normalize(currentTask.destination);
            var destinationDir = path.dirname(destinationPath);

            // validify
            var sourceValid = fs.existsSync(sourcePath);
            var destinationValid = fs.existsSync(destinationPath);

            if (!destinationValid) mkdirp.sync(destinationDir);

            // iterate
            for (var i = 0; i < currentTask.files.length; i++) {
                var currentFile = currentTask.files[i];

                var fileName = (currentFile.name) ? currentFile.name : currentFile.path;

                console.log('runQueue: processing file ' + fileName);

                // get paths
                var fileSourcePath = path.join(sourcePath, currentFile.path);
                var fileDestinationPath = path.join(destinationPath, fileName);

                // validify
                var fileSourceValid = fs.existsSync(fileSourcePath);
                var fileDestinationExisting = fs.existsSync(fileDestinationPath);

                if (fileSourceValid && fileDestinationExisting && fs.lstatSync(fileDestinationPath).isSymbolicLink()) { // symbolic link at destination
                    if (!currentTask.options.recreateSymbolicLinks) { // not allowed to unlink symbolic links
                        // process queue
                        console.error('runQueue: error - destination is symlink', fileDestinationPath);
                        currentTask.failed = true;
                        currentTask.callback(new Error('Could not create symbolic link. Destination is already a symbolic link.'), null);
                        break; // skip task
                    } else { // unlink symbolic link
                        fs.unlinkSync(fileDestinationPath);
                    }
                } else if (fileDestinationExisting) {
                    if (!currentTask.options.ignoreExisting) {
                        console.error('runQueue: error - file already existing', fileDestinationPath);
                        currentTask.failed = true;
                        currentTask.callback(new Error('Could not create symbolic link. Source file is invalid.'), null);
                        break; // skip task
                    } else {
                        continue; // with next file
                    }
                }
                if (fileSourceValid) {
                    var fileSourceStats = fs.lstatSync(fileSourcePath);

                    mkdirp.sync(path.dirname(fileDestinationPath)); // create destination directory

                    if (fileSourceStats.isDirectory()) {
                        fs.symlinkSync(fileSourcePath, fileDestinationPath, 'dir');
                    } else if (fileSourceStats.isFile()) {
                        fs.symlinkSync(fileSourcePath, fileDestinationPath, 'file');
                    } else {
                        console.error('runQueue: error - source neither file nor folder', fileSourcePath);
                        currentTask.failed = true;
                        currentTask.callback(new Error('Could not create symbolic link. Source is neither a file nor a folder.'), null);
                        break; // skip task
                    }
                } else {
                    console.error('runQueue: error - source file invalid', fileSourcePath);
                    currentTask.failed = true;
                    currentTask.callback(new Error('Could not create symbolic link. Source file is invalid.'), null);
                    break; // skip task
                }
            }
        }

        /*--------------------[unlink]--------------------*/

        else if (currentTask.action == 'unlink') {
            // get paths
            var destinationPath = path.normalize(currentTask.destination);
            var destinationValid = fs.existsSync(destinationPath);

            if (destinationValid) {
                for (var i = 0; i < currentTask.files.length; i++) {
                    var currentFile = currentTask.files[i];

                    var fileName = (currentFile.name) ? currentFile.name : currentFile.path;

                    console.log('runQueue: processing file ' + fileName);

                    // get paths
                    var filePath = path.join(destinationPath, fileName);

                    // validify
                    var fileExisting = fs.existsSync(filePath);

                    if (fileExisting && fs.lstatSync(filePath).isSymbolicLink()) { // symbolic link at destination
                        fs.unlinkSync(filePath);
                    } else if (!currentTask.options.ignoreMissingSymbolicLinks) {
                        console.error('runQueue: error - smbolic link not found', filePath);
                        currentTask.failed = true;
                        currentTask.callback(new Error('Could not unlink symbolic link. File not found.'), null);
                        break; // skip task
                    }
                }
            }

        } else {
            console.error('runQueue: error - unknown action', fileSourcePath);
            currentTask.failed = true;
            currentTask.callback(new Error('Invalid action.'), null);
        }

        if (!currentTask.failed) currentTask.callback(null, 'success');

        console.log('runQueue: finished processing task');
    }

    console.log('runQueue: finished all tasks.');

    queueRunning = false;
}

// exporting functions
exports.open = open;
exports.basic = basic;
exports.removeBasic = removeBasic;
exports.advanced = advanced;
exports.removeAdvanced = removeAdvanced;