457 lines
11 KiB
JavaScript
457 lines
11 KiB
JavaScript
|
/*!
|
||
|
* telebit/serve-index
|
||
|
* Copyright(c) 2018 AJ ONeal
|
||
|
*
|
||
|
* Derivative work of github.com/expressjs/serve-index
|
||
|
* Copyright(c) 2011 Sencha Inc.
|
||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||
|
* MIT Licensed
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
/**
|
||
|
* Module dependencies.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
var escapeHtml = require('escape-html');
|
||
|
var fs = require('fs')
|
||
|
, path = require('path')
|
||
|
, normalize = path.normalize
|
||
|
, sep = path.sep
|
||
|
, extname = path.extname
|
||
|
, join = path.join;
|
||
|
var Batch = require('batch');
|
||
|
var mime = require('mime-types');
|
||
|
|
||
|
/**
|
||
|
* Module exports.
|
||
|
* @public
|
||
|
*/
|
||
|
|
||
|
var defaultPagepath = path.join(__dirname, 'public/directory.html');
|
||
|
var defaultStylepath = path.join(__dirname, 'public/style.css');
|
||
|
var defaultList = '<ul id="files" class="view-{view}">{head}{files}</ul>';
|
||
|
var defaultHead = '<li class="header">'
|
||
|
+ '<span class="name">Name</span>'
|
||
|
+ '<span class="size">Size</span>'
|
||
|
+ '<span class="date">Modified</span>'
|
||
|
+ '</li>'
|
||
|
;
|
||
|
var defaultFile = '<li><div>'
|
||
|
+ '<a href="{path}?download=true" class="download" title="Download {file.name}">'
|
||
|
+ '<span class="download">⬇️</span>'
|
||
|
+ '</a>'
|
||
|
+ '<a href="{path}" class="{classes}" title="{file.name}">'
|
||
|
+ '<span class="name">{file.name}</span>'
|
||
|
+ '<span class="size">{file.size}</span>'
|
||
|
+ '<span class="date">{file.date}</span>'
|
||
|
+ '</a>'
|
||
|
+ '</div></li>'
|
||
|
;
|
||
|
module.exports = function (opts) {
|
||
|
if (!opts) { opts = {}; }
|
||
|
return createHtmlRender({
|
||
|
pagepath: opts.pagepath || defaultPagepath
|
||
|
, stylepath: opts.stylepath || defaultStylepath
|
||
|
, list: opts.list || defaultList
|
||
|
, head: opts.head || defaultHead
|
||
|
, file: opts.file || defaultFile
|
||
|
, privatefiles: opts.privatefiles
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/*!
|
||
|
* Icon cache.
|
||
|
*/
|
||
|
|
||
|
var cache = {};
|
||
|
|
||
|
/**
|
||
|
* Map html `files`, returning an html unordered list.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function createHtmlFileList(opts, files, dir, useIcons, view) {
|
||
|
var ftpls = files.map(function (file) {
|
||
|
var classes = [];
|
||
|
var isDir = file.stat && file.stat.isDirectory();
|
||
|
var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
|
||
|
|
||
|
if (useIcons) {
|
||
|
classes.push('icon');
|
||
|
|
||
|
if (isDir) {
|
||
|
classes.push('icon-directory');
|
||
|
} else {
|
||
|
var ext = extname(file.name);
|
||
|
var icon = iconLookup(file.name);
|
||
|
|
||
|
classes.push('icon');
|
||
|
classes.push('icon-' + ext.substring(1));
|
||
|
|
||
|
if (classes.indexOf(icon.className) === -1) {
|
||
|
classes.push(icon.className);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
path.push(encodeURIComponent(file.name));
|
||
|
|
||
|
var date = file.stat && file.name !== '..'
|
||
|
? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
|
||
|
: '';
|
||
|
var size = file.stat && !isDir
|
||
|
? file.stat.size
|
||
|
: '';
|
||
|
var OCTAL = 8;
|
||
|
var WORLD_READ = parseInt(4, OCTAL); // R(4)W(2)X(1)
|
||
|
var hasWorldRead = file.mode | WORLD_READ;
|
||
|
|
||
|
if (!hasWorldRead && 'ignore' === opts.privatefiles) {
|
||
|
return '';
|
||
|
}
|
||
|
return opts.file.replace(/{path}/g, escapeHtml(normalizeSlashes(normalize(path.join('/')))))
|
||
|
.replace(/{classes}/g, escapeHtml(classes.join(' ')))
|
||
|
.replace(/{file.name}/g, escapeHtml(file.name))
|
||
|
.replace(/{file.size}/g, escapeHtml(size))
|
||
|
.replace(/{file.date}/g, escapeHtml(date))
|
||
|
;
|
||
|
}).filter(Boolean).join('\n');
|
||
|
|
||
|
var html = opts.list
|
||
|
.replace(/{view}/g, escapeHtml(view))
|
||
|
.replace(/{head}/g, view === 'details' ? opts.head : '')
|
||
|
.replace(/{files}/g, ftpls)
|
||
|
;
|
||
|
|
||
|
return html;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create function to render html.
|
||
|
*/
|
||
|
|
||
|
function createHtmlRender(opts) {
|
||
|
return function render(locals, callback) {
|
||
|
// read template
|
||
|
fs.readFile(opts.pagepath, 'utf8', function (err, pageStr) {
|
||
|
fs.readFile(opts.stylepath, 'utf8', function (err, styleStr) {
|
||
|
if (err) return callback(err);
|
||
|
|
||
|
var body = pageStr
|
||
|
.replace(/\{style\}/g, styleStr.concat(iconStyle(locals.fileList, locals.displayIcons)))
|
||
|
.replace(/\{files\}/g, createHtmlFileList(opts, locals.fileList, locals.directory, locals.displayIcons, locals.viewName))
|
||
|
.replace(/\{directory\}/g, escapeHtml(locals.directory))
|
||
|
.replace(/\{linked-path\}/g, htmlPath(locals.directory))
|
||
|
;
|
||
|
|
||
|
callback(null, body);
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Map html `dir`, returning a linked path.
|
||
|
*/
|
||
|
|
||
|
function htmlPath(dir) {
|
||
|
var parts = dir.split('/');
|
||
|
var crumb = new Array(parts.length);
|
||
|
|
||
|
for (var i = 0; i < parts.length; i++) {
|
||
|
var part = parts[i];
|
||
|
|
||
|
if (part) {
|
||
|
parts[i] = encodeURIComponent(part);
|
||
|
crumb[i] = '<a href="' + escapeHtml(parts.slice(0, i + 1).join('/')) + '">' + escapeHtml(part) + '</a>';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return crumb.join(' / ');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the icon data for the file name.
|
||
|
*/
|
||
|
|
||
|
function iconLookup(filename) {
|
||
|
var ext = extname(filename);
|
||
|
|
||
|
// try by extension
|
||
|
if (icons[ext]) {
|
||
|
return {
|
||
|
className: 'icon-' + ext.substring(1),
|
||
|
fileName: icons[ext]
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var mimetype = mime.lookup(ext);
|
||
|
|
||
|
// default if no mime type
|
||
|
if (mimetype === false) {
|
||
|
return {
|
||
|
className: 'icon-default',
|
||
|
fileName: icons.default
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// try by mime type
|
||
|
if (icons[mimetype]) {
|
||
|
return {
|
||
|
className: 'icon-' + mimetype.replace('/', '-'),
|
||
|
fileName: icons[mimetype]
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var suffix = mimetype.split('+')[1];
|
||
|
|
||
|
if (suffix && icons['+' + suffix]) {
|
||
|
return {
|
||
|
className: 'icon-' + suffix,
|
||
|
fileName: icons['+' + suffix]
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var type = mimetype.split('/')[0];
|
||
|
|
||
|
// try by type only
|
||
|
if (icons[type]) {
|
||
|
return {
|
||
|
className: 'icon-' + type,
|
||
|
fileName: icons[type]
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
className: 'icon-default',
|
||
|
fileName: icons.default
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load icon images, return css string.
|
||
|
*/
|
||
|
|
||
|
function iconStyle(files, useIcons) {
|
||
|
if (!useIcons) return '';
|
||
|
var i;
|
||
|
var list = [];
|
||
|
var rules = {};
|
||
|
var selector;
|
||
|
var selectors = {};
|
||
|
var style = '';
|
||
|
|
||
|
for (i = 0; i < files.length; i++) {
|
||
|
var file = files[i];
|
||
|
|
||
|
var isDir = file.stat && file.stat.isDirectory();
|
||
|
var icon = isDir
|
||
|
? { className: 'icon-directory', fileName: icons.folder }
|
||
|
: iconLookup(file.name);
|
||
|
var iconName = icon.fileName;
|
||
|
|
||
|
selector = '#files .' + icon.className + ' .name';
|
||
|
|
||
|
if (!rules[iconName]) {
|
||
|
rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');'
|
||
|
selectors[iconName] = [];
|
||
|
list.push(iconName);
|
||
|
}
|
||
|
|
||
|
if (selectors[iconName].indexOf(selector) === -1) {
|
||
|
selectors[iconName].push(selector);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < list.length; i++) {
|
||
|
iconName = list[i];
|
||
|
style += selectors[iconName].join(',\n') + ' {\n ' + rules[iconName] + '\n}\n';
|
||
|
}
|
||
|
|
||
|
return style;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load and cache the given `icon`.
|
||
|
*
|
||
|
* @param {String} icon
|
||
|
* @return {String}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function load(icon) {
|
||
|
if (cache[icon]) return cache[icon];
|
||
|
return cache[icon] = fs.readFileSync(__dirname + '/public/icons/' + icon, 'base64');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalizes the path separator from system separator
|
||
|
* to URL separator, aka `/`.
|
||
|
*
|
||
|
* @param {String} path
|
||
|
* @return {String}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function normalizeSlashes(path) {
|
||
|
return path.split(sep).join('/');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send a response.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function send (res, type, body) {
|
||
|
// security header for content sniffing
|
||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
|
|
||
|
// standard headers
|
||
|
res.setHeader('Content-Type', type + '; charset=utf-8');
|
||
|
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
|
||
|
|
||
|
// body
|
||
|
res.end(body, 'utf8');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stat all files and return array of stat
|
||
|
* in same order.
|
||
|
*/
|
||
|
|
||
|
function stat(dir, files, cb) {
|
||
|
var batch = new Batch();
|
||
|
|
||
|
batch.concurrency(10);
|
||
|
|
||
|
files.forEach(function(file){
|
||
|
batch.push(function(done){
|
||
|
fs.stat(join(dir, file), function(err, stat){
|
||
|
if (err && err.code !== 'ENOENT') return done(err);
|
||
|
|
||
|
// pass ENOENT as null stat, not error
|
||
|
done(null, stat || null);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
batch.end(cb);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Icon map.
|
||
|
*/
|
||
|
|
||
|
var icons = {
|
||
|
// base icons
|
||
|
'default': 'page_white.png',
|
||
|
'folder': 'folder.png',
|
||
|
|
||
|
// generic mime type icons
|
||
|
'font': 'font.png',
|
||
|
'image': 'image.png',
|
||
|
'text': 'page_white_text.png',
|
||
|
'video': 'film.png',
|
||
|
|
||
|
// generic mime suffix icons
|
||
|
'+json': 'page_white_code.png',
|
||
|
'+xml': 'page_white_code.png',
|
||
|
'+zip': 'box.png',
|
||
|
|
||
|
// specific mime type icons
|
||
|
'application/javascript': 'page_white_code_red.png',
|
||
|
'application/json': 'page_white_code.png',
|
||
|
'application/msword': 'page_white_word.png',
|
||
|
'application/pdf': 'page_white_acrobat.png',
|
||
|
'application/postscript': 'page_white_vector.png',
|
||
|
'application/rtf': 'page_white_word.png',
|
||
|
'application/vnd.ms-excel': 'page_white_excel.png',
|
||
|
'application/vnd.ms-powerpoint': 'page_white_powerpoint.png',
|
||
|
'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png',
|
||
|
'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png',
|
||
|
'application/vnd.oasis.opendocument.text': 'page_white_word.png',
|
||
|
'application/x-7z-compressed': 'box.png',
|
||
|
'application/x-sh': 'application_xp_terminal.png',
|
||
|
'application/x-msaccess': 'page_white_database.png',
|
||
|
'application/x-shockwave-flash': 'page_white_flash.png',
|
||
|
'application/x-sql': 'page_white_database.png',
|
||
|
'application/x-tar': 'box.png',
|
||
|
'application/x-xz': 'box.png',
|
||
|
'application/xml': 'page_white_code.png',
|
||
|
'application/zip': 'box.png',
|
||
|
'image/svg+xml': 'page_white_vector.png',
|
||
|
'text/css': 'page_white_code.png',
|
||
|
'text/html': 'page_white_code.png',
|
||
|
'text/less': 'page_white_code.png',
|
||
|
|
||
|
// other, extension-specific icons
|
||
|
'.accdb': 'page_white_database.png',
|
||
|
'.apk': 'box.png',
|
||
|
'.app': 'application_xp.png',
|
||
|
'.as': 'page_white_actionscript.png',
|
||
|
'.asp': 'page_white_code.png',
|
||
|
'.aspx': 'page_white_code.png',
|
||
|
'.bat': 'application_xp_terminal.png',
|
||
|
'.bz2': 'box.png',
|
||
|
'.c': 'page_white_c.png',
|
||
|
'.cab': 'box.png',
|
||
|
'.cfm': 'page_white_coldfusion.png',
|
||
|
'.clj': 'page_white_code.png',
|
||
|
'.cc': 'page_white_cplusplus.png',
|
||
|
'.cgi': 'application_xp_terminal.png',
|
||
|
'.cpp': 'page_white_cplusplus.png',
|
||
|
'.cs': 'page_white_csharp.png',
|
||
|
'.db': 'page_white_database.png',
|
||
|
'.dbf': 'page_white_database.png',
|
||
|
'.deb': 'box.png',
|
||
|
'.dll': 'page_white_gear.png',
|
||
|
'.dmg': 'drive.png',
|
||
|
'.docx': 'page_white_word.png',
|
||
|
'.erb': 'page_white_ruby.png',
|
||
|
'.exe': 'application_xp.png',
|
||
|
'.fnt': 'font.png',
|
||
|
'.gam': 'controller.png',
|
||
|
'.gz': 'box.png',
|
||
|
'.h': 'page_white_h.png',
|
||
|
'.ini': 'page_white_gear.png',
|
||
|
'.iso': 'cd.png',
|
||
|
'.jar': 'box.png',
|
||
|
'.java': 'page_white_cup.png',
|
||
|
'.jsp': 'page_white_cup.png',
|
||
|
'.lua': 'page_white_code.png',
|
||
|
'.lz': 'box.png',
|
||
|
'.lzma': 'box.png',
|
||
|
'.m': 'page_white_code.png',
|
||
|
'.map': 'map.png',
|
||
|
'.msi': 'box.png',
|
||
|
'.mv4': 'film.png',
|
||
|
'.pdb': 'page_white_database.png',
|
||
|
'.php': 'page_white_php.png',
|
||
|
'.pl': 'page_white_code.png',
|
||
|
'.pkg': 'box.png',
|
||
|
'.pptx': 'page_white_powerpoint.png',
|
||
|
'.psd': 'page_white_picture.png',
|
||
|
'.py': 'page_white_code.png',
|
||
|
'.rar': 'box.png',
|
||
|
'.rb': 'page_white_ruby.png',
|
||
|
'.rm': 'film.png',
|
||
|
'.rom': 'controller.png',
|
||
|
'.rpm': 'box.png',
|
||
|
'.sass': 'page_white_code.png',
|
||
|
'.sav': 'controller.png',
|
||
|
'.scss': 'page_white_code.png',
|
||
|
'.srt': 'page_white_text.png',
|
||
|
'.tbz2': 'box.png',
|
||
|
'.tgz': 'box.png',
|
||
|
'.tlz': 'box.png',
|
||
|
'.vb': 'page_white_code.png',
|
||
|
'.vbs': 'page_white_code.png',
|
||
|
'.xcf': 'page_white_picture.png',
|
||
|
'.xlsx': 'page_white_excel.png',
|
||
|
'.yaws': 'page_white_code.png'
|
||
|
};
|