diff --git a/README.md b/README.md
index ce6bcf36..32979fe7 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ with the provided Dockerfile.
|`-d` |Show directory listings |`true` |
|`-dir-overrides-404` | Whether `-d` should override magic `404.html` | `false`
|`-i` | Display autoIndex | `true` |
+|`--sort` |Sort directory listings by `name`, `modified`, or `created`. `mtime` and `birthtime` are also accepted. Date sorting shows newest entries first. |`name` |
|`-g` or `--gzip` |When enabled it will serve `./public/some-file.js.gz` in place of `./public/some-file.js` when a gzipped version of the file exists and the request accepts gzip encoding. If brotli is also enabled, it will try to serve brotli first.|`false`|
|`-b` or `--brotli`|When enabled it will serve `./public/some-file.js.br` in place of `./public/some-file.js` when a brotli compressed version of the file exists and the request accepts `br` encoding. If gzip is also enabled, it will try to serve brotli first. |`false`|
|`-e` or `--ext` |Default file extension if none supplied |`html` |
diff --git a/bin/http-server b/bin/http-server
index d9820a52..1a092f7a 100755
--- a/bin/http-server
+++ b/bin/http-server
@@ -30,6 +30,7 @@ if (argv.h || argv.help) {
' --dir-overrides-404 Whether -d should override magic 404.html [false]',
' --base-dir Base directory to serve files from [/]',
' -i Display autoIndex [true]',
+ ' --sort Sort directory listings by name, modified, or created [name]',
' -g --gzip Serve gzip files when possible [false]',
' -b --brotli Serve brotli files when possible [false]',
' If both brotli and gzip are enabled, brotli takes precedence',
@@ -203,6 +204,7 @@ function listen(port) {
dirOverrides404: argv['dir-overrides-404'],
baseDir: baseDir,
autoIndex: argv.i,
+ sort: argv.sort,
gzip: argv.g || argv.gzip,
brotli: argv.b || argv.brotli,
robots: argv.r || argv.robots,
@@ -363,6 +365,7 @@ function listen(port) {
chalk.cyan(Number(argv.t) + ' seconds') : chalk.cyan('120 seconds'))].join('')),
([chalk.yellow('Directory Listings: '), argv.d ? chalk.red('not visible') : chalk.cyan('visible')].join('')),
([chalk.yellow('AutoIndex: '), argv.i ? chalk.red('not visible') : chalk.cyan('visible')].join('')),
+ ([chalk.yellow('Directory Listing Sort: '), argv.sort ? chalk.cyan(argv.sort) : chalk.cyan('name')].join('')),
([chalk.yellow('Serve GZIP Files: '), argv.g || argv.gzip ? chalk.cyan('true') : chalk.red('false')].join('')),
([chalk.yellow('Serve Brotli Files: '), argv.b || argv.brotli ? chalk.cyan('true') : chalk.red('false')].join('')),
([chalk.yellow('Default File Extension: '), argv.e ? chalk.cyan(argv.e) : (argv.ext ? chalk.cyan(argv.ext) : chalk.red('none'))].join('')),
diff --git a/doc/http-server.1 b/doc/http-server.1
index e1f3b04b..46df97b9 100644
--- a/doc/http-server.1
+++ b/doc/http-server.1
@@ -43,6 +43,13 @@ Default is false.
Display autoIndex.
Default is true.
+.TP
+.BI \-\-sort " " \fISORT\fR
+Sort directory listings by name, modified time, or creation time.
+Accepted values are name, modified, mtime, created, and birthtime.
+Date sorting shows newest entries first.
+Default is name.
+
.TP
.BI \-g ", " \-\-gzip
Serve gzip files when possible.
diff --git a/lib/core/aliases.json b/lib/core/aliases.json
index 58c77b74..a72c7d0d 100644
--- a/lib/core/aliases.json
+++ b/lib/core/aliases.json
@@ -1,5 +1,6 @@
{
"autoIndex": [ "autoIndex", "autoindex" ],
+ "sort": [ "sort" ],
"showDir": [ "showDir", "showdir" ],
"dirOverrides404": [
"dirOverrides404",
diff --git a/lib/core/opts.js b/lib/core/opts.js
index d042c406..1ae66dd5 100644
--- a/lib/core/opts.js
+++ b/lib/core/opts.js
@@ -6,6 +6,7 @@ const aliases = require('./aliases.json');
/**
* @typedef {Object} ParsedOptions
* @property {boolean} autoIndex
+ * @property {string} sort
* @property {boolean} showDir
* @property {boolean} dirOverrides404
* @property {boolean} showDotfiles
@@ -37,6 +38,7 @@ module.exports = (opts) => {
/** @type {ParsedOptions} */
const options = {
autoIndex: true,
+ sort: 'name',
showDir: true,
dirOverrides404: false,
showDotfiles: true,
@@ -98,6 +100,22 @@ module.exports = (opts) => {
return false;
});
+ aliases.sort.some((k) => {
+ if (isDeclared(k)) {
+ const sort = String(opts[k]).toLowerCase();
+ const sortAliases = {
+ name: 'name',
+ modified: 'modified',
+ mtime: 'modified',
+ created: 'created',
+ birthtime: 'created',
+ };
+ options.sort = sortAliases[sort] || 'name';
+ return true;
+ }
+ return false;
+ });
+
aliases.showDir.some((k) => {
if (isDeclared(k)) {
options.showDir = opts[k];
diff --git a/lib/core/show-dir/index.js b/lib/core/show-dir/index.js
index 001e41a2..28f95775 100644
--- a/lib/core/show-dir/index.js
+++ b/lib/core/show-dir/index.js
@@ -25,8 +25,39 @@ module.exports = (opts) => {
const handleError = opts.handleError;
const showDotfiles = opts.showDotfiles;
const si = opts.si;
+ const sort = opts.sort;
const weakEtags = opts.weakEtags;
+ function compareByName(a, b) {
+ return a[0].toString().localeCompare(b[0].toString());
+ }
+
+ function compareByDate(statName) {
+ return (a, b) => {
+ if (a[0] === '..' && b[0] !== '..') {
+ return -1;
+ }
+ if (b[0] === '..' && a[0] !== '..') {
+ return 1;
+ }
+
+ const aTime = a[1][statName] instanceof Date ? a[1][statName].getTime() : 0;
+ const bTime = b[1][statName] instanceof Date ? b[1][statName].getTime() : 0;
+
+ return bTime - aTime || compareByName(a, b);
+ };
+ }
+
+ function getListingComparator() {
+ if (sort === 'modified') {
+ return compareByDate('mtime');
+ }
+ if (sort === 'created') {
+ return compareByDate('birthtime');
+ }
+ return compareByName;
+ }
+
return function middleware(req, res, next) {
// Figure out the path for the file from the given url
const parsed = url.parse(req.url);
@@ -182,9 +213,11 @@ module.exports = (opts) => {
'\n';
};
- dirs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);
- renderFiles.sort((a, b) => a.toString().localeCompare(b.toString())).forEach(writeRow);
- errs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);
+ const listingComparator = getListingComparator();
+
+ dirs.sort(listingComparator).forEach(writeRow);
+ renderFiles.sort(listingComparator).forEach(writeRow);
+ errs.sort(compareByName).forEach(writeRow);
html += '\n';
html += `