pax_global_header 0000666 0000000 0000000 00000000064 12413544255 0014517 g ustar 00root root 0000000 0000000 52 comment=ac94303941ffa08f13b9a73d10de5355a59f0dd2 serve-index-1.4.0/ 0000775 0000000 0000000 00000000000 12413544255 0013752 5 ustar 00root root 0000000 0000000 serve-index-1.4.0/.gitignore 0000664 0000000 0000000 00000000044 12413544255 0015740 0 ustar 00root root 0000000 0000000 coverage node_modules npm-debug.log serve-index-1.4.0/.travis.yml 0000664 0000000 0000000 00000000371 12413544255 0016064 0 ustar 00root root 0000000 0000000 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" serve-index-1.4.0/HISTORY.md 0000664 0000000 0000000 00000003240 12413544255 0015434 0 ustar 00root root 0000000 0000000 1.4.0 / 2014-10-03 ================== * Add `dir` argument to `filter` function * Support using tokens multiple times 1.3.1 / 2014-10-01 ================== * Fix incorrect 403 on Windows and Node.js 0.11 * deps: accepts@~1.1.1 - deps: mime-types@~2.0.2 - deps: negotiator@0.4.8 1.3.0 / 2014-09-20 ================== * Add icon for mkv files * Lookup icon by mime type for greater icon support 1.2.1 / 2014-09-05 ================== * deps: accepts@~1.1.0 * deps: debug@~2.0.0 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 serve-index-1.4.0/LICENSE 0000664 0000000 0000000 00000002240 12413544255 0014755 0 ustar 00root root 0000000 0000000 (The MIT License) Copyright (c) 2010 Sencha Inc. Copyright (c) 2011 LearnBoost Copyright (c) 2011 TJ Holowaychuk 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. serve-index-1.4.0/README.md 0000664 0000000 0000000 00000007131 12413544255 0015233 0 ustar 00root root 0000000 0000000 # 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`. The `filter` function is called for each file, with the signature `filter(filename, index, files, dir)` where `filename` is the name of the file, `index` is the array index, `files` is the array of files and `dir` is the absolute path the file is located (and thus, the directory the listing is for). ##### 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]: https://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/ serve-index-1.4.0/index.js 0000664 0000000 0000000 00000034612 12413544255 0015425 0 ustar 00root root 0000000 0000000 /*! * serve-index * Copyright(c) 2011 Sencha Inc. * Copyright(c) 2011 TJ Holowaychuk * Copyright(c) 2014 Douglas Christopher Wilson * MIT Licensed */ // TODO: arrow key navigation // TODO: make icons extensible /** * Module dependencies. */ var accepts = require('accepts'); var debug = require('debug')('serve-index'); var http = require('http') , fs = require('fs') , path = require('path') , normalize = path.normalize , sep = path.sep , extname = path.extname , join = path.join; var Batch = require('batch'); var mime = require('mime-types'); var parseUrl = require('parseurl'); var resolve = require('path').resolve; /*! * Icon cache. */ var cache = {}; /*! * Default template. */ var defaultTemplate = join(__dirname, 'public', 'directory.html'); /*! * Stylesheet. */ var defaultStylesheet = join(__dirname, 'public', 'style.css'); /** * Media types and the map for content negotiation. */ var mediaTypes = [ 'text/html', 'text/plain', 'application/json' ]; var mediaType = { 'text/html': 'html', 'text/plain': 'plain', 'application/json': 'json' }; /** * Serve directory listings with the given `root` path. * * See Readme.md for documentation of options. * * @param {String} path * @param {Object} options * @return {Function} middleware * @api public */ exports = module.exports = function serveIndex(root, options){ options = options || {}; // root required if (!root) throw new TypeError('serveIndex() root path required'); // resolve root to absolute and normalize root = resolve(root); root = normalize(root + sep); var hidden = options.hidden , icons = options.icons , view = options.view || 'tiles' , filter = options.filter , template = options.template || defaultTemplate , stylesheet = options.stylesheet || defaultStylesheet; 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; } // parse URLs var url = parseUrl(req); var originalUrl = parseUrl.original(req); var dir = decodeURIComponent(url.pathname); var originalDir = decodeURIComponent(originalUrl.pathname); // join / normalize from root dir var path = normalize(join(root, dir)); // null byte(s), bad request if (~path.indexOf('\0')) return next(createError(400)); // malicious path if ((path + sep).substr(0, root.length) !== root) { debug('malicious path "%s"', path); return next(createError(403)); } // determine ".." display var showUp = normalize(resolve(path) + sep) !== root; // check if we have a directory debug('stat "%s"', path); fs.stat(path, function(err, stat){ 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); if (filter) files = files.filter(function(filename, index, list) { return filter(filename, index, list, path); }); files.sort(); // content-negotiation 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, stylesheet); }); }); }; }; /** * Respond with text/html. */ 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){ if (err) return next(err); stat(path, files, function(err, stats){ 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, dir) .replace(/\{linked-path\}/g, htmlPath(dir)); var buf = new Buffer(str, 'utf8'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Length', buf.length); res.end(buf); }); }); }); }; /** * Respond with application/json. */ exports.json = function(req, res, 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); }; /** * Respond with text/plain. */ exports.plain = function(req, res, 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); }; /** * Generate an `Error` from the given status `code` * and optional `msg`. * * @param {Number} code * @param {String} msg * @return {Error} * @api private */ function createError(code, msg) { var err = new Error(msg || http.STATUS_CODES[code]); err.status = code; return err; }; /** * Sort function for with directories first. */ function fileSort(a, b) { return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) || String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase()); } /** * Map html `dir`, returning a linked path. */ function htmlPath(dir) { var curr = []; return dir.split('/').map(function(part){ curr.push(encodeURIComponent(part)); return part ? '' + part + '' : ''; }).join(' / '); } /** * Get the icon data for the file name. */ function iconLookup(filename) { var ext = extname(filename); // try by extension if (icons[ext]) { return { className: 'icon-' + ext.substring(1), fileName: icons[ext] }; } var mimetype = mime.lookup(ext); // default if no mime type if (mimetype === false) { return { className: 'icon-default', fileName: icons.default }; } // try by mime type if (icons[mimetype]) { return { className: 'icon-' + mimetype.replace('/', '-'), fileName: icons[mimetype] }; } var suffix = mimetype.split('+')[1]; if (suffix && icons['+' + suffix]) { return { className: 'icon-' + suffix, fileName: icons['+' + suffix] }; } var type = mimetype.split('/')[0]; // try by type only if (icons[type]) { return { className: 'icon-' + type, fileName: icons[type] }; } return { className: 'icon-default', fileName: icons.default }; } /** * Load icon images, return css string. */ function iconStyle (files, useIcons) { if (!useIcons) return ''; var className; var i; var iconName; var list = []; var rules = {}; var selector; var selectors = {}; var style = ''; for (i = 0; i < files.length; i++) { var file = files[i]; var isDir = '..' == file.name || (file.stat && file.stat.isDirectory()); var icon = isDir ? { className: 'icon-directory', fileName: icons.folder } : iconLookup(file.name); var iconName = icon.fileName; selector = '#files .' + icon.className + ' .name'; if (!rules[iconName]) { rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');' selectors[iconName] = []; list.push(iconName); } if (selectors[iconName].indexOf(selector) === -1) { selectors[iconName].push(selector); } } for (i = 0; i < list.length; i++) { iconName = list[i]; style += selectors[iconName].join(',\n') + ' {\n ' + rules[iconName] + '\n}\n'; } return style; } /** * Map html `files`, returning an html unordered list. */ function html(files, dir, useIcons, view) { return '