directory middleware: Add optional sorting strategy #692

Closed
wants to merge 3 commits into
from
View
@@ -38,6 +38,12 @@ var cache = {};
* - `hidden` display hidden (dot) files. Defaults to false.
* - `icons` display icons. Defaults to false.
* - `filter` Apply this filter function to files. Defaults to false.
+ * - 'stat' Call stat on each entry. Defaults to false. Sort strategy can set this to true.
+ * - `sort` Apply sort strategy. Defaults to alphabetical.
+ * Can be string or function passable to Array#sort.
+ * If provided function has needStat property, result of `fs.stat`
+ * plus `.name` are given to compare function
+ * otherwise simple string.
*
* @param {String} root
* @param {Object} options
@@ -53,8 +59,21 @@ exports = module.exports = function directory(root, options){
var hidden = options.hidden
, icons = options.icons
, filter = options.filter
+ , sort = options.sort
+ , doStat = options.stat
, root = normalize(root);
+ if (sort && typeof sort !== 'function') {
+ sort = exports.sortMethods[sort];
+ if(!sort)
+ throw new Error('Invalid sort function name: ' + sort +
+ '\nValid ones are: ' +
+ Object.keys(exports.sortMethods).join(', ') +
+ '\nor custom function');
+ }
+
+ doStat = doStat || (sort && sort.needStat);
+
return function directory(req, res, next) {
if ('GET' != req.method && 'HEAD' != req.method) return next();
@@ -85,12 +104,39 @@ exports = module.exports = function directory(root, options){
if (err) return next(err);
if (!hidden) files = removeHidden(files);
if (filter) files = files.filter(filter);
- files.sort();
+ if (!(sort && sort.needStat))
+ files.sort(sort);
// content-negotiation
for (var key in exports) {
if (~accept.indexOf(key) || ~accept.indexOf('*/*')) {
- exports[key](req, res, files, next, originalDir, showUp, icons);
+ if (doStat) {
+ var pending = files.length;
+ var stats = [];
+ files.forEach(function(name) {
+ fs.stat(join(path, name), function(err, stat) {
+ if(stat) {
+ stat.name = name;
+ stats.push(stat);
+ } else {
+ stats.push({
+ name: name,
+ error: err
+ });
+ }
+ // finish the job if all files stated
+ pending--;
+ if (pending === 0) {
+ if(sort && sort.needStat)
+ stats.sort(sort);
+ files = stats.map(function(stat) { return stat.name; });
+ exports[key](req, res, files, next, originalDir, showUp, icons, stats);
+ }
+ });
+ });
+ } else {
+ exports[key](req, res, files, next, originalDir, showUp, icons);
+ }
return;
}
}
@@ -106,15 +152,18 @@ exports = module.exports = function directory(root, options){
* Respond with text/html.
*/
-exports.html = function(req, res, files, next, dir, showUp, icons){
+exports.html = function(req, res, files, next, dir, showUp, icons, stats){
fs.readFile(__dirname + '/../public/directory.html', 'utf8', function(err, str){
if (err) return next(err);
fs.readFile(__dirname + '/../public/style.css', 'utf8', function(err, style){
if (err) return next(err);
- if (showUp) files.unshift('..');
+ if (showUp) {
+ files.unshift('..');
+ if(stats) stats.unshift({ name: '..' });
+ }
str = str
.replace('{style}', style)
- .replace('{files}', html(files, dir, icons))
+ .replace('{files}', html(files, dir, icons, stats))
.replace('{directory}', dir)
.replace('{linked-path}', htmlPath(dir));
res.setHeader('Content-Type', 'text/html');
@@ -147,6 +196,22 @@ exports.plain = function(req, res, files){
};
/**
+ * Sorting
+ */
+exports.sortMethods = {};
+
+exports.sortMethods.alphabetical = function(a, b) {
+ return a < b ? -1 : 1;
+};
+
+exports.sortMethods.alphaDirFirst = function(a, b) {
+ var ca = (a.isDirectory() ? 0 : 1) + a.name;
+ var cb = (b.isDirectory() ? 0 : 1) + b.name;
+ return ca < cb ? -1 : 1;
+};
+exports.sortMethods.alphaDirFirst.needStat = true;
+
+/**
* Map html `dir`, returning a linked path.
*/
@@ -162,25 +227,31 @@ function htmlPath(dir) {
* Map html `files`, returning an html unordered list.
*/
-function html(files, dir, useIcons) {
- return '<ul id="files">' + files.map(function(file){
- var icon = ''
+function html(files, dir, useIcons, stats) {
+ var sb = [];
+ sb.push('<ul id="files">\n');
+ for(var i = 0, l = files.length; i < l; i++) {
+ var file = files[i]
+ , icon = ''
, classes = [];
if (useIcons && '..' != file) {
icon = icons[extname(file)] || icons.default;
+ if (stats && stats[i].isDirectory && stats[i].isDirectory())
+ icon = icons.directory;
icon = '<img src="data:image/png;base64,' + load(icon) + '" />';
classes.push('icon');
}
- return '<li><a href="'
- + join(dir, file)
- + '" class="'
- + classes.join(' ') + '"'
- + ' title="' + file + '">'
- + icon + file + '</a></li>';
-
- }).join('\n') + '</ul>';
+ sb.push('<li><a href="'
+ , join(dir, file)
+ , '" class="'
+ , classes.join(' '), '"'
+ , ' title="', file, '">'
+ , icon + file + '</a></li>\n');
+ }
+ sb.push('</ul>');
+ return sb.join('');
}
/**
@@ -225,5 +296,6 @@ var icons = {
, '.cpp': 'page_white_cplusplus.png'
, '.swf': 'page_white_flash.png'
, '.pdf': 'page_white_acrobat.png'
+ , 'directory': 'directory.png'
, 'default': 'page_white.png'
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.