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 };