Socket
Socket
Sign inDemoInstall

ecstatic

Package Overview
Dependencies
Maintainers
1
Versions
79
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ecstatic - npm Package Compare versions

Comparing version 0.1.7 to 0.3.0

.travis.yml

5

example/express.js
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');

180

lib/ecstatic.js

@@ -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 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc