source: frontend/node_modules/serve-index/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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