Initial import
[darksolar] / node_modules / express / node_modules / connect / lib / middleware / static.js
1
2 /*!
3  * Connect - staticProvider
4  * Copyright(c) 2010 Sencha Inc.
5  * Copyright(c) 2011 TJ Holowaychuk
6  * MIT Licensed
7  */
8
9 /**
10  * Module dependencies.
11  */
12
13 var fs = require('fs')
14   , path = require('path')
15   , join = path.join
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');
22
23 /**
24  * Static file server with the given `root` path.
25  *
26  * Examples:
27  *
28  *     var oneDay = 86400000;
29  *
30  *     connect(
31  *       connect.static(__dirname + '/public')
32  *     ).listen(3000);
33  *
34  *     connect(
35  *       connect.static(__dirname + '/public', { maxAge: oneDay })
36  *     ).listen(3000);
37  *
38  * Options:
39  *
40  *    - `maxAge`   Browser cache maxAge in milliseconds. defaults to 0
41  *    - `hidden`   Allow transfer of hidden files. defaults to false
42  *    - `redirect`   Redirect to trailing "/" when the pathname is a dir
43  *
44  * @param {String} root
45  * @param {Object} options
46  * @return {Function}
47  * @api public
48  */
49
50 exports = module.exports = function static(root, options){
51   options = options || {};
52
53   // root required
54   if (!root) throw new Error('static() root path required');
55   options.root = root;
56
57   return function static(req, res, next) {
58     options.path = req.url;
59     options.getOnly = true;
60     send(req, res, next, options);
61   };
62 };
63
64 /**
65  * Expose mime module.
66  */
67
68 exports.mime = mime;
69
70 /**
71  * Respond with 416  "Requested Range Not Satisfiable"
72  *
73  * @param {ServerResponse} res
74  * @api private
75  */
76
77 function invalidRange(res) {
78   var body = 'Requested Range Not Satisfiable';
79   res.setHeader('Content-Type', 'text/plain');
80   res.setHeader('Content-Length', body.length);
81   res.statusCode = 416;
82   res.end(body);
83 }
84
85 /**
86  * Attempt to tranfer the requseted file to `res`.
87  *
88  * @param {ServerRequest}
89  * @param {ServerResponse}
90  * @param {Function} next
91  * @param {Object} options
92  * @api private
93  */
94
95 var send = exports.send = function(req, res, next, options){
96   options = options || {};
97   if (!options.path) throw new Error('path required');
98
99   // setup
100   var maxAge = options.maxAge || 0
101     , ranges = req.headers.range
102     , head = 'HEAD' == req.method
103     , get = 'GET' == req.method
104     , root = options.root ? normalize(options.root) : null
105     , redirect = false === options.redirect ? false : true
106     , getOnly = options.getOnly
107     , fn = options.callback
108     , hidden = options.hidden
109     , done;
110
111   // replace next() with callback when available
112   if (fn) next = fn;
113
114   // ignore non-GET requests
115   if (getOnly && !get && !head) return next();
116
117   // parse url
118   var url = parse(options.path)
119     , path = decodeURIComponent(url.pathname)
120     , type;
121
122   // null byte(s)
123   if (~path.indexOf('\0')) return utils.badRequest(res);
124
125   // when root is not given, consider .. malicious
126   if (!root && ~path.indexOf('..')) return utils.forbidden(res);
127
128   // join / normalize from optional root dir
129   path = normalize(join(root, path));
130
131   // malicious path
132   if (root && 0 != path.indexOf(root)) return fn
133     ? fn(new Error('Forbidden'))
134     : utils.forbidden(res);
135
136   // index.html support
137   if (normalize('/') == path[path.length - 1]) path += 'index.html';
138
139   // "hidden" file
140   if (!hidden && '.' == basename(path)[0]) return next();
141
142   fs.stat(path, function(err, stat){
143     // mime type
144     type = mime.lookup(path);
145
146     // ignore ENOENT
147     if (err) {
148       if (fn) return fn(err);
149       return 'ENOENT' == err.code
150         ? next()
151         : next(err);
152     // redirect directory in case index.html is present
153     } else if (stat.isDirectory()) {
154       if (!redirect) return next();
155       res.statusCode = 301;
156       res.setHeader('Location', url.pathname + '/');
157       res.end('Redirecting to ' + url.pathname + '/');
158       return;
159     }
160
161     // header fields
162     if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
163     if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
164     if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
165     if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
166     if (!res.getHeader('content-type')) {
167       var charset = mime.charsets.lookup(type);
168       res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
169     }
170     res.setHeader('Accept-Ranges', 'bytes');
171
172     // conditional GET support
173     if (utils.conditionalGET(req)) {
174       if (!utils.modified(req, res)) {
175         req.emit('static');
176         return utils.notModified(res);
177       }
178     }
179
180     var opts = {};
181     var chunkSize = stat.size;
182
183     // we have a Range request
184     if (ranges) {
185       ranges = utils.parseRange(stat.size, ranges);
186       // valid
187       if (ranges) {
188         // TODO: stream options
189         // TODO: multiple support
190         opts.start = ranges[0].start;
191         opts.end = ranges[0].end;
192         chunkSize = opts.end - opts.start + 1;
193         res.statusCode = 206;
194         res.setHeader('Content-Range', 'bytes '
195           + opts.start
196           + '-'
197           + opts.end
198           + '/'
199           + stat.size);
200       // invalid
201       } else {
202         return fn
203           ? fn(new Error('Requested Range Not Satisfiable'))
204           : invalidRange(res);
205       }
206     }
207
208     res.setHeader('Content-Length', chunkSize);
209
210     // transfer
211     if (head) return res.end();
212
213     // stream
214     var stream = fs.createReadStream(path, opts);
215     req.emit('static', stream);
216     stream.pipe(res);
217
218     // callback
219     if (fn) {
220       function callback(err) { done || fn(err); done = true }
221       req.on('close', callback);
222       stream.on('end', callback);
223     }
224   });
225 };