serve-index
Advanced tools
Comparing version 1.6.4 to 1.7.0
@@ -0,1 +1,21 @@ | ||
1.7.0 / 2015-06-15 | ||
================== | ||
* Accept `function` value for `template` option | ||
* Send non-chunked response for `OPTIONS` | ||
* Stat parent directory when necessary | ||
* Use `Date.prototype.toLocaleDateString` to format date | ||
* deps: accepts@~1.2.9 | ||
- deps: mime-types@~2.1.1 | ||
- deps: negotiator@0.5.3 | ||
- perf: avoid argument reassignment & argument slice | ||
- perf: avoid negotiator recursive construction | ||
- perf: enable strict mode | ||
- perf: remove unnecessary bitwise operator | ||
* deps: escape-html@1.0.2 | ||
* deps: mime-types@~2.1.1 | ||
- Add new mime types | ||
* perf: enable strict mode | ||
* perf: remove argument reassignment | ||
1.6.4 / 2015-05-12 | ||
@@ -2,0 +22,0 @@ ================== |
253
index.js
@@ -9,4 +9,3 @@ /*! | ||
// TODO: arrow key navigation | ||
// TODO: make icons extensible | ||
'use strict'; | ||
@@ -33,2 +32,9 @@ /** | ||
/** | ||
* Module exports. | ||
* @public | ||
*/ | ||
module.exports = serveIndex; | ||
/*! | ||
@@ -73,31 +79,31 @@ * Icon cache. | ||
* | ||
* @param {String} path | ||
* @param {String} root | ||
* @param {Object} options | ||
* @return {Function} middleware | ||
* @api public | ||
* @public | ||
*/ | ||
exports = module.exports = function serveIndex(root, options){ | ||
options = options || {}; | ||
function serveIndex(root, options) { | ||
var opts = options || {}; | ||
// root required | ||
if (!root) throw new TypeError('serveIndex() root path required'); | ||
if (!root) { | ||
throw new TypeError('serveIndex() root path required'); | ||
} | ||
// resolve root to absolute and normalize | ||
root = resolve(root); | ||
root = normalize(root + sep); | ||
var rootPath = normalize(resolve(root) + sep); | ||
var hidden = options.hidden | ||
, icons = options.icons | ||
, view = options.view || 'tiles' | ||
, filter = options.filter | ||
, template = options.template || defaultTemplate | ||
, stylesheet = options.stylesheet || defaultStylesheet; | ||
var filter = opts.filter; | ||
var hidden = opts.hidden; | ||
var icons = opts.icons; | ||
var stylesheet = opts.stylesheet || defaultStylesheet; | ||
var template = opts.template || defaultTemplate; | ||
var view = opts.view || 'tiles'; | ||
return function serveIndex(req, res, next) { | ||
return function (req, res, next) { | ||
if (req.method !== 'GET' && req.method !== 'HEAD') { | ||
res.statusCode = 'OPTIONS' === req.method | ||
? 200 | ||
: 405; | ||
res.statusCode = 'OPTIONS' === req.method ? 200 : 405; | ||
res.setHeader('Allow', 'GET, HEAD, OPTIONS'); | ||
res.setHeader('Content-Length', '0'); | ||
res.end(); | ||
@@ -114,3 +120,3 @@ return; | ||
// join / normalize from root dir | ||
var path = normalize(join(root, dir)); | ||
var path = normalize(join(rootPath, dir)); | ||
@@ -121,3 +127,3 @@ // null byte(s), bad request | ||
// malicious path | ||
if ((path + sep).substr(0, root.length) !== root) { | ||
if ((path + sep).substr(0, rootPath.length) !== rootPath) { | ||
debug('malicious path "%s"', path); | ||
@@ -128,3 +134,3 @@ return next(createError(403)); | ||
// determine ".." display | ||
var showUp = normalize(resolve(path) + sep) !== root; | ||
var showUp = normalize(resolve(path) + sep) !== rootPath; | ||
@@ -163,3 +169,3 @@ // check if we have a directory | ||
if (!type) return next(createError(406)); | ||
exports[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet); | ||
serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet); | ||
}); | ||
@@ -174,19 +180,42 @@ }); | ||
exports.html = function(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet){ | ||
fs.readFile(template, 'utf8', function(err, str){ | ||
serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) { | ||
var render = typeof template !== 'function' | ||
? createHtmlRender(template) | ||
: template | ||
if (showUp) { | ||
files.unshift('..'); | ||
} | ||
// stat all files | ||
stat(path, files, function (err, stats) { | ||
if (err) return next(err); | ||
fs.readFile(stylesheet, 'utf8', function(err, style){ | ||
// combine the stats into the file list | ||
var fileList = files.map(function (file, i) { | ||
return { name: file, stat: stats[i] }; | ||
}); | ||
// sort file list | ||
fileList.sort(fileSort); | ||
// read stylesheet | ||
fs.readFile(stylesheet, 'utf8', function (err, style) { | ||
if (err) return next(err); | ||
stat(path, files, function(err, stats){ | ||
// create locals for rendering | ||
var locals = { | ||
directory: dir, | ||
displayIcons: Boolean(icons), | ||
fileList: fileList, | ||
path: path, | ||
style: style, | ||
viewName: view | ||
}; | ||
// render html | ||
render(locals, function (err, body) { | ||
if (err) return next(err); | ||
files = files.map(function(file, i){ return { name: file, stat: stats[i] }; }); | ||
files.sort(fileSort); | ||
if (showUp) files.unshift({ name: '..' }); | ||
str = str | ||
.replace(/\{style\}/g, style.concat(iconStyle(files, icons))) | ||
.replace(/\{files\}/g, html(files, dir, icons, view)) | ||
.replace(/\{directory\}/g, escapeHtml(dir)) | ||
.replace(/\{linked-path\}/g, htmlPath(dir)); | ||
var buf = new Buffer(str, 'utf8'); | ||
var buf = new Buffer(body, 'utf8'); | ||
res.setHeader('Content-Type', 'text/html; charset=utf-8'); | ||
@@ -204,3 +233,3 @@ res.setHeader('Content-Length', buf.length); | ||
exports.json = function(req, res, files){ | ||
serveIndex.json = function _json(req, res, files) { | ||
var body = JSON.stringify(files); | ||
@@ -218,3 +247,3 @@ var buf = new Buffer(body, 'utf8'); | ||
exports.plain = function(req, res, files){ | ||
serveIndex.plain = function _plain(req, res, files) { | ||
var body = files.join('\n') + '\n'; | ||
@@ -229,2 +258,84 @@ var buf = new Buffer(body, 'utf8'); | ||
/** | ||
* Map html `files`, returning an html unordered list. | ||
* @private | ||
*/ | ||
function createHtmlFileList(files, dir, useIcons, view) { | ||
var html = '<ul id="files" class="view-' + escapeHtml(view) + '">' | ||
+ (view == 'details' ? ( | ||
'<li class="header">' | ||
+ '<span class="name">Name</span>' | ||
+ '<span class="size">Size</span>' | ||
+ '<span class="date">Modified</span>' | ||
+ '</li>') : ''); | ||
html += files.map(function (file) { | ||
var classes = []; | ||
var isDir = file.stat && file.stat.isDirectory(); | ||
var path = dir.split('/').map(function (c) { return encodeURIComponent(c); }); | ||
if (useIcons) { | ||
classes.push('icon'); | ||
if (isDir) { | ||
classes.push('icon-directory'); | ||
} else { | ||
var ext = extname(file.name); | ||
var icon = iconLookup(file.name); | ||
classes.push('icon'); | ||
classes.push('icon-' + ext.substring(1)); | ||
if (classes.indexOf(icon.className) === -1) { | ||
classes.push(icon.className); | ||
} | ||
} | ||
} | ||
path.push(encodeURIComponent(file.name)); | ||
var date = file.stat && file.name !== '..' | ||
? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString() | ||
: ''; | ||
var size = file.stat && !isDir | ||
? file.stat.size | ||
: ''; | ||
return '<li><a href="' | ||
+ escapeHtml(normalizeSlashes(normalize(path.join('/')))) | ||
+ '" class="' + escapeHtml(classes.join(' ')) + '"' | ||
+ ' title="' + escapeHtml(file.name) + '">' | ||
+ '<span class="name">' + escapeHtml(file.name) + '</span>' | ||
+ '<span class="size">' + escapeHtml(size) + '</span>' | ||
+ '<span class="date">' + escapeHtml(date) + '</span>' | ||
+ '</a></li>'; | ||
}).join('\n'); | ||
html += '</ul>'; | ||
return html; | ||
} | ||
/** | ||
* Create function to render html. | ||
*/ | ||
function createHtmlRender(template) { | ||
return function render(locals, callback) { | ||
// read template | ||
fs.readFile(template, 'utf8', function (err, str) { | ||
if (err) return callback(err); | ||
var body = str | ||
.replace(/\{style\}/g, locals.style.concat(iconStyle(locals.fileList, locals.displayIcons))) | ||
.replace(/\{files\}/g, createHtmlFileList(locals.fileList, locals.directory, locals.displayIcons, locals.viewName)) | ||
.replace(/\{directory\}/g, escapeHtml(locals.directory)) | ||
.replace(/\{linked-path\}/g, htmlPath(locals.directory)); | ||
callback(null, body); | ||
}); | ||
}; | ||
} | ||
/** | ||
* Sort function for with directories first. | ||
@@ -234,2 +345,8 @@ */ | ||
function fileSort(a, b) { | ||
// sort ".." to the top | ||
if (a.name === '..' || b.name === '..') { | ||
return a.name === b.name ? 0 | ||
: a.name === '..' ? -1 : 1; | ||
} | ||
return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) || | ||
@@ -321,3 +438,3 @@ String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase()); | ||
function iconStyle (files, useIcons) { | ||
function iconStyle(files, useIcons) { | ||
if (!useIcons) return ''; | ||
@@ -336,3 +453,3 @@ var className; | ||
var isDir = '..' == file.name || (file.stat && file.stat.isDirectory()); | ||
var isDir = file.stat && file.stat.isDirectory(); | ||
var icon = isDir | ||
@@ -365,58 +482,2 @@ ? { className: 'icon-directory', fileName: icons.folder } | ||
/** | ||
* Map html `files`, returning an html unordered list. | ||
*/ | ||
function html(files, dir, useIcons, view) { | ||
return '<ul id="files" class="view-' + escapeHtml(view) + '">' | ||
+ (view == 'details' ? ( | ||
'<li class="header">' | ||
+ '<span class="name">Name</span>' | ||
+ '<span class="size">Size</span>' | ||
+ '<span class="date">Modified</span>' | ||
+ '</li>') : '') | ||
+ files.map(function(file){ | ||
var isDir = '..' == file.name || (file.stat && file.stat.isDirectory()) | ||
, classes = [] | ||
, path = dir.split('/').map(function (c) { return encodeURIComponent(c); }); | ||
if (useIcons) { | ||
classes.push('icon'); | ||
if (isDir) { | ||
classes.push('icon-directory'); | ||
} else { | ||
var ext = extname(file.name); | ||
var icon = iconLookup(file.name); | ||
classes.push('icon'); | ||
classes.push('icon-' + ext.substring(1)); | ||
if (classes.indexOf(icon.className) === -1) { | ||
classes.push(icon.className); | ||
} | ||
} | ||
} | ||
path.push(encodeURIComponent(file.name)); | ||
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="' | ||
+ escapeHtml(normalizeSlashes(normalize(path.join('/')))) | ||
+ '" class="' + escapeHtml(classes.join(' ')) + '"' | ||
+ ' title="' + escapeHtml(file.name) + '">' | ||
+ '<span class="name">' + escapeHtml(file.name) + '</span>' | ||
+ '<span class="size">' + escapeHtml(size) + '</span>' | ||
+ '<span class="date">' + escapeHtml(date) + '</span>' | ||
+ '</a></li>'; | ||
}).join('\n') + '</ul>'; | ||
} | ||
/** | ||
* Load and cache the given `icon`. | ||
@@ -423,0 +484,0 @@ * |
{ | ||
"name": "serve-index", | ||
"description": "Serve directory listings", | ||
"version": "1.6.4", | ||
"version": "1.7.0", | ||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>", | ||
@@ -9,8 +9,8 @@ "license": "MIT", | ||
"dependencies": { | ||
"accepts": "~1.2.7", | ||
"accepts": "~1.2.9", | ||
"batch": "0.5.2", | ||
"debug": "~2.2.0", | ||
"escape-html": "1.0.1", | ||
"escape-html": "1.0.2", | ||
"http-errors": "~1.3.1", | ||
"mime-types": "~2.0.11", | ||
"mime-types": "~2.1.1", | ||
"parseurl": "~1.3.0" | ||
@@ -21,4 +21,4 @@ }, | ||
"istanbul": "0.3.9", | ||
"mocha": "~2.2.4", | ||
"supertest": "~0.15.0" | ||
"mocha": "2.2.5", | ||
"supertest": "1.0.1" | ||
}, | ||
@@ -25,0 +25,0 @@ "files": [ |
@@ -59,5 +59,7 @@ # serve-index | ||
Optional path to an HTML template. Defaults to a built-in template. | ||
Optional path to an HTML template or a function that will render a HTML | ||
string. Defaults to a built-in template. | ||
The following tokens are replaced in templates: | ||
When given a string, the string is used as a file path to load and then the | ||
following tokens are replaced in templates: | ||
@@ -69,2 +71,16 @@ * `{directory}` with the name of the directory. | ||
When given as a function, the function is called as `template(locals, callback)` | ||
and it needs to invoke `callback(error, htmlString)`. The following are the | ||
provided locals: | ||
* `directory` is the directory being displayed (where `/` is the root). | ||
* `displayIcons` is a Boolean for if icons should be rendered or not. | ||
* `fileList` is a sorted array of files in the directory. The array contains | ||
objects with the following properties: | ||
- `name` is the relative name for the file. | ||
- `stat` is a `fs.Stats` object for the file. | ||
* `path` is the full filesystem path to `directory`. | ||
* `style` is the default stylesheet or the contents of the `stylesheet` option. | ||
* `viewName` is the view name provided by the `view` option. | ||
##### view | ||
@@ -71,0 +87,0 @@ |
90906
788
147
+ Addedescape-html@1.0.2(transitive)
- Removedescape-html@1.0.1(transitive)
- Removedmime-db@1.12.0(transitive)
- Removedmime-types@2.0.14(transitive)
Updatedaccepts@~1.2.9
Updatedescape-html@1.0.2
Updatedmime-types@~2.1.1