Comparing version 0.1.7 to 0.3.0
var express = require('express'); | ||
var ecstatic = require('../lib/ecstatic'); | ||
var http = require('http'); | ||
var app = express.createServer(); | ||
var app = express(); | ||
app.use(ecstatic(__dirname + '/public', { showdir : true })); | ||
app.listen(8080); | ||
http.createServer(app).listen(8080); | ||
console.log('Listening on :8080'); |
@@ -14,2 +14,7 @@ var path = require('path'), | ||
var ecstatic = module.exports = function (dir, options) { | ||
if (typeof dir !== 'string') { | ||
options = dir; | ||
dir = options.root; | ||
} | ||
var root = path.join(path.resolve(dir), '/'), | ||
@@ -19,4 +24,7 @@ opts = optsParser(options), | ||
autoIndex = opts.autoIndex, | ||
baseDir = opts.baseDir, | ||
defaultExt = opts.defaultExt; | ||
opts.root = dir; | ||
return function middleware (req, res, next) { | ||
@@ -27,3 +35,11 @@ | ||
pathname = decodeURI(parsed.pathname), | ||
file = path.normalize(path.join(root, pathname)); | ||
file = path.normalize( | ||
path.join(root, | ||
path.relative( | ||
path.join('/', baseDir), | ||
pathname | ||
) | ||
) | ||
), | ||
gzipped = file + '.gz'; | ||
@@ -33,2 +49,4 @@ // Set common headers. | ||
// TODO: This check is broken, which causes the 403 on the | ||
// expected 404. | ||
if (file.slice(0, root.length) !== root) { | ||
@@ -42,5 +60,14 @@ return status[403](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) { | ||
@@ -50,15 +77,2 @@ // This means we're already trying ./404.html | ||
} | ||
else if(req.showDir) { | ||
// In this case, we were probably attempting to autoindex with | ||
// 'index.html' and it didn't work. This should prompt the | ||
// "showdir" function, which should've been set to `next`. | ||
// TODO: Re-evaluate this dependence on recursion. Could the confusion | ||
// introduced be eliminated? | ||
// TODO: We're attaching this random property to req to make it work, | ||
// which is BAD FORM. This *needs* a refactor but I think making it | ||
// not broken is the lesser of two evils. | ||
// NOTE: Alternate check here was: | ||
// `path.basename(req.url) === 'index.html' && autoIndex | ||
next(); | ||
} | ||
else if (defaultExt && !path.extname(req.url).length) { | ||
@@ -76,3 +90,3 @@ // | ||
middleware({ | ||
url: '/404.html', | ||
url: '/' + path.join(baseDir, '404.html'), | ||
statusCode: 404 // Override the response status code | ||
@@ -86,59 +100,90 @@ }, res, next); | ||
else if (stat.isDirectory()) { | ||
// 302 to / if necessary | ||
if (!pathname.match(/\/$/)) { | ||
res.statusCode = 302; | ||
res.setHeader('location', pathname + '/'); | ||
return res.end(); | ||
} | ||
// retry for the index.html, if that's not there fall back to the | ||
// directory view (if activated) | ||
var handler = (typeof next === 'function' && !autoIndex) | ||
? next | ||
: function () { | ||
showDir(root, pathname, stat, cache)(req, res); | ||
}; | ||
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) { | ||
showDir(opts, stat)(req, res); | ||
} | ||
}); | ||
} | ||
middleware({ | ||
url: path.join(pathname, '/index.html'), | ||
showDir: true | ||
}, res, handler); | ||
if (opts.showDir) { | ||
return showDir(opts, stat)(req, res); | ||
} | ||
status[404](res, next); | ||
} | ||
else { | ||
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', 'max-age='+cache); | ||
function serve(stat) { | ||
// Return a 304 if necessary | ||
if ( req.headers | ||
&& ( (req.headers['if-none-match'] === etag(stat)) | ||
|| (Date.parse(req.headers['if-none-match']) >= stat.mtime ) | ||
) | ||
) { | ||
status[304](res, next); | ||
// 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', 'max-age='+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; | ||
} | ||
else { | ||
} | ||
res.setHeader( | ||
'content-type', | ||
mime.lookup(file) || 'application/octet-stream' | ||
); | ||
if (path.extname(file) === '.gz') { | ||
res.setHeader('Content-Encoding', 'gzip'); | ||
if (req.method === "HEAD") { | ||
res.statusCode = req.statusCode || 200; // overridden for 404's | ||
res.end(); | ||
} | ||
else { | ||
// strip gz ending and lookup mime type | ||
contentType = mime.lookup(path.basename(file, ".gz")); | ||
} | ||
var stream = fs.createReadStream(file); | ||
res.setHeader('content-type', contentType || 'application/octet-stream'); | ||
stream.pipe(res); | ||
stream.on('error', function (err) { | ||
status['500'](res, next, { error: err }); | ||
}); | ||
if (req.method === "HEAD") { | ||
res.statusCode = req.statusCode || 200; // overridden for 404's | ||
return res.end(); | ||
} | ||
stream.on('end', function () { | ||
res.statusCode = 200; | ||
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(); | ||
}); | ||
} | ||
}; | ||
@@ -150,1 +195,14 @@ }; | ||
// 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; | ||
}) | ||
; | ||
}; | ||
@@ -7,13 +7,16 @@ // This is so you can have options aliasing and defaults in one place. | ||
|| [ | ||
'showDir', | ||
'showdir', | ||
'autoIndex', | ||
'autoindex' | ||
].some(function (k) { | ||
// at least one of the flags is truthy. | ||
// This means that, in a conflict, showing the directory wins. | ||
// Not sure if this is the right behavior or not. | ||
return opts[k]; | ||
}); | ||
var showDir = !opts | ||
|| [ | ||
'showDir', | ||
'showdir' | ||
].some(function (k) { | ||
return opts[k]; | ||
}); | ||
var defaultExt; | ||
@@ -33,4 +36,6 @@ | ||
autoIndex: autoIndex, | ||
defaultExt: defaultExt | ||
showDir: showDir, | ||
defaultExt: defaultExt, | ||
baseDir: (opts && opts.baseDir) || '/' | ||
} | ||
} |
@@ -9,22 +9,22 @@ var ecstatic = require('../ecstatic'), | ||
module.exports = function (dir, pathname, stat, cache) { | ||
var root = path.resolve(dir) + '/'; | ||
module.exports = function (opts, stat) { | ||
var cache = opts.cache || 3600, | ||
root = path.resolve(opts.root), | ||
baseDir = opts.baseDir || '/'; | ||
return function (req, res, next) { | ||
return function middleware (req, res, next) { | ||
if (typeof pathname === 'undefined') { | ||
pathname = url.parse(req.url).pathname; | ||
} | ||
// Figure out the path for the file from the given url | ||
var parsed = url.parse(req.url), | ||
pathname = decodeURI(parsed.pathname), | ||
dir = path.normalize( | ||
path.join(root, | ||
path.relative( | ||
path.join('/', baseDir), | ||
pathname | ||
) | ||
) | ||
); | ||
if (typeof file === 'undefined') { | ||
file = path.normalize(path.join(root, pathname)); | ||
} | ||
if (typeof cache === 'undefined') { | ||
cache = 3600; | ||
} | ||
(function (cb) { | ||
fs.stat(file, cb); | ||
})(function (err, stat) { | ||
fs.stat(dir, function (err, stat) { | ||
if (err) { | ||
@@ -34,7 +34,6 @@ return status[500](res, next, { error: err }); | ||
fs.readdir(dir + pathname, function (err, files) { | ||
fs.readdir(dir, function (err, files) { | ||
if (err) { | ||
return status[500](res, next, { error: err }); | ||
} | ||
res.setHeader('content-type', 'text/html'); | ||
@@ -45,3 +44,22 @@ res.setHeader('etag', etag(stat)); | ||
var sortByIsDirectory = function (paths, cb) { | ||
sortByIsDirectory(files, function (errs, dirs, files) { | ||
if (errs.length > 0) { | ||
return status[500](res, next, { error: errs[0] }); | ||
} | ||
// if it makes sense to, add a .. link | ||
if (path.resolve(dir, '..').slice(0, root.length) == root) { | ||
return fs.stat(path.join(dir, '..'), function (err, s) { | ||
if (err) { | ||
return status[500](res, next, { error: err }); | ||
} | ||
dirs.unshift([ '..', s ]); | ||
render(dirs, files); | ||
}); | ||
} | ||
render(dirs, files); | ||
}); | ||
function sortByIsDirectory(paths, cb) { | ||
var pending = paths.length, | ||
@@ -52,4 +70,8 @@ errs = [], | ||
if (!pending) { | ||
return cb(errs, dirs, files); | ||
} | ||
paths.forEach(function (file) { | ||
fs.stat(dir + pathname + '/' + file, function (err, s) { | ||
fs.stat(path.join(dir, file), function (err, s) { | ||
if (err) { | ||
@@ -59,6 +81,6 @@ errs.push(err); | ||
else if (s.isDirectory()) { | ||
dirs.push(file); | ||
dirs.push([file, s]); | ||
} | ||
else { | ||
files.push(file); | ||
files.push([file, s]); | ||
} | ||
@@ -73,8 +95,3 @@ | ||
sortByIsDirectory(files, function (errs, dirs, files) { | ||
if (errs.length > 0) { | ||
return status[500](res, next, { error: errs[0] }); | ||
} | ||
function render(dirs, files) { | ||
// Lifted from nodejitsu's http server. | ||
@@ -88,3 +105,3 @@ var html = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"\ | ||
<body> \ | ||
<h1>Index of ' + pathname + '</h1>'; | ||
<h1>Index of ' + pathname + '</h1>\n'; | ||
@@ -94,14 +111,14 @@ html += '<table>'; | ||
var writeRow = function (file, i) { | ||
html += '<tr><td>' + '<a href="' | ||
html += '<tr><td><code>(' + perms(file[1]) + ')</code> <a href="' | ||
+ ent.encode(encodeURI( | ||
req.url.replace(/\/$/, '') | ||
+ '/' | ||
+ file | ||
)) + '">' + ent.encode(file) + '</a></td></tr>'; | ||
+ file[0] | ||
)) + '">' + ent.encode(file[0]) + '</a></td></tr>\n'; | ||
} | ||
dirs.sort().forEach(writeRow); | ||
files.sort().forEach(writeRow); | ||
dirs.sort(function (a, b) { return b[0] - a[0] }).forEach(writeRow); | ||
files.sort(function (a, b) { return b[0] - a[0] }).forEach(writeRow); | ||
html += '</table>'; | ||
html += '</table>\n'; | ||
html += '<br><address>Node.js ' | ||
@@ -111,3 +128,3 @@ + process.version | ||
+ ' server running @ ' | ||
+ ent.encode(req.headers.host) + '</address>' | ||
+ ent.encode(req.headers.host) + '</address>\n' | ||
+ '</body></html>' | ||
@@ -118,3 +135,3 @@ ; | ||
res.end(html); | ||
}); | ||
} | ||
}); | ||
@@ -124,1 +141,19 @@ }); | ||
}; | ||
function perms(stat) { | ||
var dir = stat.isDirectory() ? 'd' : '-', | ||
mode = stat.mode.toString(8); | ||
return dir + mode.slice(-3).split('').map(function (n) { | ||
return [ | ||
'---', | ||
'--x', | ||
'-w-', | ||
'-wx', | ||
'r--', | ||
'r-x', | ||
'rw-', | ||
'rwx' | ||
][parseInt(n, 10)] | ||
}).join(''); | ||
} |
@@ -0,7 +1,10 @@ | ||
// not modified | ||
exports['304'] = function (res, next) { | ||
res.writeHead(304, res.headers); | ||
res.statusCode = 304; | ||
res.end(); | ||
}; | ||
// access denied | ||
exports['403'] = function (res, next) { | ||
res.statusCode = 403; | ||
if (typeof next === "function") { | ||
@@ -13,3 +16,2 @@ next(); | ||
res.setHeader('content-type', 'text/plain'); | ||
res.writeHead(403, res.headers); | ||
res.end('ACCESS DENIED'); | ||
@@ -20,3 +22,5 @@ } | ||
// disallowed method | ||
exports['405'] = function (res, next, opts) { | ||
res.statusCode = 405; | ||
if (typeof next === "function") { | ||
@@ -27,3 +31,2 @@ next(); | ||
res.setHeader('allow', (opts && opts.allow) || 'GET, HEAD'); | ||
res.writeHead(405, res.headers); | ||
res.end(); | ||
@@ -33,3 +36,5 @@ } | ||
// not found | ||
exports['404'] = function (res, next) { | ||
res.statusCode = 404; | ||
if (typeof next === "function") { | ||
@@ -41,3 +46,2 @@ next(); | ||
res.setHeader('content-type', 'text/plain'); | ||
res.writeHead(404, res.headers); | ||
res.end('File not found. :('); | ||
@@ -48,6 +52,6 @@ } | ||
// flagrant error | ||
exports['500'] = function (res, next, opts) { | ||
// TODO: Return nicer messages | ||
res.writeHead(500, res.headers); | ||
res.statusCode = 500; | ||
res.end(opts.error.stack || opts.error.toString() || "No specified error"); | ||
}; |
@@ -5,3 +5,3 @@ { | ||
"description": "A simple static file server middleware that works with both Express and Flatiron", | ||
"version": "0.1.7", | ||
"version": "0.3.0", | ||
"homepage": "https://github.com/jesusabdullah/node-ecstatic", | ||
@@ -28,11 +28,12 @@ "repository": { | ||
"dependencies": { | ||
"mime" : "1.2.5", | ||
"mime" : "1.2.7", | ||
"ent" : "0.0.x" | ||
}, | ||
"devDependencies": { | ||
"tap" : "0.0.x", | ||
"request" : "2.2.x", | ||
"express" : "2.5.x", | ||
"union" : "0.1.x" | ||
"tap" : "0.3.x", | ||
"request" : "2.12.x", | ||
"express" : "3.0.x", | ||
"union" : "0.3.x", | ||
"mkdirp": "0.3.x" | ||
} | ||
} |
@@ -1,20 +0,20 @@ | ||
# Ecstatic | ||
# Ecstatic [![build status](https://secure.travis-ci.org/jesusabdullah/node-ecstatic.png)](http://travis-ci.org/jesusabdullah/node-ecstatic) | ||
A simple static file server middleware that works with both Express and Flatiron | ||
![](http://imgur.com/vhub5.png) | ||
* Built-in simple directory listings | ||
* Shows index.html files at directory roots when they exist | ||
* Use it with a raw http server, express/connect, or flatiron/union! | ||
A simple static file server middleware. Use it with a raw http server, | ||
express/connect, or flatiron/union! | ||
# Examples: | ||
## express | ||
## express 3.0.x | ||
``` js | ||
var http = require('http'); | ||
var express = require('express'); | ||
var ecstatic = require('ecstatic'); | ||
var app = express.createServer(); | ||
app.use(ecstatic(__dirname + '/public')); | ||
app.listen(8080); | ||
var app = express(); | ||
app.use(ecstatic({ root: __dirname + '/public' })); | ||
http.createServer(app).listen(8080); | ||
@@ -32,3 +32,3 @@ console.log('Listening on :8080'); | ||
before: [ | ||
ecstatic(__dirname + '/public'), | ||
ecstatic({ root: __dirname + '/public' }), | ||
] | ||
@@ -40,18 +40,12 @@ }).listen(8080); | ||
## flatiron | ||
## stock http server | ||
``` js | ||
var union = require('union'); | ||
var flatiron = require('flatiron'); | ||
var http = require('http'); | ||
var ecstatic = require('ecstatic'); | ||
app = new flatiron.App(); | ||
app.use(flatiron.plugins.http); | ||
http.createServer( | ||
ecstatic({ root: __dirname + '/public' }) | ||
).listen(8080); | ||
app.http.before = [ | ||
ecstatic(__dirname + '/public') | ||
]; | ||
app.start(8080); | ||
console.log('Listening on :8080'); | ||
@@ -63,3 +57,3 @@ ``` | ||
```js | ||
ecstatic(__dirname + '/public', {handleError: false}) | ||
ecstatic({ root: __dirname + '/public', handleError: false }) | ||
``` | ||
@@ -69,12 +63,29 @@ | ||
## ecstatic(folder, opts={}); | ||
## ecstatic(opts); | ||
Pass ecstatic a folder, and it will return your middleware! | ||
Pass ecstatic an options hash, and it will return your middleware! | ||
Turn on cache-control with `opts.cache`, in seconds. | ||
`opts.root` is the directory you want to serve up. | ||
Turn off directory listings with `opts.autoIndex === false`. | ||
`opts.baseDir` is `/` by default, but can be changed to allow your static files | ||
to be served off a specific route. For example, if `opts.baseDir === "blog"` | ||
and `opts.root = "./public"`, requests for `localhost:8080/blog/index.html` will | ||
resolve to `./public/index.html`. | ||
Turn on default file extensions with `opts.defaultExt`. If `opts.defaultExt` is true, it will default to `html`. For example if you want a request to /a-file to server /root/a-file.html set this to `true`. If you want to serve /root/a-file.json set `opts.defaultExt` to `json`. | ||
Customize cache control with `opts.cache`, in seconds. Time defaults to 3600 s | ||
(ie, 1 hour). | ||
Turn **on** directory listings with `opts.showDir === true`. Turn **on** | ||
autoIndexing with `opts.autoIndex === true`. Defaults to **false**. | ||
Turn on default file extensions with `opts.defaultExt`. If `opts.defaultExt` is | ||
true, it will default to `html`. For example if you want a request to `/a-file` | ||
to resolve to `./public/a-file.html`, set this to `true`. If you want | ||
`/a-file` to resolve to `./public/a-file.json` instead, set `opts.defaultExt` to | ||
`json`. | ||
Set `opts.gzip === true` in order to turn on "gzip mode," wherein ecstatic will | ||
serve `./public/some-file.js.gz` in place of `./public/some-file.js` when the | ||
gzipped version exists and ecstatic determines that the behavior is appropriate. | ||
### middleware(req, res, next); | ||
@@ -81,0 +92,0 @@ |
@@ -1,9 +0,15 @@ | ||
var test = require('tap').test; | ||
var ecstatic = require('../lib/ecstatic'); | ||
var express = require('express'); | ||
var request = require('request'); | ||
var test = require('tap').test, | ||
ecstatic = require('../lib/ecstatic'), | ||
http = require('http'), | ||
express = require('express'), | ||
request = require('request'), | ||
mkdirp = require('mkdirp'), | ||
fs = require('fs'), | ||
path = require('path'); | ||
var root = __dirname + '/express'; | ||
var root = __dirname + '/public', | ||
baseDir = 'base'; | ||
var fs = require('fs'); | ||
mkdirp.sync(root + '/emptyDir'); | ||
var files = { | ||
@@ -41,2 +47,5 @@ 'a.txt' : { | ||
'subdir' : { | ||
code : 302 | ||
}, | ||
'subdir/' : { | ||
code : 200, | ||
@@ -48,2 +57,15 @@ type : 'text/html', | ||
code : 404 | ||
}, | ||
'compress/foo.js' : { | ||
code : 200, | ||
file: 'compress/foo.js.gz', | ||
headers: {'accept-encoding': 'compress, gzip'} | ||
}, | ||
// no accept-encoding of gzip, so serve regular file | ||
'compress/foo_2.js' : { | ||
code : 200, | ||
file: 'compress/foo_2.js' | ||
}, | ||
'emptyDir/': { | ||
code: 200 | ||
} | ||
@@ -54,21 +76,35 @@ }; | ||
var filenames = Object.keys(files); | ||
t.plan(filenames.length * 3 - 2); | ||
var port = Math.floor(Math.random() * ((1<<16) - 1e4) + 1e4); | ||
var app = express.createServer(); | ||
app.use(ecstatic(root)); | ||
app.listen(port, function () { | ||
var app = express(); | ||
app.use(ecstatic({ | ||
root: root, | ||
gzip: true, | ||
baseDir: baseDir, | ||
autoIndex: true, | ||
showDir: true | ||
})); | ||
var server = http.createServer(app); | ||
server.listen(port, function () { | ||
var pending = filenames.length; | ||
filenames.forEach(function (file) { | ||
var uri = 'http://localhost:' + port + '/' + file; | ||
request.get(uri, function (err, res, body) { | ||
var uri = 'http://localhost:' + port + path.join('/', baseDir, file), | ||
headers = files[file].headers || {}; | ||
request.get({ | ||
uri: uri, | ||
followRedirect: false, | ||
headers: headers | ||
}, function (err, res, body) { | ||
if (err) t.fail(err); | ||
var r = files[file]; | ||
t.equal(res.statusCode, r.code, 'status code for `' + file + '`'); | ||
t.equal(r.code, res.statusCode, 'code for ' + file); | ||
if (r.type !== undefined) { | ||
t.equal( | ||
res.headers['content-type'], r.type, | ||
'content-type for ' + file | ||
res.headers['content-type'].split(';')[0], r.type, | ||
'content-type for `' + file + '`' | ||
); | ||
@@ -78,7 +114,7 @@ } | ||
if (r.body !== undefined) { | ||
t.equal(body, r.body, 'body for ' + file); | ||
t.equal(body, r.body, 'body for `' + file + '`'); | ||
} | ||
if (--pending === 0) { | ||
app.close(); | ||
server.close(); | ||
t.end(); | ||
@@ -85,0 +121,0 @@ } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
220973
27
771
102
5
6
6
+ Addedmime@1.2.7(transitive)
- Removedmime@1.2.5(transitive)
Updatedmime@1.2.7