Skip to content

Commit

Permalink
Merge 94e412b into e11f253
Browse files Browse the repository at this point in the history
  • Loading branch information
matz3 committed Mar 25, 2015
2 parents e11f253 + 94e412b commit d598cfc
Show file tree
Hide file tree
Showing 24 changed files with 162 additions and 85 deletions.
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -30,6 +30,8 @@ with a `path` of `'public'` will look at `'public/some/dir'`. If you are using
something like `express`, you can change the URL "base" with `app.use` (see
the express example).

`path` can also be an array to serve an overlay. The first matched file will be used for size and modified date (see the example).

#### Options

Serve index accepts these properties in the options object.
Expand Down Expand Up @@ -111,6 +113,19 @@ app.use('/ftp', serveIndex('public/ftp', {'icons': true}))
app.listen()
```

### Serve indexes from multiple directories with express

```js
var express = require('express')
var serveIndex = require('serve-index')

var app = express()

// Serve URLs like /ftp/thing as an overlay of public1/ftp/thing and public2/ftp/thing
app.use('/ftp', serveIndex(['public1/ftp', 'public2/ftp'], {'icons': true}))
app.listen()
```

## License

[MIT](LICENSE). The [Silk](http://www.famfamfam.com/lab/icons/silk/) icons
Expand Down
181 changes: 116 additions & 65 deletions index.js
Expand Up @@ -68,21 +68,27 @@ var mediaType = {
*
* See Readme.md for documentation of options.
*
* @param {String} path
* @param {String|Array} path
* @param {Object} options
* @return {Function} middleware
* @api public
*/

exports = module.exports = function serveIndex(root, options){
exports = module.exports = function serveIndex(roots, options){
options = options || {};

// root required
if (!root) throw new TypeError('serveIndex() root path required');
if (!roots || roots.length === 0) throw new TypeError('serveIndex() root path required');

// ensure array
if (typeof roots === 'string') {
roots = [roots];
}

// resolve root to absolute and normalize
root = resolve(root);
root = normalize(root + sep);
// resolve roots to absolute and normalize
roots = roots.map(function(root){
return normalize(resolve(root) + sep);
});

var hidden = options.hidden
, icons = options.icons
Expand All @@ -107,55 +113,93 @@ exports = module.exports = function serveIndex(root, options){
var dir = decodeURIComponent(url.pathname);
var originalDir = decodeURIComponent(originalUrl.pathname);

// join / normalize from root dir
var path = normalize(join(root, dir));
// determine ".." display
var showUp = dir !== '/';

// null byte(s), bad request
if (~path.indexOf('\0')) return next(createError(400));
// content-negotiation
var accept = accepts(req);
var type = accept.type(mediaTypes);

// malicious path
if ((path + sep).substr(0, root.length) !== root) {
debug('malicious path "%s"', path);
return next(createError(403));
}
// not acceptable
if (!type) return next(createError(406));

// determine ".." display
var showUp = normalize(resolve(path) + sep) !== root;
var batch = new Batch();

// check if we have a directory
debug('stat "%s"', path);
fs.stat(path, function(err, stat){
if (err && err.code === 'ENOENT') {
return next();
}
batch.concurrency(10);

if (err) {
err.status = err.code === 'ENAMETOOLONG'
? 414
: 500;
return next(err);
}
// read all root dirs and files with batch
roots.forEach(function(root){
batch.push(function(done){

if (!stat.isDirectory()) return next();
// join / normalize from root dir
var path = normalize(join(root, dir));

// fetch files
debug('readdir "%s"', path);
fs.readdir(path, function(err, files){
if (err) return next(err);
if (!hidden) files = removeHidden(files);
if (filter) files = files.filter(function(filename, index, list) {
return filter(filename, index, list, path);
// null byte(s), bad request
if (~path.indexOf('\0')) return done(createError(400));

// malicious path
if ((path + sep).substr(0, root.length) !== root) {
debug('malicious path "%s"', path);
return done(createError(403));
}

// check if we have a directory
debug('stat "%s"', path);
fs.stat(path, function(err, stat){
if (err && err.code === 'ENOENT') {
return done(null, null);
}

if (err) {
err.status = err.code === 'ENAMETOOLONG'
? 414
: 500;
return done(err);
}

if (!stat.isDirectory()) return done(null, null);

// fetch files
debug('readdir "%s"', path);
fs.readdir(path, function(err, files){
if (err) return done(err);

// return dir path and files
done(null, { path: path, files: files });
});
});
});
});

// process all files
batch.end(function(err, dirs){
if (err) return next(err);

var files = [], // all files
fileMap = {}; // remember already added files
dirs.forEach(function(dir){
if (!dir) return;
dir.files.forEach(function(file){
if (!fileMap[file]) {
fileMap[file] = true; // set flag to not add the file again
files.push({ name: file, path: dir.path });
}
});
files.sort();
});

// content-negotiation
var accept = accepts(req);
var type = accept.type(mediaTypes);
// no dirs/files found
if (files.length === 0) {
return next();
}

// not acceptable
if (!type) return next(createError(406));
exports[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet);
if (!hidden) files = removeHidden(files);
if (filter) files = files.filter(function(file, index, list) {
return filter(file.name, index, list, file.path);
});

files.sort(fileSort);

exports[mediaType[type]](req, res, next, originalDir, files, showUp, icons, view, template, stylesheet);
});
};
};
Expand All @@ -164,15 +208,14 @@ exports = module.exports = function serveIndex(root, options){
* Respond with text/html.
*/

exports.html = function(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet){
exports.html = function(req, res, next, dir, files, showUp, icons, view, template, stylesheet){
fs.readFile(template, 'utf8', function(err, str){
if (err) return next(err);
fs.readFile(stylesheet, 'utf8', function(err, style){
if (err) return next(err);
stat(path, files, function(err, stats){
stat(files, function(err, files){
if (err) return next(err);
files = files.map(function(file, i){ return { name: file, stat: stats[i] }; });
files.sort(fileSort);
files.sort(fileStatSort);
if (showUp) files.unshift({ name: '..' });
str = str
.replace(/\{style\}/g, style.concat(iconStyle(files, icons)))
Expand All @@ -193,8 +236,8 @@ exports.html = function(req, res, files, next, dir, showUp, icons, path, view, t
* Respond with application/json.
*/

exports.json = function(req, res, files){
var body = JSON.stringify(files);
exports.json = function(req, res, next, url, files){
var body = JSON.stringify(files.map(function(file){ return file.name; }));
var buf = new Buffer(body, 'utf8');

res.setHeader('Content-Type', 'application/json; charset=utf-8');
Expand All @@ -206,8 +249,8 @@ exports.json = function(req, res, files){
* Respond with text/plain.
*/

exports.plain = function(req, res, files){
var body = files.join('\n') + '\n';
exports.plain = function(req, res, next, url, files){
var body = files.map(function(file){ return file.name; }).join('\n') + '\n';
var buf = new Buffer(body, 'utf8');

res.setHeader('Content-Type', 'text/plain; charset=utf-8');
Expand All @@ -216,12 +259,19 @@ exports.plain = function(req, res, files){
};

/**
* Sort function for with directories first.
* Sort function to compare file names.
*/

function fileSort(a, b){
return String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());
}

/**
* Sort function to compare file names with directories first.
*/

function fileSort(a, b) {
return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) ||
String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());
function fileStatSort(a, b){
return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) || fileSort(a, b);
}

/**
Expand Down Expand Up @@ -440,32 +490,33 @@ function normalizeSlashes(path) {

function removeHidden(files) {
return files.filter(function(file){
return '.' != file[0];
return '.' != file.name[0];
});
}

/**
* Stat all files and return array of stat
* in same order.
* Stat all files and add a `stat` property to each `files` object
*/

function stat(dir, files, cb) {
function stat(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){
fs.stat(join(file.path, file.name), function(err, stat){
if (err && err.code !== 'ENOENT') return done(err);

// pass ENOENT as null stat, not error
done(null, stat || null);
if (stat) file.stat = stat; // add stat property to existing object
done(null, null); // don't pass a result as the passed object was enhanced
});
});
});

batch.end(cb);
batch.end(function(err){
// ignore batch result, just pass back original array which was enhanced
cb(err, files);
});
}

/**
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions test/fixtures/public2/todo.txt
@@ -0,0 +1 @@
- groceries
1 change: 1 addition & 0 deletions test/fixtures/public2/users/index.html
@@ -0,0 +1 @@
<p>tobi, loki, jane</p>
1 change: 1 addition & 0 deletions test/fixtures/public2/users/tobi.txt
@@ -0,0 +1 @@
ferret

0 comments on commit d598cfc

Please sign in to comment.