You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

229 lines
5.3 KiB

  1. /*!
  2. * Connect - directory
  3. * Copyright(c) 2011 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * MIT Licensed
  6. */
  7. // TODO: icon / style for directories
  8. // TODO: arrow key navigation
  9. // TODO: make icons extensible
  10. /**
  11. * Module dependencies.
  12. */
  13. var fs = require('fs')
  14. , parse = require('url').parse
  15. , utils = require('../utils')
  16. , path = require('path')
  17. , normalize = path.normalize
  18. , extname = path.extname
  19. , join = path.join;
  20. /*!
  21. * Icon cache.
  22. */
  23. var cache = {};
  24. /**
  25. * Directory:
  26. *
  27. * Serve directory listings with the given `root` path.
  28. *
  29. * Options:
  30. *
  31. * - `hidden` display hidden (dot) files. Defaults to false.
  32. * - `icons` display icons. Defaults to false.
  33. * - `filter` Apply this filter function to files. Defaults to false.
  34. *
  35. * @param {String} root
  36. * @param {Object} options
  37. * @return {Function}
  38. * @api public
  39. */
  40. exports = module.exports = function directory(root, options){
  41. options = options || {};
  42. // root required
  43. if (!root) throw new Error('directory() root path required');
  44. var hidden = options.hidden
  45. , icons = options.icons
  46. , filter = options.filter
  47. , root = normalize(root);
  48. return function directory(req, res, next) {
  49. if ('GET' != req.method && 'HEAD' != req.method) return next();
  50. var accept = req.headers.accept || 'text/plain'
  51. , url = parse(req.url)
  52. , dir = decodeURIComponent(url.pathname)
  53. , path = normalize(join(root, dir))
  54. , originalUrl = parse(req.originalUrl)
  55. , originalDir = decodeURIComponent(originalUrl.pathname)
  56. , showUp = path != root && path != root + '/';
  57. // null byte(s), bad request
  58. if (~path.indexOf('\0')) return next(utils.error(400));
  59. // malicious path, forbidden
  60. if (0 != path.indexOf(root)) return next(utils.error(403));
  61. // check if we have a directory
  62. fs.stat(path, function(err, stat){
  63. if (err) return 'ENOENT' == err.code
  64. ? next()
  65. : next(err);
  66. if (!stat.isDirectory()) return next();
  67. // fetch files
  68. fs.readdir(path, function(err, files){
  69. if (err) return next(err);
  70. if (!hidden) files = removeHidden(files);
  71. if (filter) files = files.filter(filter);
  72. files.sort();
  73. // content-negotiation
  74. for (var key in exports) {
  75. if (~accept.indexOf(key) || ~accept.indexOf('*/*')) {
  76. exports[key](req, res, files, next, originalDir, showUp, icons);
  77. return;
  78. }
  79. }
  80. // not acceptable
  81. next(utils.error(406));
  82. });
  83. });
  84. };
  85. };
  86. /**
  87. * Respond with text/html.
  88. */
  89. exports.html = function(req, res, files, next, dir, showUp, icons){
  90. fs.readFile(__dirname + '/../public/directory.html', 'utf8', function(err, str){
  91. if (err) return next(err);
  92. fs.readFile(__dirname + '/../public/style.css', 'utf8', function(err, style){
  93. if (err) return next(err);
  94. if (showUp) files.unshift('..');
  95. str = str
  96. .replace('{style}', style)
  97. .replace('{files}', html(files, dir, icons))
  98. .replace('{directory}', dir)
  99. .replace('{linked-path}', htmlPath(dir));
  100. res.setHeader('Content-Type', 'text/html');
  101. res.setHeader('Content-Length', str.length);
  102. res.end(str);
  103. });
  104. });
  105. };
  106. /**
  107. * Respond with application/json.
  108. */
  109. exports.json = function(req, res, files){
  110. files = JSON.stringify(files);
  111. res.setHeader('Content-Type', 'application/json');
  112. res.setHeader('Content-Length', files.length);
  113. res.end(files);
  114. };
  115. /**
  116. * Respond with text/plain.
  117. */
  118. exports.plain = function(req, res, files){
  119. files = files.join('\n') + '\n';
  120. res.setHeader('Content-Type', 'text/plain');
  121. res.setHeader('Content-Length', files.length);
  122. res.end(files);
  123. };
  124. /**
  125. * Map html `dir`, returning a linked path.
  126. */
  127. function htmlPath(dir) {
  128. var curr = [];
  129. return dir.split('/').map(function(part){
  130. curr.push(part);
  131. return '<a href="' + curr.join('/') + '">' + part + '</a>';
  132. }).join(' / ');
  133. }
  134. /**
  135. * Map html `files`, returning an html unordered list.
  136. */
  137. function html(files, dir, useIcons) {
  138. return '<ul id="files">' + files.map(function(file){
  139. var icon = ''
  140. , classes = [];
  141. if (useIcons && '..' != file) {
  142. icon = icons[extname(file)] || icons.default;
  143. icon = '<img src="data:image/png;base64,' + load(icon) + '" />';
  144. classes.push('icon');
  145. }
  146. return '<li><a href="'
  147. + join(dir, file)
  148. + '" class="'
  149. + classes.join(' ') + '"'
  150. + ' title="' + file + '">'
  151. + icon + file + '</a></li>';
  152. }).join('\n') + '</ul>';
  153. }
  154. /**
  155. * Load and cache the given `icon`.
  156. *
  157. * @param {String} icon
  158. * @return {String}
  159. * @api private
  160. */
  161. function load(icon) {
  162. if (cache[icon]) return cache[icon];
  163. return cache[icon] = fs.readFileSync(__dirname + '/../public/icons/' + icon, 'base64');
  164. }
  165. /**
  166. * Filter "hidden" `files`, aka files
  167. * beginning with a `.`.
  168. *
  169. * @param {Array} files
  170. * @return {Array}
  171. * @api private
  172. */
  173. function removeHidden(files) {
  174. return files.filter(function(file){
  175. return '.' != file[0];
  176. });
  177. }
  178. /**
  179. * Icon map.
  180. */
  181. var icons = {
  182. '.js': 'page_white_code_red.png'
  183. , '.c': 'page_white_c.png'
  184. , '.h': 'page_white_h.png'
  185. , '.cc': 'page_white_cplusplus.png'
  186. , '.php': 'page_white_php.png'
  187. , '.rb': 'page_white_ruby.png'
  188. , '.cpp': 'page_white_cplusplus.png'
  189. , '.swf': 'page_white_flash.png'
  190. , '.pdf': 'page_white_acrobat.png'
  191. , 'default': 'page_white.png'
  192. };