You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
5.6 KiB
225 lines
5.6 KiB
|
|
/*! |
|
* Connect - staticProvider |
|
* Copyright(c) 2010 Sencha Inc. |
|
* Copyright(c) 2011 TJ Holowaychuk |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var fs = require('fs') |
|
, path = require('path') |
|
, join = path.join |
|
, basename = path.basename |
|
, normalize = path.normalize |
|
, utils = require('../utils') |
|
, Buffer = require('buffer').Buffer |
|
, parse = require('url').parse |
|
, mime = require('mime'); |
|
|
|
/** |
|
* Static file server with the given `root` path. |
|
* |
|
* Examples: |
|
* |
|
* var oneDay = 86400000; |
|
* |
|
* connect( |
|
* connect.static(__dirname + '/public') |
|
* ).listen(3000); |
|
* |
|
* connect( |
|
* connect.static(__dirname + '/public', { maxAge: oneDay }) |
|
* ).listen(3000); |
|
* |
|
* Options: |
|
* |
|
* - `maxAge` Browser cache maxAge in milliseconds. defaults to 0 |
|
* - `hidden` Allow transfer of hidden files. defaults to false |
|
* - `redirect` Redirect to trailing "/" when the pathname is a dir |
|
* |
|
* @param {String} root |
|
* @param {Object} options |
|
* @return {Function} |
|
* @api public |
|
*/ |
|
|
|
exports = module.exports = function static(root, options){ |
|
options = options || {}; |
|
|
|
// root required |
|
if (!root) throw new Error('static() root path required'); |
|
options.root = root; |
|
|
|
return function static(req, res, next) { |
|
options.path = req.url; |
|
options.getOnly = true; |
|
send(req, res, next, options); |
|
}; |
|
}; |
|
|
|
/** |
|
* Expose mime module. |
|
*/ |
|
|
|
exports.mime = mime; |
|
|
|
/** |
|
* Respond with 416 "Requested Range Not Satisfiable" |
|
* |
|
* @param {ServerResponse} res |
|
* @api private |
|
*/ |
|
|
|
function invalidRange(res) { |
|
var body = 'Requested Range Not Satisfiable'; |
|
res.setHeader('Content-Type', 'text/plain'); |
|
res.setHeader('Content-Length', body.length); |
|
res.statusCode = 416; |
|
res.end(body); |
|
} |
|
|
|
/** |
|
* Attempt to tranfer the requseted file to `res`. |
|
* |
|
* @param {ServerRequest} |
|
* @param {ServerResponse} |
|
* @param {Function} next |
|
* @param {Object} options |
|
* @api private |
|
*/ |
|
|
|
var send = exports.send = function(req, res, next, options){ |
|
options = options || {}; |
|
if (!options.path) throw new Error('path required'); |
|
|
|
// setup |
|
var maxAge = options.maxAge || 0 |
|
, ranges = req.headers.range |
|
, head = 'HEAD' == req.method |
|
, get = 'GET' == req.method |
|
, root = options.root ? normalize(options.root) : null |
|
, redirect = false === options.redirect ? false : true |
|
, getOnly = options.getOnly |
|
, fn = options.callback |
|
, hidden = options.hidden |
|
, done; |
|
|
|
// replace next() with callback when available |
|
if (fn) next = fn; |
|
|
|
// ignore non-GET requests |
|
if (getOnly && !get && !head) return next(); |
|
|
|
// parse url |
|
var url = parse(options.path) |
|
, path = decodeURIComponent(url.pathname) |
|
, type; |
|
|
|
// null byte(s) |
|
if (~path.indexOf('\0')) return utils.badRequest(res); |
|
|
|
// when root is not given, consider .. malicious |
|
if (!root && ~path.indexOf('..')) return utils.forbidden(res); |
|
|
|
// join / normalize from optional root dir |
|
path = normalize(join(root, path)); |
|
|
|
// malicious path |
|
if (root && 0 != path.indexOf(root)) return fn |
|
? fn(new Error('Forbidden')) |
|
: utils.forbidden(res); |
|
|
|
// index.html support |
|
if (normalize('/') == path[path.length - 1]) path += 'index.html'; |
|
|
|
// "hidden" file |
|
if (!hidden && '.' == basename(path)[0]) return next(); |
|
|
|
fs.stat(path, function(err, stat){ |
|
// mime type |
|
type = mime.lookup(path); |
|
|
|
// ignore ENOENT |
|
if (err) { |
|
if (fn) return fn(err); |
|
return 'ENOENT' == err.code |
|
? next() |
|
: next(err); |
|
// redirect directory in case index.html is present |
|
} else if (stat.isDirectory()) { |
|
if (!redirect) return next(); |
|
res.statusCode = 301; |
|
res.setHeader('Location', url.pathname + '/'); |
|
res.end('Redirecting to ' + url.pathname + '/'); |
|
return; |
|
} |
|
|
|
// header fields |
|
if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); |
|
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000)); |
|
if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); |
|
if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat)); |
|
if (!res.getHeader('content-type')) { |
|
var charset = mime.charsets.lookup(type); |
|
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); |
|
} |
|
res.setHeader('Accept-Ranges', 'bytes'); |
|
|
|
// conditional GET support |
|
if (utils.conditionalGET(req)) { |
|
if (!utils.modified(req, res)) { |
|
req.emit('static'); |
|
return utils.notModified(res); |
|
} |
|
} |
|
|
|
var opts = {}; |
|
var chunkSize = stat.size; |
|
|
|
// we have a Range request |
|
if (ranges) { |
|
ranges = utils.parseRange(stat.size, ranges); |
|
// valid |
|
if (ranges) { |
|
// TODO: stream options |
|
// TODO: multiple support |
|
opts.start = ranges[0].start; |
|
opts.end = ranges[0].end; |
|
chunkSize = opts.end - opts.start + 1; |
|
res.statusCode = 206; |
|
res.setHeader('Content-Range', 'bytes ' |
|
+ opts.start |
|
+ '-' |
|
+ opts.end |
|
+ '/' |
|
+ stat.size); |
|
// invalid |
|
} else { |
|
return fn |
|
? fn(new Error('Requested Range Not Satisfiable')) |
|
: invalidRange(res); |
|
} |
|
} |
|
|
|
res.setHeader('Content-Length', chunkSize); |
|
|
|
// transfer |
|
if (head) return res.end(); |
|
|
|
// stream |
|
var stream = fs.createReadStream(path, opts); |
|
req.emit('static', stream); |
|
stream.pipe(res); |
|
|
|
// callback |
|
if (fn) { |
|
function callback(err) { done || fn(err); done = true } |
|
req.on('close', callback); |
|
stream.on('end', callback); |
|
} |
|
}); |
|
};
|
|
|