github.com/emc-advanced-dev/unik@v0.0.0-20190717152701-a58d3e8e33b7/docs/examples/example-nodejs-fileserver/node_modules/serve-index/index.js (about)

     1  /*!
     2   * serve-index
     3   * Copyright(c) 2011 Sencha Inc.
     4   * Copyright(c) 2011 TJ Holowaychuk
     5   * Copyright(c) 2014-2015 Douglas Christopher Wilson
     6   * MIT Licensed
     7   */
     8  
     9  'use strict';
    10  
    11  /**
    12   * Module dependencies.
    13   * @private
    14   */
    15  
    16  var accepts = require('accepts');
    17  var createError = require('http-errors');
    18  var debug = require('debug')('serve-index');
    19  var escapeHtml = require('escape-html');
    20  var fs = require('fs')
    21    , path = require('path')
    22    , normalize = path.normalize
    23    , sep = path.sep
    24    , extname = path.extname
    25    , join = path.join;
    26  var Batch = require('batch');
    27  var mime = require('mime-types');
    28  var parseUrl = require('parseurl');
    29  var resolve = require('path').resolve;
    30  
    31  /**
    32   * Module exports.
    33   * @public
    34   */
    35  
    36  module.exports = serveIndex;
    37  
    38  /*!
    39   * Icon cache.
    40   */
    41  
    42  var cache = {};
    43  
    44  /*!
    45   * Default template.
    46   */
    47  
    48  var defaultTemplate = join(__dirname, 'public', 'directory.html');
    49  
    50  /*!
    51   * Stylesheet.
    52   */
    53  
    54  var defaultStylesheet = join(__dirname, 'public', 'style.css');
    55  
    56  /**
    57   * Media types and the map for content negotiation.
    58   */
    59  
    60  var mediaTypes = [
    61    'text/html',
    62    'text/plain',
    63    'application/json'
    64  ];
    65  
    66  var mediaType = {
    67    'text/html': 'html',
    68    'text/plain': 'plain',
    69    'application/json': 'json'
    70  };
    71  
    72  /**
    73   * Serve directory listings with the given `root` path.
    74   *
    75   * See Readme.md for documentation of options.
    76   *
    77   * @param {String} root
    78   * @param {Object} options
    79   * @return {Function} middleware
    80   * @public
    81   */
    82  
    83  function serveIndex(root, options) {
    84    var opts = options || {};
    85  
    86    // root required
    87    if (!root) {
    88      throw new TypeError('serveIndex() root path required');
    89    }
    90  
    91    // resolve root to absolute and normalize
    92    var rootPath = normalize(resolve(root) + sep);
    93  
    94    var filter = opts.filter;
    95    var hidden = opts.hidden;
    96    var icons = opts.icons;
    97    var stylesheet = opts.stylesheet || defaultStylesheet;
    98    var template = opts.template || defaultTemplate;
    99    var view = opts.view || 'tiles';
   100  
   101    return function (req, res, next) {
   102      if (req.method !== 'GET' && req.method !== 'HEAD') {
   103        res.statusCode = 'OPTIONS' === req.method ? 200 : 405;
   104        res.setHeader('Allow', 'GET, HEAD, OPTIONS');
   105        res.setHeader('Content-Length', '0');
   106        res.end();
   107        return;
   108      }
   109  
   110      // parse URLs
   111      var url = parseUrl(req);
   112      var originalUrl = parseUrl.original(req);
   113      var dir = decodeURIComponent(url.pathname);
   114      var originalDir = decodeURIComponent(originalUrl.pathname);
   115  
   116      // join / normalize from root dir
   117      var path = normalize(join(rootPath, dir));
   118  
   119      // null byte(s), bad request
   120      if (~path.indexOf('\0')) return next(createError(400));
   121  
   122      // malicious path
   123      if ((path + sep).substr(0, rootPath.length) !== rootPath) {
   124        debug('malicious path "%s"', path);
   125        return next(createError(403));
   126      }
   127  
   128      // determine ".." display
   129      var showUp = normalize(resolve(path) + sep) !== rootPath;
   130  
   131      // check if we have a directory
   132      debug('stat "%s"', path);
   133      fs.stat(path, function(err, stat){
   134        if (err && err.code === 'ENOENT') {
   135          return next();
   136        }
   137  
   138        if (err) {
   139          err.status = err.code === 'ENAMETOOLONG'
   140            ? 414
   141            : 500;
   142          return next(err);
   143        }
   144  
   145        if (!stat.isDirectory()) return next();
   146  
   147        // fetch files
   148        debug('readdir "%s"', path);
   149        fs.readdir(path, function(err, files){
   150          if (err) return next(err);
   151          if (!hidden) files = removeHidden(files);
   152          if (filter) files = files.filter(function(filename, index, list) {
   153            return filter(filename, index, list, path);
   154          });
   155          files.sort();
   156  
   157          // content-negotiation
   158          var accept = accepts(req);
   159          var type = accept.type(mediaTypes);
   160  
   161          // not acceptable
   162          if (!type) return next(createError(406));
   163          serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet);
   164        });
   165      });
   166    };
   167  };
   168  
   169  /**
   170   * Respond with text/html.
   171   */
   172  
   173  serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet) {
   174    var render = typeof template !== 'function'
   175      ? createHtmlRender(template)
   176      : template
   177  
   178    if (showUp) {
   179      files.unshift('..');
   180    }
   181  
   182    // stat all files
   183    stat(path, files, function (err, stats) {
   184      if (err) return next(err);
   185  
   186      // combine the stats into the file list
   187      var fileList = files.map(function (file, i) {
   188        return { name: file, stat: stats[i] };
   189      });
   190  
   191      // sort file list
   192      fileList.sort(fileSort);
   193  
   194      // read stylesheet
   195      fs.readFile(stylesheet, 'utf8', function (err, style) {
   196        if (err) return next(err);
   197  
   198        // create locals for rendering
   199        var locals = {
   200          directory: dir,
   201          displayIcons: Boolean(icons),
   202          fileList: fileList,
   203          path: path,
   204          style: style,
   205          viewName: view
   206        };
   207  
   208        // render html
   209        render(locals, function (err, body) {
   210          if (err) return next(err);
   211  
   212          var buf = new Buffer(body, 'utf8');
   213          res.setHeader('Content-Type', 'text/html; charset=utf-8');
   214          res.setHeader('Content-Length', buf.length);
   215          res.end(buf);
   216        });
   217      });
   218    });
   219  };
   220  
   221  /**
   222   * Respond with application/json.
   223   */
   224  
   225  serveIndex.json = function _json(req, res, files) {
   226    var body = JSON.stringify(files);
   227    var buf = new Buffer(body, 'utf8');
   228  
   229    res.setHeader('Content-Type', 'application/json; charset=utf-8');
   230    res.setHeader('Content-Length', buf.length);
   231    res.end(buf);
   232  };
   233  
   234  /**
   235   * Respond with text/plain.
   236   */
   237  
   238  serveIndex.plain = function _plain(req, res, files) {
   239    var body = files.join('\n') + '\n';
   240    var buf = new Buffer(body, 'utf8');
   241  
   242    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
   243    res.setHeader('Content-Length', buf.length);
   244    res.end(buf);
   245  };
   246  
   247  /**
   248   * Map html `files`, returning an html unordered list.
   249   * @private
   250   */
   251  
   252  function createHtmlFileList(files, dir, useIcons, view) {
   253    var html = '<ul id="files" class="view-' + escapeHtml(view) + '">'
   254      + (view == 'details' ? (
   255        '<li class="header">'
   256        + '<span class="name">Name</span>'
   257        + '<span class="size">Size</span>'
   258        + '<span class="date">Modified</span>'
   259        + '</li>') : '');
   260  
   261    html += files.map(function (file) {
   262      var classes = [];
   263      var isDir = file.stat && file.stat.isDirectory();
   264      var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
   265  
   266      if (useIcons) {
   267        classes.push('icon');
   268  
   269        if (isDir) {
   270          classes.push('icon-directory');
   271        } else {
   272          var ext = extname(file.name);
   273          var icon = iconLookup(file.name);
   274  
   275          classes.push('icon');
   276          classes.push('icon-' + ext.substring(1));
   277  
   278          if (classes.indexOf(icon.className) === -1) {
   279            classes.push(icon.className);
   280          }
   281        }
   282      }
   283  
   284      path.push(encodeURIComponent(file.name));
   285  
   286      var date = file.stat && file.name !== '..'
   287        ? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
   288        : '';
   289      var size = file.stat && !isDir
   290        ? file.stat.size
   291        : '';
   292  
   293      return '<li><a href="'
   294        + escapeHtml(normalizeSlashes(normalize(path.join('/'))))
   295        + '" class="' + escapeHtml(classes.join(' ')) + '"'
   296        + ' title="' + escapeHtml(file.name) + '">'
   297        + '<span class="name">' + escapeHtml(file.name) + '</span>'
   298        + '<span class="size">' + escapeHtml(size) + '</span>'
   299        + '<span class="date">' + escapeHtml(date) + '</span>'
   300        + '</a></li>';
   301    }).join('\n');
   302  
   303    html += '</ul>';
   304  
   305    return html;
   306  }
   307  
   308  /**
   309   * Create function to render html.
   310   */
   311  
   312  function createHtmlRender(template) {
   313    return function render(locals, callback) {
   314      // read template
   315      fs.readFile(template, 'utf8', function (err, str) {
   316        if (err) return callback(err);
   317  
   318        var body = str
   319          .replace(/\{style\}/g, locals.style.concat(iconStyle(locals.fileList, locals.displayIcons)))
   320          .replace(/\{files\}/g, createHtmlFileList(locals.fileList, locals.directory, locals.displayIcons, locals.viewName))
   321          .replace(/\{directory\}/g, escapeHtml(locals.directory))
   322          .replace(/\{linked-path\}/g, htmlPath(locals.directory));
   323  
   324        callback(null, body);
   325      });
   326    };
   327  }
   328  
   329  /**
   330   * Sort function for with directories first.
   331   */
   332  
   333  function fileSort(a, b) {
   334    // sort ".." to the top
   335    if (a.name === '..' || b.name === '..') {
   336      return a.name === b.name ? 0
   337        : a.name === '..' ? -1 : 1;
   338    }
   339  
   340    return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) ||
   341      String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());
   342  }
   343  
   344  /**
   345   * Map html `dir`, returning a linked path.
   346   */
   347  
   348  function htmlPath(dir) {
   349    var parts = dir.split('/');
   350    var crumb = new Array(parts.length);
   351  
   352    for (var i = 0; i < parts.length; i++) {
   353      var part = parts[i];
   354  
   355      if (part) {
   356        parts[i] = encodeURIComponent(part);
   357        crumb[i] = '<a href="' + escapeHtml(parts.slice(0, i + 1).join('/')) + '">' + escapeHtml(part) + '</a>';
   358      }
   359    }
   360  
   361    return crumb.join(' / ');
   362  }
   363  
   364  /**
   365   * Get the icon data for the file name.
   366   */
   367  
   368  function iconLookup(filename) {
   369    var ext = extname(filename);
   370  
   371    // try by extension
   372    if (icons[ext]) {
   373      return {
   374        className: 'icon-' + ext.substring(1),
   375        fileName: icons[ext]
   376      };
   377    }
   378  
   379    var mimetype = mime.lookup(ext);
   380  
   381    // default if no mime type
   382    if (mimetype === false) {
   383      return {
   384        className: 'icon-default',
   385        fileName: icons.default
   386      };
   387    }
   388  
   389    // try by mime type
   390    if (icons[mimetype]) {
   391      return {
   392        className: 'icon-' + mimetype.replace('/', '-'),
   393        fileName: icons[mimetype]
   394      };
   395    }
   396  
   397    var suffix = mimetype.split('+')[1];
   398  
   399    if (suffix && icons['+' + suffix]) {
   400      return {
   401        className: 'icon-' + suffix,
   402        fileName: icons['+' + suffix]
   403      };
   404    }
   405  
   406    var type = mimetype.split('/')[0];
   407  
   408    // try by type only
   409    if (icons[type]) {
   410      return {
   411        className: 'icon-' + type,
   412        fileName: icons[type]
   413      };
   414    }
   415  
   416    return {
   417      className: 'icon-default',
   418      fileName: icons.default
   419    };
   420  }
   421  
   422  /**
   423   * Load icon images, return css string.
   424   */
   425  
   426  function iconStyle(files, useIcons) {
   427    if (!useIcons) return '';
   428    var className;
   429    var i;
   430    var iconName;
   431    var list = [];
   432    var rules = {};
   433    var selector;
   434    var selectors = {};
   435    var style = '';
   436  
   437    for (i = 0; i < files.length; i++) {
   438      var file = files[i];
   439  
   440      var isDir = file.stat && file.stat.isDirectory();
   441      var icon = isDir
   442        ? { className: 'icon-directory', fileName: icons.folder }
   443        : iconLookup(file.name);
   444      var iconName = icon.fileName;
   445  
   446      selector = '#files .' + icon.className + ' .name';
   447  
   448      if (!rules[iconName]) {
   449        rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');'
   450        selectors[iconName] = [];
   451        list.push(iconName);
   452      }
   453  
   454      if (selectors[iconName].indexOf(selector) === -1) {
   455        selectors[iconName].push(selector);
   456      }
   457    }
   458  
   459    for (i = 0; i < list.length; i++) {
   460      iconName = list[i];
   461      style += selectors[iconName].join(',\n') + ' {\n  ' + rules[iconName] + '\n}\n';
   462    }
   463  
   464    return style;
   465  }
   466  
   467  /**
   468   * Load and cache the given `icon`.
   469   *
   470   * @param {String} icon
   471   * @return {String}
   472   * @api private
   473   */
   474  
   475  function load(icon) {
   476    if (cache[icon]) return cache[icon];
   477    return cache[icon] = fs.readFileSync(__dirname + '/public/icons/' + icon, 'base64');
   478  }
   479  
   480  /**
   481   * Normalizes the path separator from system separator
   482   * to URL separator, aka `/`.
   483   *
   484   * @param {String} path
   485   * @return {String}
   486   * @api private
   487   */
   488  
   489  function normalizeSlashes(path) {
   490    return path.split(sep).join('/');
   491  };
   492  
   493  /**
   494   * Filter "hidden" `files`, aka files
   495   * beginning with a `.`.
   496   *
   497   * @param {Array} files
   498   * @return {Array}
   499   * @api private
   500   */
   501  
   502  function removeHidden(files) {
   503    return files.filter(function(file){
   504      return '.' != file[0];
   505    });
   506  }
   507  
   508  /**
   509   * Stat all files and return array of stat
   510   * in same order.
   511   */
   512  
   513  function stat(dir, files, cb) {
   514    var batch = new Batch();
   515  
   516    batch.concurrency(10);
   517  
   518    files.forEach(function(file){
   519      batch.push(function(done){
   520        fs.stat(join(dir, file), function(err, stat){
   521          if (err && err.code !== 'ENOENT') return done(err);
   522  
   523          // pass ENOENT as null stat, not error
   524          done(null, stat || null);
   525        });
   526      });
   527    });
   528  
   529    batch.end(cb);
   530  }
   531  
   532  /**
   533   * Icon map.
   534   */
   535  
   536  var icons = {
   537    // base icons
   538    'default': 'page_white.png',
   539    'folder': 'folder.png',
   540  
   541    // generic mime type icons
   542    'image': 'image.png',
   543    'text': 'page_white_text.png',
   544    'video': 'film.png',
   545  
   546    // generic mime suffix icons
   547    '+json': 'page_white_code.png',
   548    '+xml': 'page_white_code.png',
   549    '+zip': 'box.png',
   550  
   551    // specific mime type icons
   552    'application/font-woff': 'font.png',
   553    'application/javascript': 'page_white_code_red.png',
   554    'application/json': 'page_white_code.png',
   555    'application/msword': 'page_white_word.png',
   556    'application/pdf': 'page_white_acrobat.png',
   557    'application/postscript': 'page_white_vector.png',
   558    'application/rtf': 'page_white_word.png',
   559    'application/vnd.ms-excel': 'page_white_excel.png',
   560    'application/vnd.ms-powerpoint': 'page_white_powerpoint.png',
   561    'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png',
   562    'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png',
   563    'application/vnd.oasis.opendocument.text': 'page_white_word.png',
   564    'application/x-7z-compressed': 'box.png',
   565    'application/x-sh': 'application_xp_terminal.png',
   566    'application/x-font-ttf': 'font.png',
   567    'application/x-msaccess': 'page_white_database.png',
   568    'application/x-shockwave-flash': 'page_white_flash.png',
   569    'application/x-sql': 'page_white_database.png',
   570    'application/x-tar': 'box.png',
   571    'application/x-xz': 'box.png',
   572    'application/xml': 'page_white_code.png',
   573    'application/zip': 'box.png',
   574    'image/svg+xml': 'page_white_vector.png',
   575    'text/css': 'page_white_code.png',
   576    'text/html': 'page_white_code.png',
   577    'text/less': 'page_white_code.png',
   578  
   579    // other, extension-specific icons
   580    '.accdb': 'page_white_database.png',
   581    '.apk': 'box.png',
   582    '.app': 'application_xp.png',
   583    '.as': 'page_white_actionscript.png',
   584    '.asp': 'page_white_code.png',
   585    '.aspx': 'page_white_code.png',
   586    '.bat': 'application_xp_terminal.png',
   587    '.bz2': 'box.png',
   588    '.c': 'page_white_c.png',
   589    '.cab': 'box.png',
   590    '.cfm': 'page_white_coldfusion.png',
   591    '.clj': 'page_white_code.png',
   592    '.cc': 'page_white_cplusplus.png',
   593    '.cgi': 'application_xp_terminal.png',
   594    '.cpp': 'page_white_cplusplus.png',
   595    '.cs': 'page_white_csharp.png',
   596    '.db': 'page_white_database.png',
   597    '.dbf': 'page_white_database.png',
   598    '.deb': 'box.png',
   599    '.dll': 'page_white_gear.png',
   600    '.dmg': 'drive.png',
   601    '.docx': 'page_white_word.png',
   602    '.erb': 'page_white_ruby.png',
   603    '.exe': 'application_xp.png',
   604    '.fnt': 'font.png',
   605    '.gam': 'controller.png',
   606    '.gz': 'box.png',
   607    '.h': 'page_white_h.png',
   608    '.ini': 'page_white_gear.png',
   609    '.iso': 'cd.png',
   610    '.jar': 'box.png',
   611    '.java': 'page_white_cup.png',
   612    '.jsp': 'page_white_cup.png',
   613    '.lua': 'page_white_code.png',
   614    '.lz': 'box.png',
   615    '.lzma': 'box.png',
   616    '.m': 'page_white_code.png',
   617    '.map': 'map.png',
   618    '.msi': 'box.png',
   619    '.mv4': 'film.png',
   620    '.otf': 'font.png',
   621    '.pdb': 'page_white_database.png',
   622    '.php': 'page_white_php.png',
   623    '.pl': 'page_white_code.png',
   624    '.pkg': 'box.png',
   625    '.pptx': 'page_white_powerpoint.png',
   626    '.psd': 'page_white_picture.png',
   627    '.py': 'page_white_code.png',
   628    '.rar': 'box.png',
   629    '.rb': 'page_white_ruby.png',
   630    '.rm': 'film.png',
   631    '.rom': 'controller.png',
   632    '.rpm': 'box.png',
   633    '.sass': 'page_white_code.png',
   634    '.sav': 'controller.png',
   635    '.scss': 'page_white_code.png',
   636    '.srt': 'page_white_text.png',
   637    '.tbz2': 'box.png',
   638    '.tgz': 'box.png',
   639    '.tlz': 'box.png',
   640    '.vb': 'page_white_code.png',
   641    '.vbs': 'page_white_code.png',
   642    '.xcf': 'page_white_picture.png',
   643    '.xlsx': 'page_white_excel.png',
   644    '.yaws': 'page_white_code.png'
   645  };