all files / modules/middleware/ file.js

83.05% Statements 49/59
72.34% Branches 34/47
87.5% Functions 7/8
83.05% Lines 49/59
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169                                                                                                                                                                                                                                                
const fs = require("fs");
const mach = require("../index");
const Promise = require("../utils/Promise");
const getFileStats = require("../utils/getFileStats");
const generateETag = require("../utils/generateETag");
const generateIndex = require("../utils/generateIndex");
const joinPaths = require("../utils/joinPaths");
 
const {is, contains} = require("ramda");
 
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.
 *
 *   let app = mach.file('/public');
 *   mach.serve(app);
 */
function file(app, options) {
  // Allow mach.file(path|options)
    Eif (typeof app === "string" || typeof app === "object") {
        options = app;
        app = null;
    }
 
    options = options || {};
 
  // Allow mach.file(path) and app.use(mach.file, path)
    Iif (is(String, options)) {
        options = {root: options};
    }
 
    const root = options.root;
    Iif (!is(String, root) || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
        throw new Error(`Invalid root directory: ${root}`);
    }
 
    let index = options.index || [];
    Eif (index) {
        if (typeof index === "string") {
            index = [index];
        } else Iif (!Array.isArray(index)) {
            index = ["index.html"];
        }
    }
 
    const useLastModified = "useLastModified" in options ? Boolean(options.useLastModified) : true;
    const useETag = Boolean(options.useETag);
 
    function sendFile(conn, path, stats) {
        conn.file({
            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) {
        Iif (conn.method !== "GET" && conn.method !== "HEAD") {
            return conn.call(app);
        }
 
        const pathname = conn.pathname;
 
    // Reject paths that contain "..".
        if (contains("..", pathname)) {
            return conn.text(403, "Forbidden");
        }
 
        const 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.
            const indexPaths = index.map(function (indexPath) {
                return joinPaths(path, indexPath);
            });
 
            return Promise.all(indexPaths.map(getFileStats)).then(function (stats) {
                for (let 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;