3 * Connect - staticProvider
4 * Copyright(c) 2010 Sencha Inc.
5 * Copyright(c) 2011 TJ Holowaychuk
10 * Module dependencies.
13 var fs = require('fs')
14 , path = require('path')
16 , basename = path.basename
17 , normalize = path.normalize
18 , utils = require('../utils')
19 , Buffer = require('buffer').Buffer
20 , parse = require('url').parse
21 , mime = require('mime');
24 * Static file server with the given `root` path.
28 * var oneDay = 86400000;
31 * connect.static(__dirname + '/public')
35 * connect.static(__dirname + '/public', { maxAge: oneDay })
40 * - `maxAge` Browser cache maxAge in milliseconds. defaults to 0
41 * - `hidden` Allow transfer of hidden files. defaults to false
43 * @param {String} root
44 * @param {Object} options
49 exports = module.exports = function static(root, options){
50 options = options || {};
53 if (!root) throw new Error('static() root path required');
56 return function static(req, res, next) {
57 options.path = req.url;
58 options.getOnly = true;
59 send(req, res, next, options);
70 * Respond with 416 "Requested Range Not Satisfiable"
72 * @param {ServerResponse} res
76 function invalidRange(res) {
77 var body = 'Requested Range Not Satisfiable';
78 res.setHeader('Content-Type', 'text/plain');
79 res.setHeader('Content-Length', body.length);
85 * Attempt to tranfer the requseted file to `res`.
87 * @param {ServerRequest}
88 * @param {ServerResponse}
89 * @param {Function} next
90 * @param {Object} options
94 var send = exports.send = function(req, res, next, options){
95 options = options || {};
96 if (!options.path) throw new Error('path required');
99 var maxAge = options.maxAge || 0
100 , ranges = req.headers.range
101 , head = 'HEAD' == req.method
102 , get = 'GET' == req.method
103 , root = options.root ? normalize(options.root) : null
104 , getOnly = options.getOnly
105 , fn = options.callback
106 , hidden = options.hidden
109 // replace next() with callback when available
112 // ignore non-GET requests
113 if (getOnly && !get && !head) return next();
116 var url = parse(options.path)
117 , path = decodeURIComponent(url.pathname)
121 if (~path.indexOf('\0')) return utils.badRequest(res);
123 // when root is not given, consider .. malicious
124 if (!root && ~path.indexOf('..')) return utils.forbidden(res);
126 // join / normalize from optional root dir
127 path = normalize(join(root, path));
130 if (root && 0 != path.indexOf(root)) return fn
131 ? fn(new Error('Forbidden'))
132 : utils.forbidden(res);
134 // index.html support
135 if ('/' == path[path.length - 1]) path += 'index.html';
138 if (!hidden && '.' == basename(path)[0]) return next();
140 fs.stat(path, function(err, stat){
142 type = mime.lookup(path);
146 if (fn) return fn(err);
147 return 'ENOENT' == err.code
150 // redirect directory in case index.html is present
151 } else if (stat.isDirectory()) {
152 res.statusCode = 301;
153 res.setHeader('Location', url.pathname + '/');
154 res.end('Redirecting to ' + url.pathname + '/');
160 // we have a Range request
162 ranges = utils.parseRange(stat.size, ranges);
165 // TODO: stream options
166 // TODO: multiple support
167 opts.start = ranges[0].start;
168 opts.end = ranges[0].end;
169 res.statusCode = 206;
170 res.setHeader('Content-Range', 'bytes '
179 ? fn(new Error('Requested Range Not Satisfiable'))
182 // stream the entire file
184 res.setHeader('Content-Length', stat.size);
185 if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
186 if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
187 if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
189 // conditional GET support
190 if (utils.conditionalGET(req)) {
191 if (!utils.modified(req, res)) {
192 return utils.notModified(res);
198 if (!res.getHeader('content-type')) {
199 var charset = mime.charsets.lookup(type);
200 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
202 res.setHeader('Accept-Ranges', 'bytes');
205 if (head) return res.end();
208 var stream = fs.createReadStream(path, opts);
213 function callback(err) { done || fn(err); done = true }
214 req.on('close', callback);
215 req.socket.on('error', callback);
216 stream.on('end', callback);