3 * Connect - staticProvider
4 * Copyright(c) 2010 Sencha Inc.
5 * Copyright(c) 2011 TJ Holowaychuk
10 * Module dependencies.
13 var fs = require('fs')
14 , join = require('path').join
15 , utils = require('../utils')
16 , Buffer = require('buffer').Buffer
17 , parse = require('url').parse
18 , mime = require('mime');
21 * Static file server with the given `root` path.
25 * var oneDay = 86400000;
28 * connect.static(__dirname + '/public')
32 * connect.static(__dirname + '/public', { maxAge: oneDay })
37 * - `maxAge` Browser cache maxAge in milliseconds, defaults to 0
39 * @param {String} root
40 * @param {Object} options
45 exports = module.exports = function static(root, options){
46 options = options || {};
49 if (!root) throw new Error('static() root path required');
52 return function static(req, res, next) {
53 options.path = req.url;
54 send(req, res, next, options);
59 * Respond with 403 "Forbidden".
61 * @param {ServerResponse} res
65 function forbidden(res) {
66 var body = 'Forbidden';
67 res.setHeader('Content-Type', 'text/plain');
68 res.setHeader('Content-Length', body.length);
74 * Respond with 416 "Requested Range Not Satisfiable"
76 * @param {ServerResponse} res
80 function invalidRange(res) {
81 var body = 'Requested Range Not Satisfiable';
82 res.setHeader('Content-Type', 'text/plain');
83 res.setHeader('Content-Length', body.length);
89 * Attempt to tranfer the requseted file to `res`.
91 * @param {ServerRequest}
92 * @param {ServerResponse}
93 * @param {Function} next
94 * @param {Object} options
98 var send = exports.send = function(req, res, next, options){
99 options = options || {};
100 if (!options.path) throw new Error('path required');
103 var maxAge = options.maxAge || 0
104 , ranges = req.headers.range
105 , head = 'HEAD' == req.method
106 , root = options.root
107 , fn = options.callback;
109 // replace next() with callback when available
112 // ignore non-GET requests
113 if ('GET' != req.method && !head) return next();
116 var url = parse(options.path)
117 , path = decodeURIComponent(url.pathname)
120 // potentially malicious path
121 if (~path.indexOf('..')) return fn
122 ? fn(new Error('Forbidden'))
125 // join from optional root dir
126 path = join(options.root, path);
128 // index.html support
129 if ('/' == path[path.length - 1]) path += 'index.html';
132 type = mime.lookup(path);
134 fs.stat(path, function(err, stat){
137 if (fn) return fn(err);
138 return 'ENOENT' == err.code
141 // ignore directories
142 } else if (stat.isDirectory()) {
144 ? fn(new Error('Cannot Transfer Directory'))
148 // we have a Range request
150 ranges = utils.parseRange(stat.size, ranges);
153 // TODO: stream options
154 // TODO: multiple support
155 var stream = fs.createReadStream(path, ranges[0])
156 , start = ranges[0].start
157 , end = ranges[0].end;
158 res.statusCode = 206;
159 res.setHeader('Content-Range', 'bytes '
168 ? fn(new Error('Requested Range Not Satisfiable'))
171 // stream the entire file
173 res.setHeader('Content-Length', stat.size);
174 res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
175 res.setHeader('Last-Modified', stat.mtime.toUTCString());
176 res.setHeader('ETag', utils.etag(stat));
178 // conditional GET support
179 if (utils.conditionalGET(req)) {
180 if (!utils.modified(req, res)) {
181 return utils.notModified(res);
186 var stream = fs.createReadStream(path);
190 if (!res.getHeader('content-type')) {
191 var charset = mime.charsets.lookup(type);
192 res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
194 res.setHeader('Accept-Ranges', 'bytes');
196 if (head) return res.end();
199 res.connection.on('error', fn);
200 stream.on('end', fn);