| 'use strict'; |
| |
| const styles = require('./styles'); |
| const lastModifiedToString = require('./last-modified-to-string'); |
| const permsToString = require('./perms-to-string'); |
| const sizeToString = require('./size-to-string'); |
| const sortFiles = require('./sort-files'); |
| const fs = require('fs'); |
| const path = require('path'); |
| const he = require('he'); |
| const etag = require('../etag'); |
| const url = require('url'); |
| const status = require('../status-handlers'); |
| |
| const supportedIcons = styles.icons; |
| const css = styles.css; |
| |
| module.exports = (opts) => { |
| // opts are parsed by opts.js, defaults already applied |
| const cache = opts.cache; |
| const root = path.resolve(opts.root); |
| const baseDir = opts.baseDir; |
| const humanReadable = opts.humanReadable; |
| const hidePermissions = opts.hidePermissions; |
| const handleError = opts.handleError; |
| const showDotfiles = opts.showDotfiles; |
| const si = opts.si; |
| const weakEtags = opts.weakEtags; |
| |
| return function middleware(req, res, next) { |
| // Figure out the path for the file from the given url |
| const parsed = url.parse(req.url); |
| const pathname = decodeURIComponent(parsed.pathname); |
| const dir = path.normalize( |
| path.join( |
| root, |
| path.relative( |
| path.join('/', baseDir), |
| pathname |
| ) |
| ) |
| ); |
| |
| fs.stat(dir, (statErr, stat) => { |
| if (statErr) { |
| if (handleError) { |
| status[500](res, next, { error: statErr }); |
| } else { |
| next(); |
| } |
| return; |
| } |
| |
| // files are the listing of dir |
| fs.readdir(dir, (readErr, _files) => { |
| let files = _files; |
| |
| if (readErr) { |
| if (handleError) { |
| status[500](res, next, { error: readErr }); |
| } else { |
| next(); |
| } |
| return; |
| } |
| |
| // Optionally exclude dotfiles from directory listing. |
| if (!showDotfiles) { |
| files = files.filter(filename => filename.slice(0, 1) !== '.'); |
| } |
| |
| res.setHeader('content-type', 'text/html'); |
| res.setHeader('etag', etag(stat, weakEtags)); |
| res.setHeader('last-modified', (new Date(stat.mtime)).toUTCString()); |
| res.setHeader('cache-control', cache); |
| |
| function render(dirs, renderFiles, lolwuts) { |
| // each entry in the array is a [name, stat] tuple |
| |
| let html = `${[ |
| '<!doctype html>', |
| '<html>', |
| ' <head>', |
| ' <meta charset="utf-8">', |
| ' <meta name="viewport" content="width=device-width">', |
| ` <title>Index of ${he.encode(pathname)}</title>`, |
| ` <style type="text/css">${css}</style>`, |
| ' </head>', |
| ' <body>', |
| `<h1>Index of ${he.encode(pathname)}</h1>`, |
| ].join('\n')}\n`; |
| |
| html += '<table>'; |
| |
| const failed = false; |
| const writeRow = (file) => { |
| // render a row given a [name, stat] tuple |
| const isDir = file[1].isDirectory && file[1].isDirectory(); |
| let href = `./${encodeURIComponent(file[0])}`; |
| |
| // append trailing slash and query for dir entry |
| if (isDir) { |
| href += `/${he.encode((parsed.search) ? parsed.search : '')}`; |
| } |
| |
| const displayName = he.encode(file[0]) + ((isDir) ? '/' : ''); |
| const ext = file[0].split('.').pop(); |
| const classForNonDir = supportedIcons[ext] ? ext : '_page'; |
| const iconClass = `icon-${isDir ? '_blank' : classForNonDir}`; |
| |
| // TODO: use stylessheets? |
| html += `${'<tr>' + |
| '<td><i class="icon '}${iconClass}"></i></td>`; |
| if (!hidePermissions) { |
| html += `<td class="perms"><code>(${permsToString(file[1])})</code></td>`; |
| } |
| html += |
| `<td class="last-modified">${lastModifiedToString(file[1])}</td>` + |
| `<td class="file-size"><code>${sizeToString(file[1], humanReadable, si)}</code></td>` + |
| `<td class="display-name"><a href="${href}">${displayName}</a></td>` + |
| '</tr>\n'; |
| }; |
| |
| dirs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow); |
| renderFiles.sort((a, b) => a.toString().localeCompare(b.toString())).forEach(writeRow); |
| lolwuts.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow); |
| |
| html += '</table>\n'; |
| html += `<br><address>Node.js ${ |
| process.version |
| }/ <a href="https://github.com/http-party/http-server">http-server</a> ` + |
| `server running @ ${ |
| he.encode(req.headers.host || '')}</address>\n` + |
| '</body></html>' |
| ; |
| |
| if (!failed) { |
| res.writeHead(200, { 'Content-Type': 'text/html' }); |
| res.end(html); |
| } |
| } |
| |
| sortFiles(dir, files, (lolwuts, dirs, sortedFiles) => { |
| // It's possible to get stat errors for all sorts of reasons here. |
| // Unfortunately, our two choices are to either bail completely, |
| // or just truck along as though everything's cool. In this case, |
| // I decided to just tack them on as "??!?" items along with dirs |
| // and files. |
| // |
| // Whatever. |
| |
| // if it makes sense to, add a .. link |
| if (path.resolve(dir, '..').slice(0, root.length) === root) { |
| fs.stat(path.join(dir, '..'), (err, s) => { |
| if (err) { |
| if (handleError) { |
| status[500](res, next, { error: err }); |
| } else { |
| next(); |
| } |
| return; |
| } |
| dirs.unshift(['..', s]); |
| render(dirs, sortedFiles, lolwuts); |
| }); |
| } else { |
| render(dirs, sortedFiles, lolwuts); |
| } |
| }); |
| }); |
| }); |
| }; |
| }; |