| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646 | /*! * serve-index * Copyright(c) 2011 Sencha Inc. * Copyright(c) 2011 TJ Holowaychuk * Copyright(c) 2014-2015 Douglas Christopher Wilson * MIT Licensed */'use strict';/** * Module dependencies. * @private */var accepts = require('accepts');var createError = require('http-errors');var debug = require('debug')('serve-index');var escapeHtml = require('escape-html');var 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;/** * Module exports. * @public */module.exports = serveIndex;/*! * 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} root * @param {Object} options * @return {Function} middleware * @public */function serveIndex(root, options) {  var opts = options || {};  // root required  if (!root) {    throw new TypeError('serveIndex() root path required');  }  // resolve root to absolute and normalize  var rootPath = normalize(resolve(root) + sep);  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 (req, res, next) {    if (req.method !== 'GET' && req.method !== 'HEAD') {      res.statusCode = 'OPTIONS' === req.method ? 200 : 405;      res.setHeader('Allow', 'GET, HEAD, OPTIONS');      res.setHeader('Content-Length', '0');      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(rootPath, dir));    // null byte(s), bad request    if (~path.indexOf('\0')) return next(createError(400));    // malicious path    if ((path + sep).substr(0, rootPath.length) !== rootPath) {      debug('malicious path "%s"', path);      return next(createError(403));    }    // determine ".." display    var showUp = normalize(resolve(path) + sep) !== rootPath;    // 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.type(mediaTypes);        // not acceptable        if (!type) return next(createError(406));        serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet);      });    });  };};/** * Respond with text/html. */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);    // 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);      // 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)      });    });  });};/** * Respond with application/json. */serveIndex.json = function _json(req, res, files) {  send(res, 'application/json', JSON.stringify(files))};/** * Respond with text/plain. */serveIndex.plain = function _plain(req, res, files) {  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 = '<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. */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()) ||    String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());}/** * Map html `dir`, returning a linked path. */function htmlPath(dir) {  var parts = dir.split('/');  var crumb = new Array(parts.length);  for (var i = 0; i < parts.length; i++) {    var part = parts[i];    if (part) {      parts[i] = encodeURIComponent(part);      crumb[i] = '<a href="' + escapeHtml(parts.slice(0, i + 1).join('/')) + '">' + escapeHtml(part) + '</a>';    }  }  return crumb.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 i;  var list = [];  var rules = {};  var selector;  var selectors = {};  var style = '';  for (i = 0; i < files.length; i++) {    var file = files[i];    var isDir = 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;}/** * Load and cache the given `icon`. * * @param {String} icon * @return {String} * @api private */function load(icon) {  if (cache[icon]) return cache[icon];  return cache[icon] = fs.readFileSync(__dirname + '/public/icons/' + icon, 'base64');}/** * Normalizes the path separator from system separator * to URL separator, aka `/`. * * @param {String} path * @return {String} * @api private */function normalizeSlashes(path) {  return path.split(sep).join('/');};/** * Filter "hidden" `files`, aka files * beginning with a `.`. * * @param {Array} files * @return {Array} * @api private */function removeHidden(files) {  return files.filter(function(file){    return '.' != file[0];  });}/** * Send a response. * @private */function send (res, type, body) {  // security header for content sniffing  res.setHeader('X-Content-Type-Options', 'nosniff')  // standard headers  res.setHeader('Content-Type', type + '; charset=utf-8')  res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))  // body  res.end(body, 'utf8')}/** * Stat all files and return array of stat * in same order. */function stat(dir, files, cb) {  var batch = new Batch();  batch.concurrency(10);  files.forEach(function(file){    batch.push(function(done){      fs.stat(join(dir, file), function(err, stat){        if (err && err.code !== 'ENOENT') return done(err);        // pass ENOENT as null stat, not error        done(null, stat || null);      });    });  });  batch.end(cb);}/** * Icon map. */var icons = {  // base icons  'default': 'page_white.png',  'folder': 'folder.png',  // generic mime type icons  'image': 'image.png',  'text': 'page_white_text.png',  'video': 'film.png',  // generic mime suffix icons  '+json': 'page_white_code.png',  '+xml': 'page_white_code.png',  '+zip': 'box.png',  // specific mime type icons  'application/font-woff': 'font.png',  'application/javascript': 'page_white_code_red.png',  'application/json': 'page_white_code.png',  'application/msword': 'page_white_word.png',  'application/pdf': 'page_white_acrobat.png',  'application/postscript': 'page_white_vector.png',  'application/rtf': 'page_white_word.png',  'application/vnd.ms-excel': 'page_white_excel.png',  'application/vnd.ms-powerpoint': 'page_white_powerpoint.png',  'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png',  'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png',  'application/vnd.oasis.opendocument.text': 'page_white_word.png',  'application/x-7z-compressed': 'box.png',  'application/x-sh': 'application_xp_terminal.png',  'application/x-font-ttf': 'font.png',  'application/x-msaccess': 'page_white_database.png',  'application/x-shockwave-flash': 'page_white_flash.png',  'application/x-sql': 'page_white_database.png',  'application/x-tar': 'box.png',  'application/x-xz': 'box.png',  'application/xml': 'page_white_code.png',  'application/zip': 'box.png',  'image/svg+xml': 'page_white_vector.png',  'text/css': 'page_white_code.png',  'text/html': 'page_white_code.png',  'text/less': 'page_white_code.png',  // other, extension-specific icons  '.accdb': 'page_white_database.png',  '.apk': 'box.png',  '.app': 'application_xp.png',  '.as': 'page_white_actionscript.png',  '.asp': 'page_white_code.png',  '.aspx': 'page_white_code.png',  '.bat': 'application_xp_terminal.png',  '.bz2': 'box.png',  '.c': 'page_white_c.png',  '.cab': 'box.png',  '.cfm': 'page_white_coldfusion.png',  '.clj': 'page_white_code.png',  '.cc': 'page_white_cplusplus.png',  '.cgi': 'application_xp_terminal.png',  '.cpp': 'page_white_cplusplus.png',  '.cs': 'page_white_csharp.png',  '.db': 'page_white_database.png',  '.dbf': 'page_white_database.png',  '.deb': 'box.png',  '.dll': 'page_white_gear.png',  '.dmg': 'drive.png',  '.docx': 'page_white_word.png',  '.erb': 'page_white_ruby.png',  '.exe': 'application_xp.png',  '.fnt': 'font.png',  '.gam': 'controller.png',  '.gz': 'box.png',  '.h': 'page_white_h.png',  '.ini': 'page_white_gear.png',  '.iso': 'cd.png',  '.jar': 'box.png',  '.java': 'page_white_cup.png',  '.jsp': 'page_white_cup.png',  '.lua': 'page_white_code.png',  '.lz': 'box.png',  '.lzma': 'box.png',  '.m': 'page_white_code.png',  '.map': 'map.png',  '.msi': 'box.png',  '.mv4': 'film.png',  '.otf': 'font.png',  '.pdb': 'page_white_database.png',  '.php': 'page_white_php.png',  '.pl': 'page_white_code.png',  '.pkg': 'box.png',  '.pptx': 'page_white_powerpoint.png',  '.psd': 'page_white_picture.png',  '.py': 'page_white_code.png',  '.rar': 'box.png',  '.rb': 'page_white_ruby.png',  '.rm': 'film.png',  '.rom': 'controller.png',  '.rpm': 'box.png',  '.sass': 'page_white_code.png',  '.sav': 'controller.png',  '.scss': 'page_white_code.png',  '.srt': 'page_white_text.png',  '.tbz2': 'box.png',  '.tgz': 'box.png',  '.tlz': 'box.png',  '.vb': 'page_white_code.png',  '.vbs': 'page_white_code.png',  '.xcf': 'page_white_picture.png',  '.xlsx': 'page_white_excel.png',  '.yaws': 'page_white_code.png'};
 |