index.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. 'use strict';
  2. const styles = require('./styles');
  3. const lastModifiedToString = require('./last-modified-to-string');
  4. const permsToString = require('./perms-to-string');
  5. const sizeToString = require('./size-to-string');
  6. const sortFiles = require('./sort-files');
  7. const fs = require('fs');
  8. const path = require('path');
  9. const he = require('he');
  10. const etag = require('../etag');
  11. const url = require('url');
  12. const status = require('../status-handlers');
  13. const supportedIcons = styles.icons;
  14. const css = styles.css;
  15. module.exports = (opts) => {
  16. // opts are parsed by opts.js, defaults already applied
  17. const cache = opts.cache;
  18. const root = path.resolve(opts.root);
  19. const baseDir = opts.baseDir;
  20. const humanReadable = opts.humanReadable;
  21. const hidePermissions = opts.hidePermissions;
  22. const handleError = opts.handleError;
  23. const showDotfiles = opts.showDotfiles;
  24. const si = opts.si;
  25. const weakEtags = opts.weakEtags;
  26. return function middleware(req, res, next) {
  27. // Figure out the path for the file from the given url
  28. const parsed = url.parse(req.url);
  29. const pathname = decodeURIComponent(parsed.pathname);
  30. const dir = path.normalize(
  31. path.join(
  32. root,
  33. path.relative(
  34. path.join('/', baseDir),
  35. pathname
  36. )
  37. )
  38. );
  39. fs.stat(dir, (statErr, stat) => {
  40. if (statErr) {
  41. if (handleError) {
  42. status[500](res, next, { error: statErr });
  43. } else {
  44. next();
  45. }
  46. return;
  47. }
  48. // files are the listing of dir
  49. fs.readdir(dir, (readErr, _files) => {
  50. let files = _files;
  51. if (readErr) {
  52. if (handleError) {
  53. status[500](res, next, { error: readErr });
  54. } else {
  55. next();
  56. }
  57. return;
  58. }
  59. // Optionally exclude dotfiles from directory listing.
  60. if (!showDotfiles) {
  61. files = files.filter(filename => filename.slice(0, 1) !== '.');
  62. }
  63. res.setHeader('content-type', 'text/html');
  64. res.setHeader('etag', etag(stat, weakEtags));
  65. res.setHeader('last-modified', (new Date(stat.mtime)).toUTCString());
  66. res.setHeader('cache-control', cache);
  67. function render(dirs, renderFiles, lolwuts) {
  68. // each entry in the array is a [name, stat] tuple
  69. let html = `${[
  70. '<!doctype html>',
  71. '<html>',
  72. ' <head>',
  73. ' <meta charset="utf-8">',
  74. ' <meta name="viewport" content="width=device-width">',
  75. ` <title>Index of ${he.encode(pathname)}</title>`,
  76. ` <style type="text/css">${css}</style>`,
  77. ' </head>',
  78. ' <body>',
  79. `<h1>Index of ${he.encode(pathname)}</h1>`,
  80. ].join('\n')}\n`;
  81. html += '<table>';
  82. const failed = false;
  83. const writeRow = (file) => {
  84. // render a row given a [name, stat] tuple
  85. const isDir = file[1].isDirectory && file[1].isDirectory();
  86. let href = `./${encodeURIComponent(file[0])}`;
  87. // append trailing slash and query for dir entry
  88. if (isDir) {
  89. href += `/${he.encode((parsed.search) ? parsed.search : '')}`;
  90. }
  91. const displayName = he.encode(file[0]) + ((isDir) ? '/' : '');
  92. const ext = file[0].split('.').pop();
  93. const classForNonDir = supportedIcons[ext] ? ext : '_page';
  94. const iconClass = `icon-${isDir ? '_blank' : classForNonDir}`;
  95. // TODO: use stylessheets?
  96. html += `${'<tr>' +
  97. '<td><i class="icon '}${iconClass}"></i></td>`;
  98. if (!hidePermissions) {
  99. html += `<td class="perms"><code>(${permsToString(file[1])})</code></td>`;
  100. }
  101. html +=
  102. `<td class="last-modified">${lastModifiedToString(file[1])}</td>` +
  103. `<td class="file-size"><code>${sizeToString(file[1], humanReadable, si)}</code></td>` +
  104. `<td class="display-name"><a href="${href}">${displayName}</a></td>` +
  105. '</tr>\n';
  106. };
  107. dirs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);
  108. renderFiles.sort((a, b) => a.toString().localeCompare(b.toString())).forEach(writeRow);
  109. lolwuts.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);
  110. html += '</table>\n';
  111. html += `<br><address>Node.js ${
  112. process.version
  113. }/ <a href="https://github.com/http-party/http-server">http-server</a> ` +
  114. `server running @ ${
  115. he.encode(req.headers.host || '')}</address>\n` +
  116. '</body></html>'
  117. ;
  118. if (!failed) {
  119. res.writeHead(200, { 'Content-Type': 'text/html' });
  120. res.end(html);
  121. }
  122. }
  123. sortFiles(dir, files, (lolwuts, dirs, sortedFiles) => {
  124. // It's possible to get stat errors for all sorts of reasons here.
  125. // Unfortunately, our two choices are to either bail completely,
  126. // or just truck along as though everything's cool. In this case,
  127. // I decided to just tack them on as "??!?" items along with dirs
  128. // and files.
  129. //
  130. // Whatever.
  131. // if it makes sense to, add a .. link
  132. if (path.resolve(dir, '..').slice(0, root.length) === root) {
  133. fs.stat(path.join(dir, '..'), (err, s) => {
  134. if (err) {
  135. if (handleError) {
  136. status[500](res, next, { error: err });
  137. } else {
  138. next();
  139. }
  140. return;
  141. }
  142. dirs.unshift(['..', s]);
  143. render(dirs, sortedFiles, lolwuts);
  144. });
  145. } else {
  146. render(dirs, sortedFiles, lolwuts);
  147. }
  148. });
  149. });
  150. });
  151. };
  152. };