3 * Copyright(c) 2010 Sencha Inc.
4 * Copyright(c) 2011 TJ Holowaychuk
5 * Copyright(c) 2014-2015 Douglas Christopher Wilson
12 * Module dependencies.
16 var escapeHtml = require('escape-html')
17 var parseUrl = require('parseurl')
18 var resolve = require('path').resolve
19 var send = require('send')
20 var url = require('url')
27 module.exports = serveStatic
28 module.exports.mime = send.mime
31 * @param {string} root
32 * @param {object} [options]
37 function serveStatic (root, options) {
39 throw new TypeError('root path required')
42 if (typeof root !== 'string') {
43 throw new TypeError('root path must be a string')
46 // copy options object
47 var opts = Object.create(options || null)
50 var fallthrough = opts.fallthrough !== false
53 var redirect = opts.redirect !== false
56 var setHeaders = opts.setHeaders
58 if (setHeaders && typeof setHeaders !== 'function') {
59 throw new TypeError('option setHeaders must be function')
62 // setup options for send
63 opts.maxage = opts.maxage || opts.maxAge || 0
64 opts.root = resolve(root)
66 // construct directory listener
67 var onDirectory = redirect
68 ? createRedirectDirectoryListener()
69 : createNotFoundDirectoryListener()
71 return function serveStatic (req, res, next) {
72 if (req.method !== 'GET' && req.method !== 'HEAD') {
79 res.setHeader('Allow', 'GET, HEAD')
80 res.setHeader('Content-Length', '0')
85 var forwardError = !fallthrough
86 var originalUrl = parseUrl.original(req)
87 var path = parseUrl(req).pathname
89 // make sure redirect occurs at mount
90 if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
95 var stream = send(req, path, opts)
97 // add directory handler
98 stream.on('directory', onDirectory)
100 // add headers listener
102 stream.on('headers', setHeaders)
105 // add file listener for fallthrough
107 stream.on('file', function onFile () {
108 // once file is determined, always forward error
114 stream.on('error', function error (err) {
115 if (forwardError || !(err.statusCode < 500)) {
129 * Collapse all leading slashes into a single slash
132 function collapseLeadingSlashes (str) {
133 for (var i = 0; i < str.length; i++) {
134 if (str[i] !== '/') {
140 ? '/' + str.substr(i)
145 * Create a directory listener that just 404s.
149 function createNotFoundDirectoryListener () {
150 return function notFound () {
156 * Create a directory listener that performs a redirect.
160 function createRedirectDirectoryListener () {
161 return function redirect () {
162 if (this.hasTrailingSlash()) {
168 var originalUrl = parseUrl.original(this.req)
170 // append trailing slash
171 originalUrl.path = null
172 originalUrl.pathname = collapseLeadingSlashes(originalUrl.pathname + '/')
175 var loc = url.format(originalUrl)
176 var msg = 'Redirecting to <a href="' + escapeHtml(loc) + '">' + escapeHtml(loc) + '</a>\n'
179 // send redirect response
181 res.setHeader('Content-Type', 'text/html; charset=UTF-8')
182 res.setHeader('Content-Length', Buffer.byteLength(msg))
183 res.setHeader('X-Content-Type-Options', 'nosniff')
184 res.setHeader('Location', loc)