API Docs for: 0.10.0
Show:

File: lib/files.js

/**
 * Copyright (c) 2011, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * https://github.com/yui/yuidoc/blob/master/LICENSE
 */
'use strict';

YUI.add('files', function (Y) {

    /**
     * Ported fileutils methods from [Selleck](http://github.com/rgrove/selleck)
     * @class Files
     * @module yuidoc
     */

    Y.Files = {};

    /*
    Selleck
    Copyright (c) 2011 Yahoo! Inc.
    Licensed under the BSD License.
    */

    var fs = require('graceful-fs');
    var fsPath = require('path');
    var useFS = (fs.exists) ? fs : fsPath;


    function exists(file, cb) {
        if (cb) {
            useFS.exists(file, cb);
        } else {
            return useFS.existsSync(file);
        }
    }
    Y.Files.exists = exists;


    /**
    Copy a directory from one location to another
    @method copyDirectory
    @param {Path} source The source directory
    @param {Path} dest The destination directory
    @param {Boolean} [overwrite=false] Whether or not to overwrite destination files
        if they already exist.
    @param {Function} callback The callback to be executed when complete.
    **/
    function copyDirectory(source, dest, overwrite, callback) {
        // Allow callback as third arg.
        if (typeof overwrite === 'function') {
            callback = overwrite;
            overwrite = null;
        }

        fs.stat(source, afterSourceStat);

        function afterSourceStat(err, stats) {
            if (err) {
                return callback(err);
            }

            if (!stats.isDirectory()) {
                return callback(new Error('Source is not a directory: ' + source));
            }

            fs.lstat(dest, afterDestStat);
        }

        function afterDestStat(err, stats) {
            if (err && err.code !== 'ENOENT') {
                return callback(err);
            }

            if (stats) {
                // If the destination is a file or a link, either delete it or
                // bubble an error if overwrite isn't true.
                if (stats.isFile() || stats.isSymbolicLink()) {
                    if (overwrite) {
                        deletePath(dest); // TODO: make this async
                    } else {
                        callback(new Error('Destination already exists: ' + dest));
                        return undefined;
                    }
                }

                afterMkDir();
            } else {
                fs.mkdir(dest, '0755', afterMkDir);
            }
        }

        function afterMkDir(err) {
            if (err && err.code !== 'EEXIST') {
                return callback(err);
            }
            fs.readdir(source, afterReadDir);
        }

        function afterReadDir(err, files) {
            if (err) {
                return callback(err);
            }

            var pending = files.length,
                filename;

            if (!pending) {
                return callback();
            }

            while ((filename = files.shift())) {
                copyPath(fsPath.join(source, filename), fsPath.join(dest, filename), overwrite, function (copyPathErr) {
                    if (copyPathErr) {
                        return callback(copyPathErr);
                    }

                    pending -= 1;

                    if (!pending) {
                        callback();
                    }
                });
            }
        }
    }
    Y.Files.copyDirectory = copyDirectory;

    /**
    Copy a file from one location to another
    @method copyFile
    @param {Path} source The source file
    @param {Path} dest The destination file
    @param {Boolean} [overwrite=false] Whether or not to overwrite destination files
        if they already exist.
    @param {Callback} callback The callback to be executed when complete.
    @param {Error} callback.err The Error returned from Node
    **/
    function copyFile(source, dest, overwrite, callback) {
        // Allow callback as third arg.
        if (typeof overwrite === 'function') {
            callback = overwrite;
            overwrite = null;
        }

        fs.lstat(source, function (err, sourceStats) {
            if (err) {
                return callback(err);
            }

            if (!sourceStats.isFile()) {
                return callback(new Error('Source is not a file: ' + source));
            }

            fs.lstat(dest, function (destStatsErr, destStats) {
                var rs;

                if (destStatsErr && destStatsErr.code !== 'ENOENT') {
                    return callback(destStatsErr);
                }

                if (destStats) {
                    if (overwrite) {
                        deletePath(dest); // TODO: make this async
                    } else {
                        callback(new Error('Destination already exists: ' + dest));
                        return undefined;
                    }
                }

                rs = fs.createReadStream(source);
                rs.pipe(fs.createWriteStream(dest, {
                    mode: '0655'
                }));
                rs.on('end', callback);
            });
        });
    }
    Y.Files.copyFile = copyFile;


    /**
    If _source_ is a file, copies it to _dest_. If it's a directory, recursively
    copies it and all files and directories it contains to _dest_.

    Note that when attempting to copy a file into a directory, you should specify
    the full path to the new file (including the new filename). Otherwise, it will
    be interpreted as an attempt to copy the _source_ file *over* the _dest_
    directory instead of *into* it.

    Known issues:
    - Doesn't preserve ownership or permissions on copied files/directories.

    @method copyPath
    @param {String} source Source path.
    @param {String} dest Destination path.
    @param {Boolean} [overwrite=false] Whether or not to overwrite destination files
    if they already exist.
    @param {Callback} callback The callback to execute when completed.
    @param {Error} callback.err
    **/
    function copyPath(source, dest, overwrite, callback) {
        var sourceStats = statSync(source);

        // Allow callback as third arg.
        if (typeof overwrite === 'function') {
            callback = overwrite;
            overwrite = null;
        }

        if (!sourceStats) {
            callback(new Error('Source not found: ' + source));
            return;
        }

        if (sourceStats.isFile()) {
            copyFile(source, dest, overwrite, callback);
        } else if (sourceStats.isDirectory()) {
            copyDirectory(source, dest, overwrite, callback);
        } else {
            callback(new Error('Source is neither a file nor a directory: ' + source));
        }
    }
    Y.Files.copyPath = copyPath;

    // TODO: copySymbolicLink()?

    /**
    If _path_ is a file, deletes it. If _path_ is a directory, recursively deletes
    it and all files and directories it contains.

    This method is synchronous.

    @method deletePath
    @param {String} path File or directory to delete.
    **/
    function deletePath(path) {
        var stats = fs.lstatSync(path);

        if (stats.isFile() || stats.isSymbolicLink()) {
            fs.unlinkSync(path);
        } else if (stats.isDirectory()) {
            fs.readdirSync(path).forEach(function (filename) {
                deletePath(fsPath.join(path, filename));
            });

            fs.rmdirSync(path);
        }
    }
    Y.Files.deletePath = deletePath;


    /**
    Check to see if this is a directory

    @method isDirectory
    @param {Path} path The path to check
    @param {Boolean} [link=false] Also validate a symlink
    @return {Boolean} True if it is a directory
    **/
    function isDirectory(path, link) {
        var stat,
            result = false;

        link = (link === false) ? false : true;

        try {
            stat = fs.lstatSync(path);
            if (stat) {
                if (stat.isSymbolicLink() && link) {
                    stat = fs.statSync(path);
                }
                result = stat.isDirectory();
            }
        } catch (e) {
            result = false;
        }

        return result;
    }
    Y.Files.isDirectory = isDirectory;


    /**
    Check to see if this is a File

    @method isFile
    @param {Path} path The path to check
    @param {Boolean} [link=false] Also validate a symlink
    @return {Boolean} True if it is a file
    **/
    function isFile(path, link) {
        var stat,
            result = false;

        try {
            stat = fs.lstatSync(path);
            if (stat) {
                if (stat.isSymbolicLink() && link) {
                    stat = fs.statSync(path);
                }
                result = stat.isFile();
            }
        } catch (e) {
            result = false;
        }

        return result;
    }
    Y.Files.isFile = isFile;


    /**
    Check to see if this is a SymLink

    @method isSymbolicLink
    @param {Path} path The path to check
    @return {Boolean} True if it is a link
    **/
    function isSymbolicLink(path) {
        var stats = lstatSync(path);
        return stats ? stats.isSymbolicLink() : false;
    }
    Y.Files.isSymbolicLink = isSymbolicLink;


    /**
    Like `fs.lstatSync()`, but returns `null` instead of throwing when _path_
    doesn't exist. Will still throw on other types of errors.

    @method lstatSync
    @param {String} path Path to stat.
    @return {fs.Stats|null} `fs.Stats` object, or `null` if _path_ doesn't exist.
    **/
    function lstatSync(path) {
        try {
            return fs.lstatSync(path);
        } catch (ex) {
            if (ex.code === 'ENOENT') {
                return null;
            }

            throw ex;
        }
    }
    Y.Files.lstatSync = lstatSync;


    /**
    Like `fs.statSync()`, but returns `null` instead of throwing when _path_
    doesn't exist. Will still throw on other types of errors.

    @method statSync
    @param {String} path Path to stat.
    @return {fs.Stats|null} `fs.Stats` object, or `null` if _path_ doesn't exist.
    **/
    function statSync(path) {
        try {
            return fs.statSync(path);
        } catch (ex) {
            if (ex.code === 'ENOENT') {
                return null;
            }

            throw ex;
        }
    }
    Y.Files.statSync = statSync;

    /**
    Copy the theme assets directory

    @method copyAssets
    @param {Path} from The source directory
    @param {Path} dest The destination directory
    @param {Boolean} deleteFirst Should the directory be deleted if it exists
    @param {Function} callback The callback to be executed
    */
    function copyAssets() {
        var args = Array.prototype.slice.call(arguments),
            callback = args.pop(),
            from = args.shift(),
            to = args.shift(),
            deleteFirst = args.shift();

        if (from[0] === from[1]) {
            if (isDirectory(from[0])) {
                if (deleteFirst && isDirectory(to)) {
                    deletePath(to);
                }

                copyPath(from[0], to, true, callback);
            } else {
                callback();
            }
        } else {
            if (isDirectory(from[0])) {
                if (deleteFirst && isDirectory(to)) {
                    deletePath(to);
                }

                copyPath(from[0], to, true, function () {
                    if (isDirectory(from[1])) {
                        copyPath(from[1], to, true, callback);
                    } else {
                        callback();
                    }
                });
            } else {
                callback();
            }

        }
    }
    Y.Files.copyAssets = copyAssets;


    /**
    Helper method for getting JSON data from a local file

    @method getJSON
    @param {Path} filename The filename to parse JSON from
    @return {Object} The JSON data
    **/
    Y.Files.getJSON = function (filename) {
        var data = {};
        if (exists(filename)) {
            data = JSON.parse(fs.readFileSync(filename, 'utf8'));
        }
        return data;
    };

    /**
    Helper method for writing files to disk. It wraps the NodeJS file API

    @method writeFile
    @param {Path} file The filename to write to
    @param {String} data The data to write
    @param {Callback} callback*
    **/
    function writeFile(file, data, cb) {
        var out,
            args = arguments,
            flags = {
                flags: 'w',
                encoding: Y.charset,
                mode: '0644'
            };

        if (cb) {
            fs.writeFile(file, data, flags, function (err) {
                if (err && err.message.match(/^EMFILE, Too many open files/)) {
                    Y.log('Writefile failed, too many open files (' + args[0] + '). Trying again.', 'warn', 'files');
                    writeFile.timer++;
                    Y.later(writeFile.timer, Y, writeFile, args);
                    return;
                }
                cb();
            });
        } else {
            out = fs.createWriteStream(file, flags);
            out.write(data);
            out.end();
        }
    }
    writeFile.timer = 100;

    Y.Files.writeFile = writeFile;


    function readFile(file, enc, cb) {
        var args = arguments;
        fs.readFile(file, enc, function (err, data) {
            if (err && err.message.match(/^EMFILE, Too many open files/)) {
                Y.log('Readfile failed, too many open files (' + args[0] + '). Trying again.', 'warn', 'files');
                readFile.timer++;
                Y.later(readFile.timer, Y, readFile, args);
                return;
            }
            cb(err, data);
        });
    }
    readFile.timer = 100;

    Y.Files.readFile = readFile;

});