Projekt

Obecné

Profil

Stáhnout (15.3 KB) Statistiky
| Větev: | Revize:
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
        send(res, 'text/html', body)
212
      });
213
    });
214
  });
215
};
216

    
217
/**
218
 * Respond with application/json.
219
 */
220

    
221
serveIndex.json = function _json(req, res, files) {
222
  send(res, 'application/json', JSON.stringify(files))
223
};
224

    
225
/**
226
 * Respond with text/plain.
227
 */
228

    
229
serveIndex.plain = function _plain(req, res, files) {
230
  send(res, 'text/plain', (files.join('\n') + '\n'))
231
};
232

    
233
/**
234
 * Map html `files`, returning an html unordered list.
235
 * @private
236
 */
237

    
238
function createHtmlFileList(files, dir, useIcons, view) {
239
  var html = '<ul id="files" class="view-' + escapeHtml(view) + '">'
240
    + (view == 'details' ? (
241
      '<li class="header">'
242
      + '<span class="name">Name</span>'
243
      + '<span class="size">Size</span>'
244
      + '<span class="date">Modified</span>'
245
      + '</li>') : '');
246

    
247
  html += files.map(function (file) {
248
    var classes = [];
249
    var isDir = file.stat && file.stat.isDirectory();
250
    var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
251

    
252
    if (useIcons) {
253
      classes.push('icon');
254

    
255
      if (isDir) {
256
        classes.push('icon-directory');
257
      } else {
258
        var ext = extname(file.name);
259
        var icon = iconLookup(file.name);
260

    
261
        classes.push('icon');
262
        classes.push('icon-' + ext.substring(1));
263

    
264
        if (classes.indexOf(icon.className) === -1) {
265
          classes.push(icon.className);
266
        }
267
      }
268
    }
269

    
270
    path.push(encodeURIComponent(file.name));
271

    
272
    var date = file.stat && file.name !== '..'
273
      ? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
274
      : '';
275
    var size = file.stat && !isDir
276
      ? file.stat.size
277
      : '';
278

    
279
    return '<li><a href="'
280
      + escapeHtml(normalizeSlashes(normalize(path.join('/'))))
281
      + '" class="' + escapeHtml(classes.join(' ')) + '"'
282
      + ' title="' + escapeHtml(file.name) + '">'
283
      + '<span class="name">' + escapeHtml(file.name) + '</span>'
284
      + '<span class="size">' + escapeHtml(size) + '</span>'
285
      + '<span class="date">' + escapeHtml(date) + '</span>'
286
      + '</a></li>';
287
  }).join('\n');
288

    
289
  html += '</ul>';
290

    
291
  return html;
292
}
293

    
294
/**
295
 * Create function to render html.
296
 */
297

    
298
function createHtmlRender(template) {
299
  return function render(locals, callback) {
300
    // read template
301
    fs.readFile(template, 'utf8', function (err, str) {
302
      if (err) return callback(err);
303

    
304
      var body = str
305
        .replace(/\{style\}/g, locals.style.concat(iconStyle(locals.fileList, locals.displayIcons)))
306
        .replace(/\{files\}/g, createHtmlFileList(locals.fileList, locals.directory, locals.displayIcons, locals.viewName))
307
        .replace(/\{directory\}/g, escapeHtml(locals.directory))
308
        .replace(/\{linked-path\}/g, htmlPath(locals.directory));
309

    
310
      callback(null, body);
311
    });
312
  };
313
}
314

    
315
/**
316
 * Sort function for with directories first.
317
 */
318

    
319
function fileSort(a, b) {
320
  // sort ".." to the top
321
  if (a.name === '..' || b.name === '..') {
322
    return a.name === b.name ? 0
323
      : a.name === '..' ? -1 : 1;
324
  }
325

    
326
  return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) ||
327
    String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());
328
}
329

    
330
/**
331
 * Map html `dir`, returning a linked path.
332
 */
333

    
334
function htmlPath(dir) {
335
  var parts = dir.split('/');
336
  var crumb = new Array(parts.length);
337

    
338
  for (var i = 0; i < parts.length; i++) {
339
    var part = parts[i];
340

    
341
    if (part) {
342
      parts[i] = encodeURIComponent(part);
343
      crumb[i] = '<a href="' + escapeHtml(parts.slice(0, i + 1).join('/')) + '">' + escapeHtml(part) + '</a>';
344
    }
345
  }
346

    
347
  return crumb.join(' / ');
348
}
349

    
350
/**
351
 * Get the icon data for the file name.
352
 */
353

    
354
function iconLookup(filename) {
355
  var ext = extname(filename);
356

    
357
  // try by extension
358
  if (icons[ext]) {
359
    return {
360
      className: 'icon-' + ext.substring(1),
361
      fileName: icons[ext]
362
    };
363
  }
364

    
365
  var mimetype = mime.lookup(ext);
366

    
367
  // default if no mime type
368
  if (mimetype === false) {
369
    return {
370
      className: 'icon-default',
371
      fileName: icons.default
372
    };
373
  }
374

    
375
  // try by mime type
376
  if (icons[mimetype]) {
377
    return {
378
      className: 'icon-' + mimetype.replace('/', '-'),
379
      fileName: icons[mimetype]
380
    };
381
  }
382

    
383
  var suffix = mimetype.split('+')[1];
384

    
385
  if (suffix && icons['+' + suffix]) {
386
    return {
387
      className: 'icon-' + suffix,
388
      fileName: icons['+' + suffix]
389
    };
390
  }
391

    
392
  var type = mimetype.split('/')[0];
393

    
394
  // try by type only
395
  if (icons[type]) {
396
    return {
397
      className: 'icon-' + type,
398
      fileName: icons[type]
399
    };
400
  }
401

    
402
  return {
403
    className: 'icon-default',
404
    fileName: icons.default
405
  };
406
}
407

    
408
/**
409
 * Load icon images, return css string.
410
 */
411

    
412
function iconStyle(files, useIcons) {
413
  if (!useIcons) return '';
414
  var i;
415
  var list = [];
416
  var rules = {};
417
  var selector;
418
  var selectors = {};
419
  var style = '';
420

    
421
  for (i = 0; i < files.length; i++) {
422
    var file = files[i];
423

    
424
    var isDir = file.stat && file.stat.isDirectory();
425
    var icon = isDir
426
      ? { className: 'icon-directory', fileName: icons.folder }
427
      : iconLookup(file.name);
428
    var iconName = icon.fileName;
429

    
430
    selector = '#files .' + icon.className + ' .name';
431

    
432
    if (!rules[iconName]) {
433
      rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');'
434
      selectors[iconName] = [];
435
      list.push(iconName);
436
    }
437

    
438
    if (selectors[iconName].indexOf(selector) === -1) {
439
      selectors[iconName].push(selector);
440
    }
441
  }
442

    
443
  for (i = 0; i < list.length; i++) {
444
    iconName = list[i];
445
    style += selectors[iconName].join(',\n') + ' {\n  ' + rules[iconName] + '\n}\n';
446
  }
447

    
448
  return style;
449
}
450

    
451
/**
452
 * Load and cache the given `icon`.
453
 *
454
 * @param {String} icon
455
 * @return {String}
456
 * @api private
457
 */
458

    
459
function load(icon) {
460
  if (cache[icon]) return cache[icon];
461
  return cache[icon] = fs.readFileSync(__dirname + '/public/icons/' + icon, 'base64');
462
}
463

    
464
/**
465
 * Normalizes the path separator from system separator
466
 * to URL separator, aka `/`.
467
 *
468
 * @param {String} path
469
 * @return {String}
470
 * @api private
471
 */
472

    
473
function normalizeSlashes(path) {
474
  return path.split(sep).join('/');
475
};
476

    
477
/**
478
 * Filter "hidden" `files`, aka files
479
 * beginning with a `.`.
480
 *
481
 * @param {Array} files
482
 * @return {Array}
483
 * @api private
484
 */
485

    
486
function removeHidden(files) {
487
  return files.filter(function(file){
488
    return '.' != file[0];
489
  });
490
}
491

    
492
/**
493
 * Send a response.
494
 * @private
495
 */
496

    
497
function send (res, type, body) {
498
  // security header for content sniffing
499
  res.setHeader('X-Content-Type-Options', 'nosniff')
500

    
501
  // standard headers
502
  res.setHeader('Content-Type', type + '; charset=utf-8')
503
  res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
504

    
505
  // body
506
  res.end(body, 'utf8')
507
}
508

    
509
/**
510
 * Stat all files and return array of stat
511
 * in same order.
512
 */
513

    
514
function stat(dir, files, cb) {
515
  var batch = new Batch();
516

    
517
  batch.concurrency(10);
518

    
519
  files.forEach(function(file){
520
    batch.push(function(done){
521
      fs.stat(join(dir, file), function(err, stat){
522
        if (err && err.code !== 'ENOENT') return done(err);
523

    
524
        // pass ENOENT as null stat, not error
525
        done(null, stat || null);
526
      });
527
    });
528
  });
529

    
530
  batch.end(cb);
531
}
532

    
533
/**
534
 * Icon map.
535
 */
536

    
537
var icons = {
538
  // base icons
539
  'default': 'page_white.png',
540
  'folder': 'folder.png',
541

    
542
  // generic mime type icons
543
  'image': 'image.png',
544
  'text': 'page_white_text.png',
545
  'video': 'film.png',
546

    
547
  // generic mime suffix icons
548
  '+json': 'page_white_code.png',
549
  '+xml': 'page_white_code.png',
550
  '+zip': 'box.png',
551

    
552
  // specific mime type icons
553
  'application/font-woff': 'font.png',
554
  'application/javascript': 'page_white_code_red.png',
555
  'application/json': 'page_white_code.png',
556
  'application/msword': 'page_white_word.png',
557
  'application/pdf': 'page_white_acrobat.png',
558
  'application/postscript': 'page_white_vector.png',
559
  'application/rtf': 'page_white_word.png',
560
  'application/vnd.ms-excel': 'page_white_excel.png',
561
  'application/vnd.ms-powerpoint': 'page_white_powerpoint.png',
562
  'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png',
563
  'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png',
564
  'application/vnd.oasis.opendocument.text': 'page_white_word.png',
565
  'application/x-7z-compressed': 'box.png',
566
  'application/x-sh': 'application_xp_terminal.png',
567
  'application/x-font-ttf': 'font.png',
568
  'application/x-msaccess': 'page_white_database.png',
569
  'application/x-shockwave-flash': 'page_white_flash.png',
570
  'application/x-sql': 'page_white_database.png',
571
  'application/x-tar': 'box.png',
572
  'application/x-xz': 'box.png',
573
  'application/xml': 'page_white_code.png',
574
  'application/zip': 'box.png',
575
  'image/svg+xml': 'page_white_vector.png',
576
  'text/css': 'page_white_code.png',
577
  'text/html': 'page_white_code.png',
578
  'text/less': 'page_white_code.png',
579

    
580
  // other, extension-specific icons
581
  '.accdb': 'page_white_database.png',
582
  '.apk': 'box.png',
583
  '.app': 'application_xp.png',
584
  '.as': 'page_white_actionscript.png',
585
  '.asp': 'page_white_code.png',
586
  '.aspx': 'page_white_code.png',
587
  '.bat': 'application_xp_terminal.png',
588
  '.bz2': 'box.png',
589
  '.c': 'page_white_c.png',
590
  '.cab': 'box.png',
591
  '.cfm': 'page_white_coldfusion.png',
592
  '.clj': 'page_white_code.png',
593
  '.cc': 'page_white_cplusplus.png',
594
  '.cgi': 'application_xp_terminal.png',
595
  '.cpp': 'page_white_cplusplus.png',
596
  '.cs': 'page_white_csharp.png',
597
  '.db': 'page_white_database.png',
598
  '.dbf': 'page_white_database.png',
599
  '.deb': 'box.png',
600
  '.dll': 'page_white_gear.png',
601
  '.dmg': 'drive.png',
602
  '.docx': 'page_white_word.png',
603
  '.erb': 'page_white_ruby.png',
604
  '.exe': 'application_xp.png',
605
  '.fnt': 'font.png',
606
  '.gam': 'controller.png',
607
  '.gz': 'box.png',
608
  '.h': 'page_white_h.png',
609
  '.ini': 'page_white_gear.png',
610
  '.iso': 'cd.png',
611
  '.jar': 'box.png',
612
  '.java': 'page_white_cup.png',
613
  '.jsp': 'page_white_cup.png',
614
  '.lua': 'page_white_code.png',
615
  '.lz': 'box.png',
616
  '.lzma': 'box.png',
617
  '.m': 'page_white_code.png',
618
  '.map': 'map.png',
619
  '.msi': 'box.png',
620
  '.mv4': 'film.png',
621
  '.otf': 'font.png',
622
  '.pdb': 'page_white_database.png',
623
  '.php': 'page_white_php.png',
624
  '.pl': 'page_white_code.png',
625
  '.pkg': 'box.png',
626
  '.pptx': 'page_white_powerpoint.png',
627
  '.psd': 'page_white_picture.png',
628
  '.py': 'page_white_code.png',
629
  '.rar': 'box.png',
630
  '.rb': 'page_white_ruby.png',
631
  '.rm': 'film.png',
632
  '.rom': 'controller.png',
633
  '.rpm': 'box.png',
634
  '.sass': 'page_white_code.png',
635
  '.sav': 'controller.png',
636
  '.scss': 'page_white_code.png',
637
  '.srt': 'page_white_text.png',
638
  '.tbz2': 'box.png',
639
  '.tgz': 'box.png',
640
  '.tlz': 'box.png',
641
  '.vb': 'page_white_code.png',
642
  '.vbs': 'page_white_code.png',
643
  '.xcf': 'page_white_picture.png',
644
  '.xlsx': 'page_white_excel.png',
645
  '.yaws': 'page_white_code.png'
646
};
(4-4/5)