diff --git a/index.js b/index.js index e707bbc5..0f376ea3 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,21 @@ var cache = {}; */ var defaultTemplate = join(__dirname, 'public', 'directory.html'); +var templates = { + html: { + list: '', + header: '
  • ' + + 'Name' + + 'Size' + + 'Modified' + + '
  • ', + item: '
  • ' + + '{file.name}' + + '{file.size}' + + '{file.lastModified}' + + '
  • ' + } +} /*! * Stylesheet. @@ -154,13 +169,51 @@ function serveIndex(root, options) { }); files.sort(); + // add parent directory as first + if (showUp) { + files.unshift('..'); + } + // content-negotiation var accept = accepts(req); var type = accept.type(mediaTypes); // not acceptable if (!type) return next(createError(406)); - serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet); + + // stat all files + fstat(path, files, function (err, stats) { + if (err) return next(err); + + // 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); + + // make similar to file object (with stat) + var directory = { + name: originalDir, + type: 'inode/directory', + size: stat.size, + lastModified: stat.mtime + } + + var nodes = fileList.map(function (file) { + var ext = extname(file.name) + var mimetype = mime.lookup(ext) + return { + name: file.name, + type: file.stat.isDirectory() ? 'inode/directory' : mimetype, + size: file.stat.size, + lastModified: file.stat.mtime + } + }) + + serveIndex[mediaType[type]](req, res, directory, nodes, next, showUp, icons, path, view, template, stylesheet) + }); }); }); }; @@ -170,46 +223,29 @@ function serveIndex(root, options) { * Respond with text/html. */ -serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) { +serveIndex.html = function _html(req, res, directory, files, next, 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) { + // read stylesheet + fs.readFile(stylesheet, 'utf8', function (err, style) { if (err) return next(err); - // 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); + // create locals for rendering + var locals = { + directory: directory.name, + displayIcons: Boolean(icons), + fileList: files, + path: path, + style: style, + viewName: view + }; - // read stylesheet - fs.readFile(stylesheet, 'utf8', function (err, style) { + // render html + render(locals, function (err, body) { if (err) return next(err); - - // 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); - send(res, 'text/html', body) - }); + send(res, 'text/html', body) }); }); }; @@ -218,36 +254,34 @@ serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path * Respond with application/json. */ -serveIndex.json = function _json(req, res, files) { +serveIndex.json = function _json(req, res, directory, nodes) { + var files = nodes.map(function (file) { return file.name }) send(res, 'application/json', JSON.stringify(files)) -}; +} /** * Respond with text/plain. */ -serveIndex.plain = function _plain(req, res, files) { +serveIndex.plain = function _plain(req, res, directory, nodes) { + var files = nodes.map(function (file) { return file.name }) send(res, 'text/plain', (files.join('\n') + '\n')) -}; +} /** * Map html `files`, returning an html unordered list. * @private */ -function createHtmlFileList(files, dir, useIcons, view) { - var html = ''; - - return html; + return html.replace(/{items}/g, items) } /** @@ -302,10 +332,10 @@ function createHtmlRender(template) { 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)); + .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); }); @@ -421,7 +451,7 @@ function iconStyle(files, useIcons) { for (i = 0; i < files.length; i++) { var file = files[i]; - var isDir = file.stat && file.stat.isDirectory(); + var isDir = 'inode/directory' === file.type var icon = isDir ? { className: 'icon-directory', fileName: icons.folder } : iconLookup(file.name); @@ -511,7 +541,7 @@ function send (res, type, body) { * in same order. */ -function stat(dir, files, cb) { +function fstat(dir, files, cb) { var batch = new Batch(); batch.concurrency(10); diff --git a/test/test.js b/test/test.js index b0c9531d..3b7a0727 100644 --- a/test/test.js +++ b/test/test.js @@ -436,7 +436,9 @@ describe('serveIndex(root)', function () { it('should provide "fileList" local', function (done) { var server = createServer(fixtures, {'template': function (locals, callback) { callback(null, JSON.stringify(locals.fileList.map(function (file) { - file.stat = file.stat instanceof fs.Stats; + file.lastModified = file.lastModified instanceof Date + file.size = file.size >= 0 + file.type = /\//.test(file.type) return file; }))); }}); @@ -444,7 +446,7 @@ describe('serveIndex(root)', function () { request(server) .get('/users/') .set('Accept', 'text/html') - .expect('[{"name":"..","stat":true},{"name":"#dir","stat":true},{"name":"index.html","stat":true},{"name":"tobi.txt","stat":true}]') + .expect('[{"name":"..","type":true,"size":true,"lastModified":true},{"name":"#dir","type":true,"size":true,"lastModified":true},{"name":"index.html","type":true,"size":true,"lastModified":true},{"name":"tobi.txt","type":true,"size":true,"lastModified":true}]') .expect(200, done); }); @@ -504,9 +506,9 @@ describe('serveIndex(root)', function () { it('should get file list', function (done) { var server = createServer() - serveIndex.html = function (req, res, files) { + serveIndex.html = function (req, res, directory, files) { var text = files - .filter(function (f) { return /\.txt$/.test(f) }) + .filter(function (f) { return /\.txt$/.test(f.name) }) .sort() res.setHeader('Content-Type', 'text/html') res.end('' + text.length + ' text files') @@ -521,9 +523,9 @@ describe('serveIndex(root)', function () { it('should get dir name', function (done) { var server = createServer() - serveIndex.html = function (req, res, files, next, dir) { + serveIndex.html = function (req, res, directory, files, next) { res.setHeader('Content-Type', 'text/html') - res.end('' + dir + '') + res.end('' + directory.name + '') } request(server) @@ -535,7 +537,7 @@ describe('serveIndex(root)', function () { it('should get template path', function (done) { var server = createServer() - serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template) { + serveIndex.html = function (req, res, directory, files, next, showUp, icons, path, view, template) { res.setHeader('Content-Type', 'text/html') res.end(String(fs.existsSync(template))) } @@ -549,7 +551,7 @@ describe('serveIndex(root)', function () { it('should get template with tokens', function (done) { var server = createServer() - serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template) { + serveIndex.html = function (req, res, directory, files, next, showUp, icons, path, view, template) { res.setHeader('Content-Type', 'text/html') res.end(fs.readFileSync(template, 'utf8')) } @@ -567,7 +569,7 @@ describe('serveIndex(root)', function () { it('should get stylesheet path', function (done) { var server = createServer() - serveIndex.html = function (req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) { + serveIndex.html = function (req, res, directory, files, next, showUp, icons, path, view, template, stylesheet) { res.setHeader('Content-Type', 'text/html') res.end(String(fs.existsSync(stylesheet))) } @@ -585,7 +587,7 @@ describe('serveIndex(root)', function () { it('should get called with Accept: text/plain', function (done) { var server = createServer() - serveIndex.plain = function (req, res, files) { + serveIndex.plain = function (req, res, directory, files) { res.setHeader('Content-Type', 'text/plain'); res.end('called'); } @@ -603,7 +605,7 @@ describe('serveIndex(root)', function () { it('should get called with Accept: application/json', function (done) { var server = createServer() - serveIndex.json = function (req, res, files) { + serveIndex.json = function (req, res, directory, files) { res.setHeader('Content-Type', 'application/json'); res.end('"called"'); }