diff --git a/README.md b/README.md index 5dde8372..f0a4f037 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ the express example). Serve index accepts these properties in the options object. + ##### filter Apply this filter function to files. Defaults to `false`. The `filter` function @@ -50,6 +51,17 @@ Display hidden (dot) files. Defaults to `false`. Display icons. Defaults to `false`. +##### jsonStats + +Enables detailed information to be served via JSON when the request header includes `Accept: application/json`. Defaults to `false`. + +When this flag is disabled, JSON requests will return a simple array of filenames. With this flag enabled, JSON requests will return an array of objects, each containing objects with the following properties: + + - `name` is the relative name for the file. + - `stat` is subset of the `fs.Stats` object for the file. + +The following "safe" properties will be present as file stats: `isFile`,`isDirectory`,`size`, `atime`, `mtime`, `ctime` and `birthtime`. + ##### stylesheet Optional path to a CSS stylesheet. Defaults to a built-in stylesheet. diff --git a/index.js b/index.js index 7b9d856c..3bf60f73 100644 --- a/index.js +++ b/index.js @@ -91,6 +91,7 @@ function serveIndex(root, options) { // resolve root to absolute and normalize var rootPath = normalize(resolve(root) + sep); + var jsonStats = opts.jsonStats || false; var filter = opts.filter; var hidden = opts.hidden; var icons = opts.icons; @@ -160,7 +161,7 @@ function serveIndex(root, options) { // not acceptable if (!type) return next(createError(406)); - serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet); + serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, stylesheet, jsonStats); }); }); }; @@ -222,13 +223,47 @@ serveIndex.html = function _html(req, res, files, next, dir, showUp, icons, path * Respond with application/json. */ -serveIndex.json = function _json(req, res, files) { - var body = JSON.stringify(files); - var buf = new Buffer(body, 'utf8'); +serveIndex.json = function _json(req, res, files, next, dir, showUp, icons, path, view, template, stylesheet, jsonStats) { - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.setHeader('Content-Length', buf.length); - res.end(buf); + if (! jsonStats) { + //return the simple file list with no stats + var body = JSON.stringify(files); + var buf = new Buffer(body, 'utf8'); + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Content-Length', buf.length); + res.end(buf); + } + + else { + // stat all files + stat(path, files, function (err, stats) { + if (err) return next(err); + + // combine the stats into the file list + var filesAndStats = files.map(function (file, i) { + var stat = {}; + + stat["isDirectory"] = stats[i].isDirectory(); + stat["isFile"] = stats[i].isFile(); + + //next select only the safe and non-machine-specific fields from the stat + var safeFields = ["size", "atime", "mtime", "ctime", "birthtime"] + safeFields.forEach(function (field) { + stat[field] = stats[i][field]; + }); + + return { name: file, stat: stat }; + }); + + + //return the file list with stats + var body = JSON.stringify(filesAndStats); + var buf = new Buffer(body, 'utf8'); + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Content-Length', buf.length); + res.end(buf); + }); + } }; /** diff --git a/test/test.js b/test/test.js index 408ba92d..c6db85a8 100644 --- a/test/test.js +++ b/test/test.js @@ -677,6 +677,74 @@ describe('serveIndex(root)', function () { }); }); + describe('when Accept: application/json is given', function () { + describe('when requesting file stats via JSON', function() { + it('should correctly identify files and directories', function (done) { + + var server = createServer(fixtures, {'jsonStats': true}); + + request(server) + .get('/') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(hasTodoStats) + .expect(hasUserStats) + .expect(200, done); + + function hasTodoStats(res) { + //check the todo.txt record + var todo = res.body.find(function(value) { + return value.name == "todo.txt"; + }) + + if (! todo.stat.size == 11) + throw new Error("'todo.txt' size should be 11"); + if (! todo.stat.isFile) + throw new Error("'todo.txt' isFile should be true"); + if (todo.stat.isDirectory) + throw new Error("'todo.txt' isDirectory should be false"); + } + + function hasUserStats(res) { + //check the users record + var users = res.body.find(function(value) { + return value.name == "users"; + }); + + if (users.stat.isFile) + throw new Error("'users' isFile should be false"); + if (!users.stat.isDirectory) + throw new Error("'users' isDirectory should be true"); + } + }); + + it('should not return unsafe stats', function (done) { + + var server = createServer(fixtures, {'jsonStats': true}); + + request(server) + .get('/') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(noUnsafeStats) + .expect(200, done); + + function noUnsafeStats(res) { + var safeFields = ["isFile","isDirectory","size", "atime", "mtime", "ctime", "birthtime"]; + + res.body.forEach(function (item) { + safeFields.forEach(function (field) { + delete item.stat[field]; + }); + + if (Object.keys(item.stat).length>0) + throw new Error("stats contains unsafe keys"); + }); + } + }); + + }); + }); describe('when set with trailing slash', function () { var server; before(function () {