nyc-bookstores/node_modules/ecstatic/lib/ecstatic.js
2013-05-27 13:45:59 -07:00

228 lines
6.0 KiB
JavaScript
Executable File

#! /usr/bin/env node
var path = require('path'),
fs = require('fs'),
url = require('url'),
mime = require('mime'),
showDir = require('./ecstatic/showdir'),
version = JSON.parse(
fs.readFileSync(__dirname + '/../package.json').toString()
).version,
status = require('./ecstatic/status-handlers'),
etag = require('./ecstatic/etag'),
optsParser = require('./ecstatic/opts');
var ecstatic = module.exports = function (dir, options) {
if (typeof dir !== 'string') {
options = dir;
dir = options.root;
}
var root = path.join(path.resolve(dir), '/'),
opts = optsParser(options),
cache = opts.cache,
autoIndex = opts.autoIndex,
baseDir = opts.baseDir,
defaultExt = opts.defaultExt;
opts.root = dir;
return function middleware (req, res, next) {
// Figure out the path for the file from the given url
var parsed = url.parse(req.url);
try {
var pathname = decodeURI(parsed.pathname);
}
catch (err) {
return status[400](res, next, { error: err });
}
var file = path.normalize(
path.join(root,
path.relative(
path.join('/', baseDir),
pathname
)
)
),
gzipped = file + '.gz';
// Set common headers.
res.setHeader('server', 'ecstatic-'+version);
// TODO: This check is broken, which causes the 403 on the
// expected 404.
if (file.slice(0, root.length) !== root) {
return status[403](res, next);
}
if (req.method && (req.method !== 'GET' && req.method !== 'HEAD' )) {
return status[405](res, next);
}
// Look for a gzipped file if this is turned on
if (opts.gzip && shouldCompress(req)) {
fs.stat(gzipped, function (err, stat) {
if (!err && stat.isFile()) {
file = gzipped;
return serve(stat);
}
});
}
fs.stat(file, function (err, stat) {
if (err && err.code === 'ENOENT') {
if (req.statusCode == 404) {
// This means we're already trying ./404.html
status[404](res, next);
}
else if (defaultExt && !path.extname(req.url).length) {
//
// If no file extension is specified and there is a default extension
// try that before rendering 404.html.
//
middleware({
url: req.url + '.' + defaultExt
}, res, next);
}
else {
// Try for ./404.html
middleware({
url: '/' + path.join(baseDir, '404.html'),
statusCode: 404 // Override the response status code
}, res, next);
}
}
else if (err) {
status[500](res, next, { error: err });
}
else if (stat.isDirectory()) {
// 302 to / if necessary
if (!pathname.match(/\/$/)) {
res.statusCode = 302;
res.setHeader('location', pathname + '/');
return res.end();
}
if (autoIndex) {
return middleware({
url: path.join(pathname, '/index.html')
}, res, function (err) {
if (err) {
return status[500](res, next, { error: err });
}
if (opts.showDir) {
return showDir(opts, stat)(req, res);
}
return status[403](res, next);
});
}
if (opts.showDir) {
return showDir(opts, stat)(req, res);
}
status[404](res, next);
}
else {
serve(stat);
}
});
function serve(stat) {
// TODO: Helper for this, with default headers.
res.setHeader('etag', etag(stat));
res.setHeader('last-modified', (new Date(stat.mtime)).toUTCString());
res.setHeader('cache-control', cache);
// Return a 304 if necessary
if ( req.headers
&& (
(req.headers['if-none-match'] === etag(stat))
|| (Date.parse(req.headers['if-modified-since']) >= stat.mtime)
)
) {
return status[304](res, next);
}
res.setHeader('content-length', stat.size);
// Do a MIME lookup, fall back to octet-stream and handle gzip
// special case.
var contentType = mime.lookup(file), charSet;
if (contentType) {
charSet = mime.charsets.lookup(contentType);
if (charSet) {
contentType += '; charset=' + charSet;
}
}
if (path.extname(file) === '.gz') {
res.setHeader('Content-Encoding', 'gzip');
// strip gz ending and lookup mime type
contentType = mime.lookup(path.basename(file, ".gz"));
}
res.setHeader('content-type', contentType || 'application/octet-stream');
if (req.method === "HEAD") {
res.statusCode = req.statusCode || 200; // overridden for 404's
return res.end();
}
var stream = fs.createReadStream(file);
stream.pipe(res);
stream.on('error', function (err) {
status['500'](res, next, { error: err });
});
stream.on('end', function () {
res.statusCode = 200;
res.end();
});
}
};
};
ecstatic.version = version;
ecstatic.showDir = showDir;
// Check to see if we should try to compress a file with gzip.
function shouldCompress(req) {
var headers = req.headers;
return headers && headers['accept-encoding'] &&
headers['accept-encoding']
.split(",")
.some(function (el) {
return ['*','compress', 'gzip', 'deflate'].indexOf(el) != -1;
})
;
};
if(!module.parent) {
var http = require('http'),
opts = require('optimist').argv,
port = opts.port || opts.p || 8000,
dir = opts.root || opts._[0] || process.cwd();
if(opts.help || opts.h) {
var u = console.error
u('usage: ecstatic [dir] {options} --port PORT')
u('see https://npm.im/ecstatic for more docs')
return
}
http.createServer(ecstatic(dir, opts))
.listen(port, function () {
console.log('ecstatic serving ' + dir + ' on port ' + port);
});
}