Compare commits

...

58 Commits

Author SHA1 Message Date
Douglas Christopher Wilson
63456cccf2 1.2.0 2014-08-25 19:13:55 -04:00
Douglas Christopher Wilson
6ce2fd216b Add debug messages 2014-08-25 18:56:27 -04:00
Douglas Christopher Wilson
83de752968 Resolve relative paths at middleware setup
fixes #15
2014-08-25 18:51:14 -04:00
Douglas Christopher Wilson
86db218bdb docs: readme tweaks 2014-08-25 18:36:10 -04:00
Douglas Christopher Wilson
0375dd3638 tests: check for global leaks 2014-08-25 18:33:22 -04:00
Douglas Christopher Wilson
005dbad307 build: change readme/history capitalization 2014-08-25 18:32:20 -04:00
Douglas Christopher Wilson
a189d6b5e5 docs: link to license from readme 2014-08-25 18:30:59 -04:00
Douglas Christopher Wilson
a01c0286df docs: update badges 2014-08-25 18:29:49 -04:00
Douglas Christopher Wilson
6e4da1e8d6 build: remove npmignore file 2014-08-25 18:27:54 -04:00
Douglas Christopher Wilson
d88117fad9 docs: expand examples
closes #14
2014-08-17 13:38:00 -04:00
Douglas Christopher Wilson
194663a06f 1.1.6 2014-08-10 17:45:04 -04:00
Douglas Christopher Wilson
fd277c695c Fix URL parsing 2014-08-10 17:40:45 -04:00
Douglas Christopher Wilson
ea7b37f19b 1.1.5 2014-07-27 18:16:38 -04:00
Douglas Christopher Wilson
d613373f78 docs: add Gittip badge 2014-07-27 18:16:28 -04:00
Douglas Christopher Wilson
afb181df84 deps: istanbul@0.3.0 2014-07-27 18:08:51 -04:00
Douglas Christopher Wilson
a70edf46cd deps: mocha@~1.21.1 2014-07-27 18:07:51 -04:00
Douglas Christopher Wilson
9ccd58738b deps: accepts@~1.0.7 2014-07-27 18:07:19 -04:00
Yad Smood
db905a6d8a Fix Content-Length calculation for multi-byte file names
closes #12
2014-07-27 17:42:18 -04:00
Douglas Christopher Wilson
eeda184dbd deps: istanbul@0.2.11 2014-06-24 21:12:07 -04:00
Douglas Christopher Wilson
a879aee5e5 1.1.4 2014-06-20 15:38:46 -04:00
Douglas Christopher Wilson
7ba4f7cf44 deps: accepts@~1.0.5 2014-06-20 15:38:04 -04:00
Douglas Christopher Wilson
a73d752247 1.1.3 2014-06-20 13:49:12 -04:00
Douglas Christopher Wilson
4e8cf53ce5 deps: accepts@~1.0.4 2014-06-20 13:48:16 -04:00
Douglas Christopher Wilson
4e8726c41c 1.1.2 2014-06-19 11:09:23 -04:00
Douglas Christopher Wilson
d7d661245e deps: batch@0.5.1 2014-06-19 11:02:13 -04:00
Douglas Christopher Wilson
5037f808c0 1.1.1 2014-06-11 23:59:59 -04:00
Douglas Christopher Wilson
910923b920 deps: accepts@1.0.3 2014-06-11 23:49:07 -04:00
Douglas Christopher Wilson
70c35f62b2 build: use compact formats in package 2014-06-11 22:46:53 -04:00
Basarat Ali Syed
73ceabeb00 docs: use module name as export name
closes #10
2014-06-08 23:14:33 -04:00
Douglas Christopher Wilson
76daee7400 1.1.0 2014-05-29 15:29:13 -04:00
Douglas Christopher Wilson
f75c172d72 deps: should@~4.0.0 2014-05-29 15:03:58 -04:00
Douglas Christopher Wilson
7a2a8d2ecc Use accepts for negotiation 2014-05-29 14:37:29 -04:00
Douglas Christopher Wilson
cb38e459ca tests: add more tests 2014-05-29 10:55:48 -04:00
Douglas Christopher Wilson
2edcc09345 Treat ENAMETOOLONG as code 414 2014-05-29 09:05:25 -04:00
Douglas Christopher Wilson
a0415f5df2 Remove res.writeHead 2014-05-29 08:38:31 -04:00
Douglas Christopher Wilson
711194e063 Fix content negotiation when no Accept header 2014-05-29 08:35:04 -04:00
Douglas Christopher Wilson
4d19115d6b tests: add more basic tests 2014-05-28 17:22:13 -04:00
Douglas Christopher Wilson
c00618fa62 Properly support all HTTP methods 2014-05-28 17:13:22 -04:00
Douglas Christopher Wilson
db91ccac66 Throw TypeError for missing root argument 2014-05-28 17:01:53 -04:00
Douglas Christopher Wilson
926ee987fa build: add coverage reporting 2014-05-28 16:56:31 -04:00
Douglas Christopher Wilson
6277105d11 build: add test coverage 2014-05-28 16:36:56 -04:00
Douglas Christopher Wilson
199f5d0205 tests: remove use of connect 2014-05-28 16:31:41 -04:00
Douglas Christopher Wilson
871f4d679e Support vanilla node.js http servers 2014-05-28 16:27:28 -04:00
Douglas Christopher Wilson
6e3f352894 tests: add custom handler tests 2014-05-24 23:15:03 -04:00
Douglas Christopher Wilson
9fbc4bf182 1.0.3 2014-05-20 10:23:05 -04:00
Douglas Christopher Wilson
027f6dcf54 Fix error from non-statable files in HTML view
fixes #6
2014-05-20 10:14:31 -04:00
Douglas Christopher Wilson
f7eb0ae92a docs: add History 2014-05-20 10:00:15 -04:00
Fishrock123
51e9763c0a docs: enhance 2014-04-29 10:53:47 -04:00
Chris O'Connor
dd6a4a864a docs: fix typo, add stylesheet
closes #4
2014-04-29 10:37:11 -04:00
Douglas Christopher Wilson
cff5890bce 1.0.2 2014-04-28 14:44:13 -04:00
Douglas Christopher Wilson
6d9e23c5ac docs: use SVG Travis CI badge 2014-04-28 14:34:00 -04:00
Douglas Christopher Wilson
36d68485ab deps: negotiator@0.4.3 2014-04-28 14:32:38 -04:00
Chris O'Connor
e40300aa86 Add stylesheet option
closes #2
2014-04-28 14:31:00 -04:00
Anthony Verez
5771318d78 docs: document the view option
closes #1
2014-04-26 13:16:02 -04:00
Douglas Christopher Wilson
d006e3a123 deps: tighten devDepencency ranges 2014-03-06 14:51:51 -05:00
Douglas Christopher Wilson
a2364a8abf test: test on node.js 0.11 2014-03-06 13:26:46 -05:00
Douglas Christopher Wilson
90e39a9a4f deps: use 0.8-compatible semver in devDependencies 2014-03-06 13:23:48 -05:00
Douglas Christopher Wilson
480fdcc468 docs: add Travis CI badge 2014-03-06 13:02:48 -05:00
11 changed files with 734 additions and 248 deletions

64
.gitignore vendored
View File

@@ -1,65 +1,3 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store*
ehthumbs.db
Thumbs.db
# Node.js #
###########
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
coverage
node_modules
npm-debug.log
# Git #
#######
*.orig
*.BASE.*
*.BACKUP.*
*.LOCAL.*
*.REMOTE.*
# Components #
##############
/build
/components

View File

@@ -1 +0,0 @@
test

View File

@@ -2,3 +2,10 @@ language: node_js
node_js:
- "0.8"
- "0.10"
- "0.11"
matrix:
allow_failures:
- node_js: "0.11"
fast_finish: true
script: "npm run-script test-travis"
after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls"

69
HISTORY.md Normal file
View File

@@ -0,0 +1,69 @@
1.2.0 / 2014-08-25
==================
* Add `debug` messages
* Resolve relative paths at middleware setup
1.1.6 / 2014-08-10
==================
* Fix URL parsing
* deps: parseurl@~1.3.0
1.1.5 / 2014-07-27
==================
* Fix Content-Length calculation for multi-byte file names
* deps: accepts@~1.0.7
- deps: negotiator@0.4.7
1.1.4 / 2014-06-20
==================
* deps: accepts@~1.0.5
1.1.3 / 2014-06-20
==================
* deps: accepts@~1.0.4
- use `mime-types`
1.1.2 / 2014-06-19
==================
* deps: batch@0.5.1
1.1.1 / 2014-06-11
==================
* deps: accepts@1.0.3
1.1.0 / 2014-05-29
==================
* Fix content negotiation when no `Accept` header
* Properly support all HTTP methods
* Support vanilla node.js http servers
* Treat `ENAMETOOLONG` as code 414
* Use accepts for negotiation
1.0.3 / 2014-05-20
==================
* Fix error from non-statable files in HTML view
1.0.2 / 2014-04-28
==================
* Add `stylesheet` option
* deps: negotiator@0.4.3
1.0.1 / 2014-03-05
==================
* deps: negotiator@0.4.2
1.0.0 / 2014-03-05
==================
* Genesis from connect

123
README.md Normal file
View File

@@ -0,0 +1,123 @@
# serve-index
[![NPM Version][npm-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url]
[![Build Status][travis-image]][travis-url]
[![Test Coverage][coveralls-image]][coveralls-url]
[![Gittip][gittip-image]][gittip-url]
Serves pages that contain directory listings for a given path.
## Install
```sh
$ npm install serve-index
```
## API
```js
var serveIndex = require('serve-index')
```
### serveIndex(path, options)
Returns middlware that serves an index of the directory in the given `path`.
The `path` is based off the `req.url` value, so a `req.url` of `'/some/dir`
with a `path` of `'public'` will look at `'public/some/dir'`. If you are using
something like `express`, you can change the URL "base" with `app.use` (see
the express example).
#### Options
Serve index accepts these properties in the options object.
##### filter
Apply this filter function to files. Defaults to `false`.
##### hidden
Display hidden (dot) files. Defaults to `false`.
##### icons
Display icons. Defaults to `false`.
##### stylesheet
Optional path to a CSS stylesheet. Defaults to a built-in stylesheet.
##### template
Optional path to an HTML template. Defaults to a built-in template.
The following tokens are replaced in templates:
* `{directory}` with the name of the directory.
* `{files}` with the HTML of an unordered list of file links.
* `{linked-path}` with the HTML of a link to the directory.
* `{style}` with the specified stylesheet and embedded images.
##### view
Display mode. `tiles` and `details` are available. Defaults to `tiles`.
## Examples
### Serve directory indexes with vanilla node.js http server
```js
var finalhandler = require('finalhandler')
var http = require('http')
var serveIndex = require('serve-index')
var serveStatic = require('serve-static')
// Serve directory indexes for public/ftp folder (with icons)
var index = serveIndex('public/ftp', {'icons': true})
// Serve up public/ftp folder files
var serve = serveStatic('public/ftp')
// Create server
var server = http.createServer(function onRequest(req, res){
var done = finalhandler(req, res)
serve(req, res, function onNext(err) {
if (err) return done(err)
index(req, res, done)
})
})
// Listen
server.listen(3000)
```
### Serve directory indexes with express
```js
var express = require('express')
var serveIndex = require('serve-index')
var app = express()
// Serve URLs like /ftp/thing as public/ftp/thing
app.use('/ftp', serveIndex('public/ftp', {'icons': true}))
app.listen()
```
## License
[MIT](LICENSE). The [Silk](http://www.famfamfam.com/lab/icons/silk/) icons
are created by/copyright of [FAMFAMFAM](http://www.famfamfam.com/).
[npm-image]: https://img.shields.io/npm/v/serve-index.svg?style=flat
[npm-url]: https://npmjs.org/package/serve-index
[travis-image]: https://img.shields.io/travis/expressjs/serve-index.svg?style=flat
[travis-url]: https://travis-ci.org/expressjs/serve-index
[coveralls-image]: https://img.shields.io/coveralls/expressjs/serve-index.svg?style=flat
[coveralls-url]: https://coveralls.io/r/expressjs/serve-index?branch=master
[downloads-image]: http://img.shields.io/npm/dm/serve-index.svg?style=flat
[downloads-url]: https://npmjs.org/package/serve-index
[gittip-image]: https://img.shields.io/gittip/dougwilson.svg?style=flat
[gittip-url]: https://www.gittip.com/dougwilson/

View File

@@ -1,42 +0,0 @@
# Serve Index
Previously `connect.directory()`.
Usage:
```js
var connect = require('connect');
var serveIndex = require('serve-index');
var app = connect();
app.use(serveIndex('public/ftp', {'icons': true}));
app.listen();
```
## License
The MIT License (MIT)
Copyright (c) 2014 Douglas Christopher Wilson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
The [Silk](http://www.famfamfam.com/lab/icons/silk/) icons are created
by/copyright of [FAMFAMFAM](http://www.famfamfam.com/).

136
index.js
View File

@@ -1,6 +1,6 @@
/*!
* Connect - directory
* serve-index
* Copyright(c) 2011 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2014 Douglas Christopher Wilson
@@ -14,16 +14,18 @@
* Module dependencies.
*/
var accepts = require('accepts');
var debug = require('debug')('serve-index');
var http = require('http')
, fs = require('fs')
, parse = require('url').parse
, path = require('path')
, normalize = path.normalize
, sep = path.sep
, extname = path.extname
, join = path.join;
var Batch = require('batch');
var Negotiator = require('negotiator');
var parseUrl = require('parseurl');
var resolve = require('path').resolve;
/*!
* Icon cache.
@@ -41,7 +43,7 @@ var defaultTemplate = join(__dirname, 'public', 'directory.html');
* Stylesheet.
*/
var stylesheet = join(__dirname, 'public', 'style.css');
var defaultStylesheet = join(__dirname, 'public', 'style.css');
/**
* Media types and the map for content negotiation.
@@ -60,65 +62,78 @@ var mediaType = {
};
/**
* Directory:
*
* Serve directory listings with the given `root` path.
*
* Options:
* See Readme.md for documentation of options.
*
* - `hidden` display hidden (dot) files. Defaults to false.
* - `icons` display icons. Defaults to false.
* - `filter` Apply this filter function to files. Defaults to false.
* - `template` Optional path to html template. Defaults to a built-in template.
* The following tokens are replaced:
* - `{directory}` with the name of the directory.
* - `{files}` with the HTML of an unordered list of file links.
* - `{linked-path}` with the HTML of a link to the directory.
* - `{style}` with the built-in CSS and embedded images.
*
* @param {String} root
* @param {String} path
* @param {Object} options
* @return {Function}
* @return {Function} middleware
* @api public
*/
exports = module.exports = function directory(root, options){
exports = module.exports = function serveIndex(root, options){
options = options || {};
// root required
if (!root) throw new Error('directory() root path required');
if (!root) throw new TypeError('serveIndex() root path required');
// resolve root to absolute
root = resolve(root);
var hidden = options.hidden
, icons = options.icons
, view = options.view || 'tiles'
, filter = options.filter
, root = normalize(root + sep)
, template = options.template || defaultTemplate;
, template = options.template || defaultTemplate
, stylesheet = options.stylesheet || defaultStylesheet;
return function directory(req, res, next) {
if ('GET' != req.method && 'HEAD' != req.method) return next();
return function serveIndex(req, res, next) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.statusCode = 'OPTIONS' === req.method
? 200
: 405;
res.setHeader('Allow', 'GET, HEAD, OPTIONS');
res.end();
return;
}
var url = parse(req.url)
, dir = decodeURIComponent(url.pathname)
// parse URLs
var url = parseUrl(req);
var originalUrl = parseUrl.original(req);
var dir = decodeURIComponent(url.pathname)
, path = normalize(join(root, dir))
, originalUrl = parse(req.originalUrl)
, originalDir = decodeURIComponent(originalUrl.pathname)
, showUp = path != root;
var showUp = resolve(path) !== root;
// null byte(s), bad request
if (~path.indexOf('\0')) return next(createError(400));
// malicious path, forbidden
if (0 != path.indexOf(root)) return next(createError(403));
// malicious path
if (path.substr(0, root.length) !== root) {
debug('malicious path "%s"', path);
return next(createError(403));
}
// check if we have a directory
debug('stat "%s"', path);
fs.stat(path, function(err, stat){
if (err) return 'ENOENT' == err.code
? next()
: next(err);
if (err && err.code === 'ENOENT') {
return next();
}
if (err) {
err.status = err.code === 'ENAMETOOLONG'
? 414
: 500;
return next(err);
}
if (!stat.isDirectory()) return next();
// fetch files
debug('readdir "%s"', path);
fs.readdir(path, function(err, files){
if (err) return next(err);
if (!hidden) files = removeHidden(files);
@@ -126,11 +141,12 @@ exports = module.exports = function directory(root, options){
files.sort();
// content-negotiation
var type = new Negotiator(req).preferredMediaType(mediaTypes);
var accept = accepts(req);
var type = accept.types(mediaTypes);
// not acceptable
if (!type) return next(createError(406));
exports[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template);
exports[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet);
});
});
};
@@ -140,7 +156,7 @@ exports = module.exports = function directory(root, options){
* Respond with text/html.
*/
exports.html = function(req, res, files, next, dir, showUp, icons, path, view, template){
exports.html = function(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet){
fs.readFile(template, 'utf8', function(err, str){
if (err) return next(err);
fs.readFile(stylesheet, 'utf8', function(err, style){
@@ -155,9 +171,11 @@ exports.html = function(req, res, files, next, dir, showUp, icons, path, view, t
.replace('{files}', html(files, dir, icons, view))
.replace('{directory}', dir)
.replace('{linked-path}', htmlPath(dir));
res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Length', str.length);
res.end(str);
var buf = new Buffer(str, 'utf8');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Content-Length', buf.length);
res.end(buf);
});
});
});
@@ -168,10 +186,12 @@ exports.html = function(req, res, files, next, dir, showUp, icons, path, view, t
*/
exports.json = function(req, res, files){
files = JSON.stringify(files);
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Length', files.length);
res.end(files);
var body = JSON.stringify(files);
var buf = new Buffer(body, 'utf8');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Content-Length', buf.length);
res.end(buf);
};
/**
@@ -179,10 +199,12 @@ exports.json = function(req, res, files){
*/
exports.plain = function(req, res, files){
files = files.join('\n') + '\n';
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', files.length);
res.end(files);
var body = files.join('\n') + '\n';
var buf = new Buffer(body, 'utf8');
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Content-Length', buf.length);
res.end(buf);
};
/**
@@ -292,9 +314,12 @@ function html(files, dir, useIcons, view) {
path.push(encodeURIComponent(file.name));
var date = file.name == '..' ? ''
: file.stat.mtime.toDateString()+' '+file.stat.mtime.toLocaleTimeString();
var size = isDir ? '' : file.stat.size;
var date = file.stat && file.name !== '..'
? file.stat.mtime.toDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
: '';
var size = file.stat && !isDir
? file.stat.size
: '';
return '<li><a href="'
+ normalizeSlashes(normalize(path.join('/')))
@@ -362,7 +387,14 @@ function stat(dir, files, cb) {
files.forEach(function(file){
batch.push(function(done){
fs.stat(join(dir, file), done);
fs.stat(join(dir, file), function(err, stat){
if (err && err.code !== 'ENOENT') {
// pass ENOENT as null stat, not error
return done(err);
}
done(null, stat || null);
});
});
});

View File

@@ -1,30 +1,34 @@
{
"name": "serve-index",
"description": "Serve directory listings",
"version": "1.0.1",
"version": "1.2.0",
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/expressjs/serve-index.git"
},
"bugs": {
"url": "https://github.com/expressjs/serve-index/issues"
},
"repository": "expressjs/serve-index",
"dependencies": {
"batch": "0.5.0",
"negotiator": "0.4.2"
"accepts": "~1.0.7",
"batch": "0.5.1",
"debug": "1.0.4",
"parseurl": "~1.3.0"
},
"devDependencies": {
"connect": "^2.13.0",
"mocha": "^1.17.0",
"should": "^3.0.0",
"supertest": "~0.9.0"
"istanbul": "0.3.0",
"mocha": "~1.21.1",
"should": "~4.0.0",
"supertest": "~0.13.0"
},
"files": [
"public/",
"LICENSE",
"HISTORY.md",
"index.js"
],
"engines": {
"node": ">= 0.8.0"
},
"scripts": {
"test": "mocha --reporter spec --require should"
"test": "mocha --reporter spec --bail --check-leaks test/",
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/",
"test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/"
}
}

0
test/fixtures/さくら.txt vendored Normal file
View File

3
test/shared/styles.css Normal file
View File

@@ -0,0 +1,3 @@
body {
color: #00ff00;
}

View File

@@ -1,59 +1,148 @@
process.env.NODE_ENV = 'test';
var connect = require('connect');
var http = require('http');
var fs = require('fs');
var path = require('path');
var request = require('supertest');
var should = require('should');
var serveIndex = require('..');
describe('directory()', function(){
describe('when given Accept: header', function () {
var server;
before(function () {
server = createServer();
});
after(function (done) {
server.close(done);
});
var fixtures = path.join(__dirname, '/fixtures');
var relative = path.relative(process.cwd(), fixtures);
describe('of application/json', function () {
var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative;
describe('serveIndex(root)', function () {
it('should require root', function () {
serveIndex.should.throw(/root path required/)
})
it('should serve text/html without Accept header', function (done) {
var server = createServer()
request(server)
.get('/')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(200, done)
})
it('should serve a directory index', function (done) {
var server = createServer()
request(server)
.get('/')
.expect(200, /todo\.txt/, done)
})
it('should work with HEAD requests', function (done) {
var server = createServer()
request(server)
.head('/')
.expect(200, '', done)
})
it('should work with OPTIONS requests', function (done) {
var server = createServer()
request(server)
.options('/')
.expect('Allow', 'GET, HEAD, OPTIONS')
.expect(200, done)
})
it('should deny POST requests', function (done) {
var server = createServer()
request(server)
.post('/')
.expect(405, done)
})
it('should deny path will NULL byte', function (done) {
var server = createServer()
request(server)
.get('/%00')
.expect(400, done)
})
it('should deny path outside root', function (done) {
var server = createServer()
request(server)
.get('/../')
.expect(403, done)
})
it('should skip non-existent paths', function (done) {
var server = createServer()
request(server)
.get('/bogus')
.expect(404, 'Not Found', done)
})
it('should treat an ENAMETOOLONG as a 414', function (done) {
var path = Array(11000).join('foobar')
var server = createServer()
request(server)
.get('/' + path)
.expect(414, done)
})
it('should skip non-directories', function (done) {
var server = createServer()
request(server)
.get('/nums')
.expect(404, 'Not Found', done)
})
describe('when given Accept: header', function () {
describe('when Accept: application/json is given', function () {
it('should respond with json', function (done) {
var server = createServer()
request(server)
.get('/')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res) {
if (err) throw err;
res.body.should.include('g# %3 o %2525 %37 dir');
res.body.should.include('users');
res.body.should.include('file #1.txt');
res.body.should.include('nums');
res.body.should.include('todo.txt');
done();
});
.expect(/g# %3 o %2525 %37 dir/)
.expect(/users/)
.expect(/file #1\.txt/)
.expect(/nums/)
.expect(/todo\.txt/)
.expect(/さくら\.txt/)
.expect(200, done)
});
});
describe('when Accept: text/html is given', function () {
it('should respond with html', function (done) {
var server = createServer()
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(/<a href="\/g%23%20%253%20o%20%252525%20%2537%20dir"/)
.expect(/<a href="\/users"/)
.expect(/<a href="\/file%20%231.txt"/)
.expect(/<a href="\/todo.txt"/)
.expect(/<a href="\/%E3%81%95%E3%81%8F%E3%82%89\.txt"/)
.end(done);
});
it('should sort folders first', function (done) {
var server = createServer()
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.expect('Content-Type', 'text/html; charset=utf-8')
.end(function (err, res) {
if (err) throw err;
var urls = res.text.split(/<a href="([^"]*)"/).filter(function(s, i){ return i%2; });
@@ -65,6 +154,7 @@ describe('directory()', function(){
'/foo%20bar',
'/nums',
'/todo.txt',
'/%E3%81%95%E3%81%8F%E3%82%89.txt'
]);
done();
});
@@ -73,63 +163,307 @@ describe('directory()', function(){
describe('when Accept: text/plain is given', function () {
it('should respond with text', function (done) {
var server = createServer()
request(server)
.get('/')
.set('Accept', 'text/plain')
.expect(200)
.expect('Content-Type', /plain/)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(/users/)
.expect(/g# %3 o %2525 %37 dir/)
.expect(/file #1.txt/)
.expect(/todo.txt/)
.expect(/さくら\.txt/)
.end(done);
});
});
describe('when Accept: application/x-bogus is given', function () {
it('should respond with 406', function (done) {
var server = createServer()
request(server)
.get('/')
.set('Accept', 'application/x-bogus')
.expect(406, done)
});
});
});
describe('with "hidden" option', function () {
it('should filter hidden files by default', function (done) {
var server = createServer()
request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
res.text.should.not.containEql('.hidden')
done()
});
});
it('should filter hidden files', function (done) {
var server = createServer('test/fixtures', {'hidden': false})
request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
res.text.should.not.containEql('.hidden')
done()
});
});
it('should not filter hidden files', function (done) {
var server = createServer('test/fixtures', {'hidden': true})
request(server)
.get('/')
.expect(200, /\.hidden/, done)
});
});
describe('with "filter" option', function () {
it('should custom filter files', function (done) {
var seen = false
var server = createServer(fixtures, {'filter': filter})
function filter(name) {
if (name.indexOf('foo') === -1) return true
seen = true
return false
}
request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
seen.should.be.true
res.text.should.not.containEql('foo')
done()
});
});
it('should filter after hidden filter', function (done) {
var seen = false
var server = createServer(fixtures, {'filter': filter, 'hidden': false})
function filter(name) {
seen = seen || name.indexOf('.') === 0
return true
}
request(server)
.get('/')
.expect(200, function (err, res) {
if (err) return done(err)
seen.should.be.false
done()
});
});
});
describe('with "icons" option', function () {
it('should include icons for html', function (done) {
var server = createServer(fixtures, {'icons': true})
request(server)
.get('/')
.expect(/data:image\/png/)
.expect(/icon-default/)
.expect(/icon-directory/)
.expect(/icon-txt/)
.expect(200, done)
});
});
describe('when using custom handler', function () {
describe('exports.html', function () {
var orig = serveIndex.html
after(function () {
serveIndex.html = orig
})
it('should get called with Accept: text/html', function (done) {
var server = createServer()
serveIndex.html = function (req, res, files) {
res.setHeader('Content-Type', 'text/html');
res.end('called');
}
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200, 'called', done)
});
it('should get file list', function (done) {
var server = createServer()
serveIndex.html = function (req, res, files) {
var text = files
.filter(function (f) { return /\.txt$/.test(f) })
.sort()
res.setHeader('Content-Type', 'text/html')
res.end('<b>' + text.length + ' text files</b>')
}
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200, '<b>3 text files</b>', done)
});
it('should get dir name', function (done) {
var server = createServer()
serveIndex.html = function (req, res, files, next, dir) {
res.setHeader('Content-Type', 'text/html')
res.end('<b>' + dir + '</b>')
}
request(server)
.get('/users/')
.set('Accept', 'text/html')
.expect(200, '<b>/users/</b>', done)
});
it('should get template path', function (done) {
var server = createServer()
serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template) {
res.setHeader('Content-Type', 'text/html')
res.end(String(fs.existsSync(template)))
}
request(server)
.get('/users/')
.set('Accept', 'text/html')
.expect(200, 'true', done)
});
it('should get template with tokens', function (done) {
var server = createServer()
serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template) {
res.setHeader('Content-Type', 'text/html')
res.end(fs.readFileSync(template, 'utf8'))
}
request(server)
.get('/users/')
.set('Accept', 'text/html')
.expect(/{directory}/)
.expect(/{files}/)
.expect(/{linked-path}/)
.expect(/{style}/)
.expect(200, done)
});
it('should get stylesheet path', function (done) {
var server = createServer()
serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) {
res.setHeader('Content-Type', 'text/html')
res.end(String(fs.existsSync(stylesheet)))
}
request(server)
.get('/users/')
.set('Accept', 'text/html')
.expect(200, 'true', done)
});
});
describe('exports.plain', function () {
var orig = serveIndex.plain
after(function () {
serveIndex.plain = orig
})
it('should get called with Accept: text/plain', function (done) {
var server = createServer()
serveIndex.plain = function (req, res, files) {
res.setHeader('Content-Type', 'text/plain');
res.end('called');
}
request(server)
.get('/')
.set('Accept', 'text/plain')
.expect(200, 'called', done)
});
});
describe('exports.json', function () {
var orig = serveIndex.json
after(function () {
serveIndex.json = orig
})
it('should get called with Accept: application/json', function (done) {
var server = createServer()
serveIndex.json = function (req, res, files) {
res.setHeader('Content-Type', 'application/json');
res.end('"called"');
}
request(server)
.get('/')
.set('Accept', 'application/json')
.expect(200, '"called"', done)
});
});
});
describe('when navigating to other directory', function () {
var server;
before(function () {
server = createServer();
});
after(function (done) {
server.close(done);
});
it('should respond with correct listing', function (done) {
var server = createServer()
request(server)
.get('/users/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(/<a href="\/users\/index.html"/)
.expect(/<a href="\/users\/tobi.txt"/)
.end(done);
});
it('should work for directory with #', function (done) {
var server = createServer()
request(server)
.get('/%23directory/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(/<a href="\/%23directory"/)
.expect(/<a href="\/%23directory\/index.html"/)
.end(done);
});
it('should work for directory with special chars', function (done) {
var server = createServer()
request(server)
.get('/g%23%20%253%20o%20%252525%20%2537%20dir/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(/<a href="\/g%23%20%253%20o%20%252525%20%2537%20dir"/)
.expect(/<a href="\/g%23%20%253%20o%20%252525%20%2537%20dir\/empty.txt"/)
.end(done);
});
it('should not work for outside root', function (done) {
var server = createServer()
request(server)
.get('/../support/')
.set('Accept', 'text/html')
@@ -140,23 +474,48 @@ describe('directory()', function(){
describe('when setting a custom template', function () {
var server;
before(function () {
server = createServer('test/fixtures', {'template': __dirname + '/shared/template.html'});
});
after(function (done) {
server.close(done);
server = createServer(fixtures, {'template': __dirname + '/shared/template.html'});
});
it('should respond with file list and testing template sentence', function (done) {
it('should respond with file list', function (done) {
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.expect(/<a href="\/g%23%20%253%20o%20%252525%20%2537%20dir"/)
.expect(/<a href="\/users"/)
.expect(/<a href="\/file%20%231.txt"/)
.expect(/<a href="\/todo.txt"/)
.expect(/This is the test template/)
.expect(200, done)
});
it('should respond with testing template sentence', function (done) {
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200, /This is the test template/, done)
});
it('should have default styles', function (done) {
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200, /ul#files/, done)
});
});
describe('when setting a custom stylesheet', function () {
var server;
before(function () {
server = createServer(fixtures, {'stylesheet': __dirname + '/shared/styles.css'});
});
it('should respond with appropriate embedded styles', function (done) {
request(server)
.get('/')
.set('Accept', 'text/html')
.expect(200)
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(/color: #00ff00;/)
.end(done);
});
});
@@ -164,51 +523,39 @@ describe('directory()', function(){
describe('when set with trailing slash', function () {
var server;
before(function () {
server = createServer('test/fixtures/');
});
after(function (done) {
server.close(done);
server = createServer(fixtures + '/');
});
it('should respond with file list', function (done) {
request(server)
.get('/')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res) {
if (err) throw err;
res.body.should.include('users');
res.body.should.include('file #1.txt');
res.body.should.include('nums');
res.body.should.include('todo.txt');
done();
});
.expect(/users/)
.expect(/file #1\.txt/)
.expect(/nums/)
.expect(/todo\.txt/)
.expect(200, done)
});
});
describe('when set to \'.\'', function () {
(skipRelative ? describe.skip : describe)('when set to \'.\'', function () {
var server;
before(function () {
server = createServer('.');
});
after(function (done) {
server.close(done);
});
it('should respond with file list', function (done) {
var dest = relative.split(path.sep).join('/');
request(server)
.get('/')
.get('/' + dest + '/')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res) {
if (err) throw err;
res.body.should.include('LICENSE');
res.body.should.include('public');
res.body.should.include('test');
done();
});
.expect(/users/)
.expect(/file #1\.txt/)
.expect(/nums/)
.expect(/todo\.txt/)
.expect(200, done)
});
it('should not allow serving outside root', function (done) {
@@ -221,8 +568,14 @@ describe('directory()', function(){
});
function createServer(dir, opts) {
var app = connect();
dir = dir || 'test/fixtures';
app.use(serveIndex(dir, opts));
return app.listen();
dir = dir || fixtures
var _serveIndex = serveIndex(dir, opts)
return http.createServer(function (req, res) {
_serveIndex(req, res, function (err) {
res.statusCode = err ? (err.status || 500) : 404
res.end(err ? err.message : 'Not Found')
})
})
}