Rory's Blog
Home
Blog
Projects
Facebook
Google+
Github
RSS

Publishing Script

04/14/15

Last time, I made a means of generating my index areas for my diary and blog. This is a considerable step towards DRYing up my blog, and now the ability to add what gets published to my index area is a clear next step in my javascripting enterprise.

Whenever I update how I generate or serve my index areas, I'll need to update how I save the posts I write, I think. Right now, it's pretty simple, I just need to move the post to the right directory, and add the filename to the correct "database" textfile list of posts.

Code Review

Without further ado, my publish.js:

/*jslint node: true */
(function () {
    'use strict';
    var args,
        usage,
        errorCount,
        publishCount,
        publishPosts;

    errorCount = 0;
    usage = function (err) {
        if (errorCount === 0) {
            console.log('USAGE: publish <area> <file> [<file>] [...]');
            console.log('    area: diary or blog');
            console.log('    file: html file containing post to publish');
            console.log();
        }

        if (err) {
            console.log('Error: ' + err);
        }

        errorCount += 1;
        return false;
    };

    args = (function () {
        var processArgs,
            area,
            validAreas,
            files;

        processArgs = process.argv.slice(2);

        if (processArgs.length < 2) {
            return usage();
        }

        area = processArgs[0];
        validAreas = ['diary', 'blog', 'test'];
        if (validAreas.indexOf(area) === -1) {
            return usage('area must be diary or blog');
        }

        files = processArgs.slice(1);

        return {
            area: area,
            files: files
        };
    }());

    if (!args) {
        return;
    }

    publishPosts = function (area, filenames) {
        var path,
            database,
            fs,
            publishedPosts;

        path = require('path');
        database = path.join('database', area + '.txt');
        fs = require('fs');

        // Move posts to published area
        filenames.forEach(function (filename) {
            var publishedPath;

            publishedPath = path.join(
                'public',
                area,
                path.basename(filename)
            );
            fs.rename(filename, publishedPath, function (err) {
                if (err) {
                    console.log(
                        'Error moving ' + filename  + ' to ' + area
                    );
                }
            });
        });

        publishedPosts = '\n'
            + filenames.map(function (filename) {
                return path.basename(filename);
            }).join('\n');

        // Add to the database of published posts
        fs.appendFile(database, publishedPosts, 'utf-8', function (err) {
            if (err) {
                return console.log('Failed to publish to ' + area);
            }
        });
    };

    publishCount = 0;
    args.files.forEach(function (filename) {
        var fs;

        fs = require('fs');
        fs.readFile(filename, 'utf-8', function (err, data) {
            if (err) {
                return usage('could not open ' + filename);
            }

            // Verify no empty strings in file to be published
            if (data.indexOf('""') !== -1) {
                return usage('Empty string "" in ' + filename);
            }

            // TODO: Verify there's actually a post to publish

            publishCount += 1;
            if (publishCount === args.files.length) {
                publishPosts(args.area, args.files);
            }
        });
    });
}());

Oh, look, actually taking command-line arguments. When was the last time I did that? Let's break down what this code does.

args = (function () {
    var processArgs,
        area,
        validAreas,
        files;

    processArgs = process.argv.slice(2);

    if (processArgs.length < 2) {
        return usage();
    }

    area = processArgs[0];
    validAreas = ['diary', 'blog', 'test'];
    if (validAreas.indexOf(area) === -1) {
        return usage('area must be diary or blog');
    }

    files = processArgs.slice(1);

    return {
        area: area,
        files: files
    };
}());

if (!args) {
    return;
}

Argument handling is still something I'm experimenting with in node javascript. I'm sure there's a good node library out there, but it doesn't seem like there's a definitive one, so I'll play with the built-in stuff for a bit until I know what I'm looking for.

Here I grab the arguments fed into the script (process.argv's first two values is "node" and the script being called).

Now for this script I want at least two arguments: the area I'm publishing to, and any number of filepaths leading to a file that I will be publishing to that area.

Now, there's a few things I can check the validity of right away, like whether or not the area the script is trying to publish to is valid. I check that and return a usage if it is not correct.

Then I split the args further into specific, well-named variables and return an object containing said named variables.

errorCount = 0;
usage = function (err) {
    if (errorCount === 0) {
        console.log('USAGE: publish <area> <file> [<file>] [...]');
        console.log('    area: diary or blog');
        console.log('    file: html file containing post to publish');
        console.log();
    }

    if (err) {
        console.log('Error: ' + err);
    }

    errorCount += 1;
    return false;
};

Here is my usage printout. I use a closure to track how many times the function has been called. Really, it's just so I only print out the usage string once, and then I can also print out any errors I come across by calling this usage, and not reprint the usage over and over.

I return false with this function because I figure if I'm going to return usage elsewhere, I shouldn't let it just be an undefined.

publishCount = 0;
args.files.forEach(function (filename) {
    var fs;

    fs = require('fs');
    fs.readFile(filename, 'utf-8', function (err, data) {
        if (err) {
            return usage('could not open ' + filename);
        }

        // Verify no empty strings in file to be published
        if (data.indexOf('""') !== -1) {
            return usage('Empty string "" in ' + filename);
        }

        // TODO: Verify there's actually a post to publish

        publishCount += 1;
        if (publishCount === args.files.length) {
            publishPosts(args.area, args.files);
        }
    });
});

I now do an asynchronous iteration over the filenames input, read each file, and if the file passes all of my (admittedly a work-in-progress) tests, I increment the closure publishCount and if all input files have been verified, I execute the process to publish the input files.

publishPosts = function (area, filenames) {
    var path,
        database,
        fs,
        publishedPosts;

    path = require('path');
    database = path.join('database', area + '.txt');
    fs = require('fs');

    // Move posts to published area
    filenames.forEach(function (filename) {
        var publishedPath;

        publishedPath = path.join(
            'public',
            area,
            path.basename(filename)
        );
        fs.rename(filename, publishedPath, function (err) {
            if (err) {
                console.log(
                    'Error moving ' + filename  + ' to ' + area
                );
            }
        });
    });

    publishedPosts = '\n'
        + filenames.map(function (filename) {
            return path.basename(filename);
        }).join('\n');

    // Add to the database of published posts
    fs.appendFile(database, publishedPosts, 'utf-8', function (err) {
        if (err) {
            return console.log('Failed to publish to ' + area);
        }
    });
};

This code is the longest bit, and does the actual work, but I feel it's pretty straightforward what it does. It moves each file to the directory where published posts live, and adds each base filename to the relevant area's "database" (appends the basename of the file to the end of the file).

Overall, it's a simple bit of code that I tried to build good feedback of bad input or something wrong going on with filesystem interactions. I also believe that it should be pretty straightforward to iterate on this script to better check for silly things wrong with my posts, maybe even add better additional mark-up to my code things (color-coding would be pretty cool), and upgrade the whole thing to use a proper database.

It's getting to be about time to actually go to my server, and setup things so content is actually being served through node, and that it restarts when the system restarts, and also when new server code is pushed.

I believe I've read a tutorial devoted to just such a thing. Or maybe it's the simple guide predecessor I'm thinking of. Either way, I should probably start setting up that system locally, get it working, set it up on my server, and make the transition.

It might also be prudent to explore my options further. I mean, I've used nodemon, is using grunt over that... ideal? Does grunt use that and other stuff?

I suppose I'll get back to you on that.