mjackson/mach

View on GitHub
modules/middleware/file.js

Summary

Maintainability
B
4 hrs
Test Coverage
var fs = require('fs');
var mach = require('../index');
var Promise = require('../utils/Promise');
var getFileStats = require('../utils/getFileStats');
var generateETag = require('../utils/generateETag');
var generateIndex = require('../utils/generateIndex');
var joinPaths = require('../utils/joinPaths');

mach.extend(
  require('../extensions/server')
);

/**
 * A middleware for serving files efficiently from the file system according
 * to the path specified in the `pathname` variable.
 *
 * Options may be any of the following:
 *
 * - root               The path to the root directory to serve files from
 * - index              An array of file names to try and serve when the
 *                      request targets a directory (e.g. ["index.html", "index.htm"]).
 *                      May simply be truthy to use ["index.html"]
 * - autoIndex          Set this true to automatically generate an index page
 *                      listing a directory's contents when the request targets
 *                      a directory with no index file
 * - useLastModified    Set this true to include the Last-Modified header
 *                      based on the mtime of the file. Defaults to true
 * - useETag            Set this true to include the ETag header based on
 *                      the MD5 checksum of the file. Defaults to false
 *
 * Alternatively, options may be a file path to the root directory.
 *
 * If a matching file cannot be found, the request is forwarded to the
 * downstream app. Otherwise, the file is streamed through to the response.
 *
 * Examples:
 *
 *   // Use the root directory name directly.
 *   app.use(mach.file, '/public');
 *
 *   // Serve static files out of /public, and automatically
 *   // serve an index.htm from any directory that has one.
 *   app.use(mach.file, {
 *     root: '/public',
 *     index: 'index.htm',
 *     useETag: true
 *   });
 *
 *   // Serve static files out of /public, and automatically
 *   // serve an index.html from any directory that has one.
 *   // Also, automatically generate a directory listing for
 *   // any directory without an index.html file.
 *   app.use(mach.file, {
 *     root: '/public',
 *     index: true,
 *     autoIndex: true
 *   });
 *
 * This function may also be used outside of the context of a middleware
 * stack to create a standalone app.
 *
 *   var app = mach.file('/public');
 *   mach.serve(app);
 */
function file(app, options) {
  // Allow mach.file(path|options)
  if (typeof app === 'string' || typeof app === 'object') {
    options = app;
    app = null;
  }

  options = options || {};

  // Allow mach.file(path) and app.use(mach.file, path)
  if (typeof options === 'string')
    options = { root: options };

  var root = options.root;
  if (typeof root !== 'string' || !fs.existsSync(root) || !fs.statSync(root).isDirectory())
    throw new Error('Invalid root directory: ' + root);

  var index = options.index || [];
  if (index) {
    if (typeof index === 'string') {
      index = [ index ];
    } else if (!Array.isArray(index)) {
      index = [ 'index.html' ];
    }
  }

  var useLastModified = ('useLastModified' in options) ? !!options.useLastModified : true;
  var useETag = !!options.useETag;

  function sendFile(conn, path, stats) {
    conn.file({
      path: path,
      size: stats.size
    });

    if (useLastModified)
      conn.response.headers['Last-Modified'] = stats.mtime.toUTCString();

    if (useETag) {
      return generateETag(path).then(function (etag) {
        conn.response.headers.ETag = etag;
      });
    }
  }

  return function (conn) {
    if (conn.method !== 'GET' && conn.method !== 'HEAD')
      return conn.call(app);

    var pathname = conn.pathname;

    // Reject paths that contain "..".
    if (pathname.indexOf('..') !== -1)
      return conn.text(403, 'Forbidden');

    var path = joinPaths(root, pathname);

    return getFileStats(path).then(function (stats) {
      if (stats && stats.isFile())
        return sendFile(conn, path, stats);

      if (!stats || !stats.isDirectory())
        return conn.call(app);

      // Try to serve one of the index files.
      var indexPaths = index.map(function (indexPath) {
        return joinPaths(path, indexPath);
      });

      return Promise.all(indexPaths.map(getFileStats)).then(function (stats) {
        for (var i = 0, len = stats.length; i < len; ++i)
          if (stats[i])
            return sendFile(conn, indexPaths[i], stats[i]);

        if (!options.autoIndex)
          return conn.call(app);

        // Redirect /images => /images/
        if (!(/\/$/).test(pathname))
          return conn.redirect(pathname + '/');

        // Automatically generate and serve an index file.
        return generateIndex(root, pathname, conn.basename).then(function (html) {
          conn.html(html);
        });
      });
    });
  };
}

module.exports = file;