Skip to content

Commit

Permalink
Merge pull request #590 from walmartlabs/user/eran
Browse files Browse the repository at this point in the history
Support multi directory path
  • Loading branch information
geek committed Feb 27, 2013
2 parents fbf3b47 + 3da8d3c commit 7c92aa0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 46 deletions.
2 changes: 1 addition & 1 deletion docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ a directory content listing.
Routes utilizing the directory handler must include a single path parameter at the end of the path string (e.g. _'/path/to/somewhere/{param}'_).
The directory handler is an object with the following options:
- `path` - a required path string or function. If the `path` is a string, it is used as the prefix for any resources requested within the route by appending the required route path parameter to the provided string. Alternatively, the `path` can be a function with the signature _'function (request) { return './path'; }'_. The function is passed the request object and must return a string with the relative or absolute path to the static resource. Relative paths are resolved based on the server's `files` option as described in (Files)[#files].
- `path` - a required path string, an array of strings, or function. If the `path` is a string, it is used as the prefix for any resources requested within the route by appending the required route path parameter to the provided string. An array of string will be attemped in order until a match is found. Alternatively, the `path` can be a function with the signature _'function (request) { return './path'; }'_. The function is passed the request object and must return a string with the relative or absolute path to the static resource. Relative paths are resolved based on the server's `files` option as described in (Files)[#files].
- `index` - optional boolean, determines if 'index.html' will be served if exists in the folder when requesting a directory. Defaults to _'true'_.
- `listing` - optional boolean, determines if directory listing is generated when a directory is requested without an index document. Defaults to _'false'_.
- `showHidden` - optional boolean, determines if hidden files will be shown and served. Defaults to _'false'_.
Expand Down
57 changes: 29 additions & 28 deletions lib/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ exports.directoryHandler = function (route, options) {

Utils.assert(options, 'Options must exist');
Utils.assert(typeof options === 'object' && options.path, 'Options must be an object with a path');
Utils.assert(typeof options.path === 'function' || typeof options.path === 'string', 'options.path must be a function or a string');
Utils.assert(typeof options.path === 'function' || typeof options.path === 'string' || options.path instanceof Array, 'options.path must be a function, a string, or an array of strings');
Utils.assert(route.path[route.path.length - 1] === '}', 'The route path must end with a parameter');
Utils.assert(route.params.length === 1, 'The route path must include one and only one parameter');

Expand All @@ -65,54 +65,55 @@ exports.directoryHandler = function (route, options) {
// Normalize static string path

if (typeof settings.path === 'string') {
if (settings.path[settings.path.length - 1] !== '/') {
settings.path += '/';
}

if (settings.path[0] !== '/') {
settings.path = absolutePath + '/' + settings.path;
}
settings.path = [settings.path];
}

var handler = function (request) {
var normalize = function (path) {

var path = null;
if (path[path.length - 1] !== '/') {
path += '/';
}

if (typeof settings.path === 'function') {
path = settings.path(request);
if (path[0] !== '/') {
path = absolutePath + '/' + path;
}

return path;
};

// Normalize function string path
var normalized = [];
if (settings.path instanceof Array) {
settings.path.forEach(function (path) {

Utils.assert(path && typeof path === 'string', 'Directory path array must only contain strings');
normalized.push(normalize(path));
});
}

if (path[path.length - 1] !== '/') {
path += '/';
}
var handler = function (request) {

if (path[0] !== '/') {
path = absolutePath + '/' + path;
}
}
else {
path = settings.path;
var paths = normalized;
if (typeof settings.path === 'function') {
paths = [normalize(settings.path(request))];
}

// Append parameter

var isRoot = true;
var selection = null;
if (request._paramsArray[0]) {

if (request._paramsArray[0].indexOf('..') !== -1) {
return request.reply(Boom.forbidden());
}

path += request._paramsArray[0];
isRoot = false;
selection = request._paramsArray[0];
}

// Generate response

var response = new Response.Directory(path, {
var response = new Response.Directory(paths, {

resource: request.path,
isRoot: isRoot,
selection: selection,
index: settings.index,
listing: settings.listing,
showHidden: settings.showHidden
Expand Down
59 changes: 44 additions & 15 deletions lib/response/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var Fs = require('fs');
var Path = require('path');
var Async = require('async');
var Cacheable = require('./cacheable');
var Boom = require('boom');
var File = require('./file');
Expand All @@ -15,7 +16,7 @@ var internals = {};

// File response (Base -> Generic -> Cacheable -> Directory)

exports = module.exports = internals.Directory = function (path, options) {
exports = module.exports = internals.Directory = function (paths, options) {

Utils.assert(this.constructor === internals.Directory, 'Directory must be instantiated using new');
Utils.assert(options, 'Options must exist');
Expand All @@ -24,9 +25,9 @@ exports = module.exports = internals.Directory = function (path, options) {
this.variety = 'directory';
this.varieties.directory = true;

this._path = Path.normalize(path);
this._paths = paths;
this._resource = options.resource;
this._isRoot = options.isRoot;
this._selection = options.selection;
this._index = options.index === false ? false : true; // Defaults to true
this._listing = !!options.listing; // Defaults to false
this._showHidden = !!options.showHidden; // Defaults to false
Expand All @@ -37,19 +38,47 @@ exports = module.exports = internals.Directory = function (path, options) {
Utils.inherits(internals.Directory, Cacheable);


internals.Directory.prototype._prepare = function (request, callback) {
internals.Directory.prototype._prepare = function (request, callback) {

var self = this;

this._wasPrepared = true;
this._wasPrepared = true;

var selection = this._selection || '';

Async.forEachSeries(this._paths, function (path, next) {

if (this._hideFile(this._path)) { // Don't serve hidden files when showHidden is disabled
return callback(Boom.notFound());
}
path = Path.normalize(path + selection);

if (self._hideFile(path)) { // Don't serve hidden files when showHidden is disabled
return callback(Boom.notFound());
}

self._preparePath(path, request, function (response) {

if (response instanceof Error === false ||
response.response.code !== 404) {

return callback(response);
}

next();
});
},
function (err) {

callback(Boom.notFound());
});
};


internals.Directory.prototype._preparePath = function (path, request, callback) {

var self = this;

// Lookup file

(new File(this._path))._prepare(request, function (response) {
(new File(path))._prepare(request, function (response) {

// File loaded successfully

Expand All @@ -73,10 +102,10 @@ internals.Directory.prototype._prepare = function (request, callback) {
}

if (!self._index) {
return self._generateListing(request, callback);
return self._generateListing(path, request, callback);
}

var indexFile = Path.normalize(self._path + (self._path[self._path.length - 1] !== '/' ? '/' : '') + 'index.html');
var indexFile = Path.normalize(path + (path[path.length - 1] !== '/' ? '/' : '') + 'index.html');
(new File(indexFile))._prepare(request, function (indexResponse) {

// File loaded successfully
Expand All @@ -98,17 +127,17 @@ internals.Directory.prototype._prepare = function (request, callback) {
return callback(Boom.forbidden());
}

return self._generateListing(request, callback);
return self._generateListing(path, request, callback);
});
});
};


internals.Directory.prototype._generateListing = function (request, callback) {
internals.Directory.prototype._generateListing = function (path, request, callback) {

var self = this;

Fs.readdir(self._path, function (err, files) {
Fs.readdir(path, function (err, files) {

if (err) {
return callback(Boom.internal('Error accessing directory'));
Expand All @@ -118,7 +147,7 @@ internals.Directory.prototype._generateListing = function (request, callback) {
var display = Utils.escapeHtml(self._resource);
var html = '<html><head><title>' + display + '</title></head><body><h1>Directory: ' + display + '</h1><ul>';

if (!self._isRoot) {
if (self._selection) {
separator = '/';
var parent = self._resource.substring(0, self._resource.lastIndexOf('/')) || '/';
html += '<li><a href="' + internals.pathEncode(parent) + '">Parent Directory</a></li>';
Expand Down
15 changes: 15 additions & 0 deletions test/integration/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,7 @@ describe('Response', function () {
server.route({ method: 'GET', path: '/noshowhidden/{path*}', handler: { directory: { path: './', listing: true } } });
server.route({ method: 'GET', path: '/{path*}', handler: { directory: { path: './', index: true, listing: true } } });
server.route({ method: 'GET', path: '/showindex/{path*}', handler: { directory: { path: './', index: true, listing: true } } });
server.route({ method: 'GET', path: '/multiple/{path*}', handler: { directory: { path: ['./', '../'], listing: true } } });

it('returns a 403 when no index exists and listing is disabled', function (done) {

Expand Down Expand Up @@ -841,6 +842,20 @@ describe('Response', function () {
});
});

it('returns a file when requesting a file from multi directory setup', function (done) {

server.start(function () {

Request.get(server.settings.uri + '/multiple/unit/response/directory.js', function (err, res, body) {

expect(err).to.not.exist;
expect(res.statusCode).to.equal(200);
expect(body).to.contain('no_such_path');
done();
});
});
});

it('returns the correct file when requesting a file from a child directory', function (done) {

server.start(function () {
Expand Down
4 changes: 2 additions & 2 deletions test/unit/response/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe('Response', function () {

it('returns an error when reading an invalid directory', function (done) {

var dir = new Hapi.response.Directory('no_such_path', {});
dir._generateListing(null, function (response) {
var dir = new Hapi.response.Directory(['no_such_path'], {});
dir._generateListing('no_such_path', null, function (response) {

expect(response.response.code).to.equal(500);
done();
Expand Down

0 comments on commit 7c92aa0

Please sign in to comment.