Comparing version 2.2.1 to 3.0.0
@@ -0,1 +1,12 @@ | ||
2017/08/28 Version 3.0.0 | ||
- Lint ./lib/ ./example and ./test against airbnb modified to support node 4.x | ||
and a few quirky hard-to-fix idioms | ||
- Change gzip behavior to default | ||
- Change weak etags and weak etag comparisons to be on by default | ||
- Remove support for 0.12.0 | ||
- Remove union examples and test harnesses (support should have been removed | ||
long ago) | ||
- Fix icon styles in directory listing for small screens | ||
- Update mime to ^v1.4.0 - This changes gzip responses to always have application/gzip as their content-type | ||
2017/06/06 Version 2.2.1 | ||
@@ -2,0 +13,0 @@ - Fix version number in CHANGELOG.md |
@@ -40,15 +40,9 @@ # Contributing Guidelines | ||
Ecstatic's code base follows a relatively consistent style. The closer your | ||
patch blends in with the status quo, the better. | ||
Ecstatic lints using a number of modifications on top of airbnb. If you think | ||
"airbnb except it doesn't need to be transpiled for targeted platforms" you're | ||
pretty close. | ||
A few PROTIPS off the top of my head: | ||
Linting is executed as part of pretest. Your code should pass linting before | ||
being merged. | ||
1. Variables don't need to all be declared at the top, BUT variable *blocks* | ||
should do the whole one-var, tons-of-commas thing. | ||
2. Look at how spacing is done around conditionals and functions. Do it like | ||
that. | ||
3. `else`'s and similar should be on the line *after* the preceding bracket. | ||
We can refine this as the need arises. | ||
## A Few Other Minor Guidelines | ||
@@ -55,0 +49,0 @@ |
@@ -63,1 +63,3 @@ General format is: contributor, github handle, email. In some cases, the | ||
* @wood1986 | ||
* Mahdi Hasheminejad @mahdi-ninja | ||
* Bradley Farias @bmeck <bradley.meck@gmail.com> |
@@ -1,6 +0,9 @@ | ||
var http = require('http'); | ||
var ecstatic = require('../lib/ecstatic')({ | ||
root: __dirname + '/public', | ||
'use strict'; | ||
const http = require('http'); | ||
const ecstatic = require('../lib/ecstatic')({ | ||
root: `${__dirname}/public`, | ||
showDir: true, | ||
autoIndex: true | ||
autoIndex: true, | ||
}); | ||
@@ -7,0 +10,0 @@ |
@@ -1,12 +0,16 @@ | ||
var express = require('express'); | ||
var ecstatic = require('../lib/ecstatic'); | ||
var http = require('http'); | ||
'use strict'; | ||
var app = express(); | ||
const express = require('express'); | ||
const ecstatic = require('../lib/ecstatic'); | ||
const http = require('http'); | ||
const app = express(); | ||
app.use(ecstatic({ | ||
root: __dirname + '/public', | ||
showdir : true | ||
root: `${__dirname}/public`, | ||
showdir: true, | ||
})); | ||
http.createServer(app).listen(8080); | ||
console.log('Listening on :8080'); |
#! /usr/bin/env node | ||
var path = require('path'), | ||
fs = require('fs'), | ||
url = require('url'), | ||
mime = require('mime'), | ||
urlJoin = require('url-join'), | ||
showDir = require('./ecstatic/show-dir'), | ||
version = JSON.parse( | ||
fs.readFileSync(__dirname + '/../package.json').toString() | ||
).version, | ||
status = require('./ecstatic/status-handlers'), | ||
generateEtag = require('./ecstatic/etag'), | ||
optsParser = require('./ecstatic/opts'); | ||
'use strict'; | ||
var ecstatic = module.exports = function (dir, options) { | ||
if (typeof dir !== 'string') { | ||
options = dir; | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
const url = require('url'); | ||
const mime = require('mime'); | ||
const urlJoin = require('url-join'); | ||
const showDir = require('./ecstatic/show-dir'); | ||
const version = require('../package.json').version; | ||
const status = require('./ecstatic/status-handlers'); | ||
const generateEtag = require('./ecstatic/etag'); | ||
const optsParser = require('./ecstatic/opts'); | ||
let ecstatic = null; | ||
// See: https://github.com/jesusabdullah/node-ecstatic/issues/109 | ||
function decodePathname(pathname) { | ||
const pieces = pathname.replace(/\\/g, '/').split('/'); | ||
return pieces.map((rawPiece) => { | ||
const piece = decodeURIComponent(rawPiece); | ||
if (process.platform === 'win32' && /\\/.test(piece)) { | ||
throw new Error('Invalid forward slash character'); | ||
} | ||
return piece; | ||
}).join('/'); | ||
} | ||
// Check to see if we should try to compress a file with gzip. | ||
function shouldCompress(req) { | ||
const headers = req.headers; | ||
return headers && headers['accept-encoding'] && | ||
headers['accept-encoding'] | ||
.split(',') | ||
.some(el => ['*', 'compress', 'gzip', 'deflate'].indexOf(el) !== -1) | ||
; | ||
} | ||
function hasGzipId12(gzipped, cb) { | ||
const stream = fs.createReadStream(gzipped, { start: 0, end: 1 }); | ||
let buffer = Buffer(''); | ||
let hasBeenCalled = false; | ||
stream.on('data', (chunk) => { | ||
buffer = Buffer.concat([buffer, chunk], 2); | ||
}); | ||
stream.on('error', (err) => { | ||
if (hasBeenCalled) { | ||
throw err; | ||
} | ||
hasBeenCalled = true; | ||
cb(err); | ||
}); | ||
stream.on('close', () => { | ||
if (hasBeenCalled) { | ||
return; | ||
} | ||
hasBeenCalled = true; | ||
cb(null, buffer[0] === 31 && buffer[1] === 139); | ||
}); | ||
} | ||
module.exports = function createMiddleware(_dir, _options) { | ||
let dir; | ||
let options; | ||
if (typeof _dir === 'string') { | ||
dir = _dir; | ||
options = _options; | ||
} else { | ||
options = _dir; | ||
dir = options.root; | ||
} | ||
var root = path.join(path.resolve(dir), '/'), | ||
opts = optsParser(options), | ||
cache = opts.cache, | ||
autoIndex = opts.autoIndex, | ||
baseDir = opts.baseDir, | ||
defaultExt = opts.defaultExt, | ||
handleError = opts.handleError, | ||
headers = opts.headers, | ||
serverHeader = opts.serverHeader, | ||
weakEtags = opts.weakEtags, | ||
handleOptionsMethod = opts.handleOptionsMethod; | ||
const root = path.join(path.resolve(dir), '/'); | ||
const opts = optsParser(options); | ||
const cache = opts.cache; | ||
const autoIndex = opts.autoIndex; | ||
const baseDir = opts.baseDir; | ||
let defaultExt = opts.defaultExt; | ||
const handleError = opts.handleError; | ||
const headers = opts.headers; | ||
const serverHeader = opts.serverHeader; | ||
const weakEtags = opts.weakEtags; | ||
const handleOptionsMethod = opts.handleOptionsMethod; | ||
opts.root = dir; | ||
if (defaultExt && /^\./.test(defaultExt)) defaultExt = defaultExt.replace(/^\./, ''); | ||
if (defaultExt && /^\./.test(defaultExt)) { | ||
defaultExt = defaultExt.replace(/^\./, ''); | ||
} | ||
@@ -42,7 +108,8 @@ // Support hashes and .types files in mimeTypes @since 0.8 | ||
opts.mimeTypes = JSON.parse(opts.mimeTypes); | ||
} catch (e) {} | ||
} catch (e) { | ||
// swallow parse errors, treat this as a string mimetype input | ||
} | ||
if (typeof opts.mimeTypes === 'string') { | ||
mime.load(opts.mimeTypes); | ||
} | ||
else if (typeof opts.mimeTypes === 'object') { | ||
} else if (typeof opts.mimeTypes === 'object') { | ||
mime.define(opts.mimeTypes); | ||
@@ -52,5 +119,55 @@ } | ||
function shouldReturn304(req, serverLastModified, serverEtag) { | ||
if (!req || !req.headers) { | ||
return false; | ||
} | ||
return function middleware (req, res, next) { | ||
const clientModifiedSince = req.headers['if-modified-since']; | ||
const clientEtag = req.headers['if-none-match']; | ||
let clientModifiedDate; | ||
if (!clientModifiedSince && !clientEtag) { | ||
// Client did not provide any conditional caching headers | ||
return false; | ||
} | ||
if (clientModifiedSince) { | ||
// Catch "illegal access" dates that will crash v8 | ||
// https://github.com/jfhbrook/node-ecstatic/pull/179 | ||
try { | ||
clientModifiedDate = new Date(Date.parse(clientModifiedSince)); | ||
} catch (err) { | ||
return false; | ||
} | ||
if (clientModifiedDate.toString() === 'Invalid Date') { | ||
return false; | ||
} | ||
// If the client's copy is older than the server's, don't return 304 | ||
if (clientModifiedDate < new Date(serverLastModified)) { | ||
return false; | ||
} | ||
} | ||
if (clientEtag) { | ||
// Do a strong or weak etag comparison based on setting | ||
// https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3 | ||
if (opts.weakCompare && clientEtag !== serverEtag | ||
&& clientEtag !== `W/${serverEtag}` && `W/${clientEtag}` !== serverEtag) { | ||
return false; | ||
} else if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
return function middleware(req, res, next) { | ||
// Figure out the path for the file from the given url | ||
const parsed = url.parse(req.url); | ||
let pathname = null; | ||
let file = null; | ||
let gzipped = null; | ||
// Strip any null bytes from the url | ||
@@ -67,3 +184,3 @@ // This was at one point necessary because of an old bug in url.parse | ||
/* | ||
while(req.url.indexOf('%00') !== -1) { | ||
while (req.url.indexOf('%00') !== -1) { | ||
req.url = req.url.replace(/\%00/g, ''); | ||
@@ -73,32 +190,30 @@ } | ||
// Figure out the path for the file from the given url | ||
var parsed = url.parse(req.url); | ||
try { | ||
decodeURIComponent(req.url); // check validity of url | ||
var pathname = decodePathname(parsed.pathname); | ||
pathname = decodePathname(parsed.pathname); | ||
} catch (err) { | ||
status[400](res, next, { error: err }); | ||
return; | ||
} | ||
catch (err) { | ||
return status[400](res, next, { error: err }); | ||
} | ||
var file = path.normalize( | ||
path.join(root, | ||
path.relative( | ||
path.join('/', baseDir), | ||
pathname | ||
) | ||
) | ||
), | ||
gzipped = file + '.gz'; | ||
file = path.normalize( | ||
path.join( | ||
root, | ||
path.relative(path.join('/', baseDir), pathname) | ||
) | ||
); | ||
gzipped = `${file}.gz`; | ||
if(serverHeader !== false) { | ||
if (serverHeader !== false) { | ||
// Set common headers. | ||
res.setHeader('server', 'ecstatic-'+version); | ||
res.setHeader('server', `ecstatic-${version}`); | ||
} | ||
Object.keys(headers).forEach(function (key) { | ||
res.setHeader(key, headers[key]) | ||
}) | ||
Object.keys(headers).forEach((key) => { | ||
res.setHeader(key, headers[key]); | ||
}); | ||
if (req.method === 'OPTIONS' && handleOptionsMethod) { | ||
return res.end(); | ||
res.end(); | ||
return; | ||
} | ||
@@ -109,105 +224,22 @@ | ||
if (file.slice(0, root.length) !== root) { | ||
return status[403](res, next); | ||
status[403](res, next); | ||
return; | ||
} | ||
if (req.method && (req.method !== 'GET' && req.method !== 'HEAD' )) { | ||
return status[405](res, next); | ||
if (req.method && (req.method !== 'GET' && req.method !== 'HEAD')) { | ||
status[405](res, next); | ||
return; | ||
} | ||
function statFile() { | ||
fs.stat(file, function (err, stat) { | ||
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) { | ||
if (req.statusCode == 404) { | ||
// This means we're already trying ./404.html and can not find it. | ||
// So send plain text response with 404 status code | ||
status[404](res, next); | ||
} | ||
else if (!path.extname(parsed.pathname).length && defaultExt) { | ||
// If there is no file extension in the path and we have a default | ||
// extension try filename and default extension combination before rendering 404.html. | ||
middleware({ | ||
url: parsed.pathname + '.' + defaultExt + ((parsed.search) ? parsed.search : ''), | ||
headers: req.headers | ||
}, res, next); | ||
} | ||
else { | ||
// Try to serve default ./404.html | ||
middleware({ | ||
url: (handleError ? ('/' + path.join(baseDir, '404.' + defaultExt)) : req.url), | ||
headers: req.headers, | ||
statusCode: 404 | ||
}, res, next); | ||
} | ||
} | ||
else if (err) { | ||
status[500](res, next, { error: err }); | ||
} | ||
else if (stat.isDirectory()) { | ||
if (!autoIndex && !opts.showDir) { | ||
status[404](res, next); | ||
return; | ||
} | ||
// 302 to / if necessary | ||
if (!parsed.pathname.match(/\/$/)) { | ||
res.statusCode = 302; | ||
res.setHeader('location', parsed.pathname + '/' + | ||
(parsed.query? ('?' + parsed.query):'') | ||
); | ||
return res.end(); | ||
} | ||
if (autoIndex) { | ||
return middleware({ | ||
url: urlJoin(encodeURIComponent(pathname), '/index.' + defaultExt), | ||
headers: req.headers | ||
}, res, function (err) { | ||
if (err) { | ||
return status[500](res, next, { error: err }); | ||
} | ||
if (opts.showDir) { | ||
return showDir(opts, stat)(req, res); | ||
} | ||
return status[403](res, next); | ||
}); | ||
} | ||
if (opts.showDir) { | ||
return showDir(opts, stat)(req, res); | ||
} | ||
} | ||
else { | ||
serve(stat); | ||
} | ||
}); | ||
} | ||
// Look for a gzipped file if this is turned on | ||
if (opts.gzip && shouldCompress(req)) { | ||
fs.stat(gzipped, function (err, stat) { | ||
if (!err && stat.isFile()) { | ||
hasGzipId12(gzipped, function (err, isGzip) { | ||
if (isGzip) { | ||
file = gzipped; | ||
return serve(stat); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} else { | ||
statFile(); | ||
} | ||
function serve(stat) { | ||
// Do a MIME lookup, fall back to octet-stream and handle gzip | ||
// special case. | ||
var defaultType = opts.contentType || 'application/octet-stream', | ||
contentType = mime.lookup(file, defaultType), | ||
charSet; | ||
const defaultType = opts.contentType || 'application/octet-stream'; | ||
let contentType = mime.lookup(file, defaultType); | ||
let charSet; | ||
const range = (req.headers && req.headers.range); | ||
const lastModified = (new Date(stat.mtime)).toUTCString(); | ||
const etag = generateEtag(stat, weakEtags); | ||
let stream = null; | ||
@@ -217,3 +249,3 @@ if (contentType) { | ||
if (charSet) { | ||
contentType += '; charset=' + charSet; | ||
contentType += `; charset=${charSet}`; | ||
} | ||
@@ -226,29 +258,35 @@ } | ||
// strip gz ending and lookup mime type | ||
contentType = mime.lookup(path.basename(file, ".gz"), defaultType); | ||
contentType = mime.lookup(path.basename(file, '.gz'), defaultType); | ||
} | ||
var range = (req.headers && req.headers['range']); | ||
if (range) { | ||
var total = stat.size; | ||
var parts = range.replace(/bytes=/, "").split("-"); | ||
var partialstart = parts[0]; | ||
var partialend = parts[1]; | ||
var start = parseInt(partialstart, 10); | ||
var end = Math.min(total-1, partialend ? parseInt(partialend, 10) : total-1); | ||
var chunksize = (end-start)+1; | ||
const total = stat.size; | ||
const parts = range.replace(/bytes=/, '').split('-'); | ||
const partialstart = parts[0]; | ||
const partialend = parts[1]; | ||
const start = parseInt(partialstart, 10); | ||
const end = Math.min( | ||
total - 1, | ||
partialend ? parseInt(partialend, 10) : total - 1 | ||
); | ||
const chunksize = (end - start) + 1; | ||
let fstream = null; | ||
if (start > end || isNaN(start) || isNaN(end)) { | ||
return status['416'](res, next); | ||
status['416'](res, next); | ||
return; | ||
} | ||
var fstream = fs.createReadStream(file, {start: start, end: end}); | ||
fstream.on('error', function (err) { | ||
fstream = fs.createReadStream(file, { start, end }); | ||
fstream.on('error', (err) => { | ||
status['500'](res, next, { error: err }); | ||
}); | ||
res.on('close', function () { | ||
fstream.destroy(); | ||
res.on('close', () => { | ||
fstream.destroy(); | ||
}); | ||
res.writeHead(206, { | ||
'Content-Range': 'bytes ' + start + '-' + end + '/' + total, | ||
'Content-Range': `bytes ${start}-${end}/${total}`, | ||
'Accept-Ranges': 'bytes', | ||
'Content-Length': chunksize, | ||
'Content-Type': contentType | ||
'Content-Type': contentType, | ||
}); | ||
@@ -260,4 +298,2 @@ fstream.pipe(res); | ||
// TODO: Helper for this, with default headers. | ||
var lastModified = (new Date(stat.mtime)).toUTCString(), | ||
etag = generateEtag(stat, weakEtags); | ||
res.setHeader('last-modified', lastModified); | ||
@@ -267,5 +303,5 @@ res.setHeader('etag', etag); | ||
if (typeof cache === 'function') { | ||
var requestSpecificCache = cache(pathname); | ||
let requestSpecificCache = cache(pathname); | ||
if (typeof requestSpecificCache === 'number') { | ||
requestSpecificCache = 'max-age=' + requestSpecificCache; | ||
requestSpecificCache = `max-age=${requestSpecificCache}`; | ||
} | ||
@@ -279,3 +315,4 @@ res.setHeader('cache-control', requestSpecificCache); | ||
if (shouldReturn304(req, lastModified, etag)) { | ||
return status[304](res, next); | ||
status[304](res, next); | ||
return; | ||
} | ||
@@ -291,10 +328,11 @@ | ||
if (req.method === "HEAD") { | ||
return res.end(); | ||
if (req.method === 'HEAD') { | ||
res.end(); | ||
return; | ||
} | ||
var stream = fs.createReadStream(file); | ||
stream = fs.createReadStream(file); | ||
stream.pipe(res); | ||
stream.on('error', function (err) { | ||
stream.on('error', (err) => { | ||
status['500'](res, next, { error: err }); | ||
@@ -304,44 +342,91 @@ }); | ||
function shouldReturn304(req, serverLastModified, serverEtag) { | ||
if (!req || !req.headers) { | ||
return false; | ||
} | ||
var clientModifiedSince = req.headers['if-modified-since'], | ||
clientEtag = req.headers['if-none-match']; | ||
function statFile() { | ||
fs.stat(file, (err, stat) => { | ||
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) { | ||
if (req.statusCode === 404) { | ||
// This means we're already trying ./404.html and can not find it. | ||
// So send plain text response with 404 status code | ||
status[404](res, next); | ||
} else if (!path.extname(parsed.pathname).length && defaultExt) { | ||
// If there is no file extension in the path and we have a default | ||
// extension try filename and default extension combination before rendering 404.html. | ||
middleware({ | ||
url: `${parsed.pathname}.${defaultExt}${(parsed.search) ? parsed.search : ''}`, | ||
headers: req.headers, | ||
}, res, next); | ||
} else { | ||
// Try to serve default ./404.html | ||
middleware({ | ||
url: (handleError ? `/${path.join(baseDir, `404.${defaultExt}`)}` : req.url), | ||
headers: req.headers, | ||
statusCode: 404, | ||
}, res, next); | ||
} | ||
} else if (err) { | ||
status[500](res, next, { error: err }); | ||
} else if (stat.isDirectory()) { | ||
if (!autoIndex && !opts.showDir) { | ||
status[404](res, next); | ||
return; | ||
} | ||
if (!clientModifiedSince && !clientEtag) { | ||
// Client did not provide any conditional caching headers | ||
return false; | ||
} | ||
// 302 to / if necessary | ||
if (!parsed.pathname.match(/\/$/)) { | ||
res.statusCode = 302; | ||
const q = parsed.query ? `?${parsed.query}` : ''; | ||
res.setHeader('location', `${parsed.pathname}/${q}`); | ||
res.end(); | ||
return; | ||
} | ||
if (clientModifiedSince) { | ||
// Catch "illegal access" dates that will crash v8 | ||
// https://github.com/jfhbrook/node-ecstatic/pull/179 | ||
try { | ||
var clientModifiedDate = new Date(Date.parse(clientModifiedSince)); | ||
} | ||
catch (err) { return false } | ||
if (autoIndex) { | ||
middleware({ | ||
url: urlJoin( | ||
encodeURIComponent(pathname), | ||
`/index.${defaultExt}` | ||
), | ||
headers: req.headers, | ||
}, res, (autoIndexError) => { | ||
if (autoIndexError) { | ||
status[500](res, next, { error: autoIndexError }); | ||
return; | ||
} | ||
if (opts.showDir) { | ||
showDir(opts, stat)(req, res); | ||
return; | ||
} | ||
if (clientModifiedDate.toString() === 'Invalid Date') { | ||
return false; | ||
status[403](res, next); | ||
}); | ||
return; | ||
} | ||
if (opts.showDir) { | ||
showDir(opts, stat)(req, res); | ||
} | ||
} else { | ||
serve(stat); | ||
} | ||
// If the client's copy is older than the server's, don't return 304 | ||
if (clientModifiedDate < new Date(serverLastModified)) { | ||
return false; | ||
} | ||
} | ||
}); | ||
} | ||
if (clientEtag) { | ||
// Do a strong or weak etag comparison based on setting | ||
// https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3 | ||
if (opts.weakCompare && clientEtag !== serverEtag | ||
&& clientEtag !== ('W/' + serverEtag) && ('W/' + clientEtag) !== serverEtag) { | ||
return false; | ||
} else if (!opts.weakCompare && (clientEtag !== serverEtag || clientEtag.indexOf('W/') === 0)) { | ||
return false; | ||
// Look for a gzipped file if this is turned on | ||
if (opts.gzip && shouldCompress(req)) { | ||
fs.stat(gzipped, (err, stat) => { | ||
if (!err && stat.isFile()) { | ||
hasGzipId12(gzipped, (gzipErr, isGzip) => { | ||
if (!gzipErr && isGzip) { | ||
file = gzipped; | ||
serve(stat); | ||
} else { | ||
statFile(); | ||
} | ||
}); | ||
} else { | ||
statFile(); | ||
} | ||
} | ||
return true; | ||
}); | ||
} else { | ||
statFile(); | ||
} | ||
@@ -351,86 +436,37 @@ }; | ||
ecstatic = module.exports; | ||
ecstatic.version = version; | ||
ecstatic.showDir = showDir; | ||
function hasGzipId12(gzipped, cb) { | ||
var stream = fs.createReadStream(gzipped, { start: 0, end: 1 }), | ||
buffer = Buffer(''), | ||
hasBeenCalled = false; | ||
stream.on("data", function (chunk) { | ||
buffer = Buffer.concat([buffer, chunk], 2); | ||
}); | ||
if (!module.parent) { | ||
/* eslint-disable global-require */ | ||
/* eslint-disable no-console */ | ||
const defaults = require('./ecstatic/defaults.json'); | ||
const http = require('http'); | ||
const minimist = require('minimist'); | ||
const aliases = require('./ecstatic/aliases.json'); | ||
stream.on("error", function (err) { | ||
if (hasBeenCalled) { | ||
throw err; | ||
} | ||
hasBeenCalled = true; | ||
cb(error); | ||
const opts = minimist(process.argv.slice(2), { | ||
alias: aliases, | ||
default: defaults, | ||
boolean: Object.keys(defaults).filter( | ||
key => typeof defaults[key] === 'boolean' | ||
), | ||
}); | ||
const envPORT = parseInt(process.env.PORT, 10); | ||
const port = envPORT > 1024 && envPORT <= 65536 ? envPORT : opts.port || opts.p || 8000; | ||
const dir = opts.root || opts._[0] || process.cwd(); | ||
stream.on("close", function () { | ||
if (hasBeenCalled) { | ||
return; | ||
} | ||
hasBeenCalled = true; | ||
cb(null, buffer[0] == 31 && buffer[1] == 139); | ||
}); | ||
} | ||
// Check to see if we should try to compress a file with gzip. | ||
function shouldCompress(req) { | ||
var headers = req.headers; | ||
return headers && headers['accept-encoding'] && | ||
headers['accept-encoding'] | ||
.split(",") | ||
.some(function (el) { | ||
return ['*','compress', 'gzip', 'deflate'].indexOf(el) != -1; | ||
if (opts.help || opts.h) { | ||
console.error('usage: ecstatic [dir] {options} --port PORT'); | ||
console.error('see https://npm.im/ecstatic for more docs'); | ||
} else { | ||
http.createServer(ecstatic(dir, opts)) | ||
.listen(port, () => { | ||
console.log(`ecstatic serving ${dir} at http://0.0.0.0:${port}`); | ||
}) | ||
; | ||
} | ||
// See: https://github.com/jesusabdullah/node-ecstatic/issues/109 | ||
function decodePathname(pathname) { | ||
var pieces = pathname.replace(/\\/g,"/").split('/'); | ||
return pieces.map(function (piece) { | ||
piece = decodeURIComponent(piece); | ||
if (process.platform === 'win32' && /\\/.test(piece)) { | ||
throw new Error('Invalid forward slash character'); | ||
} | ||
return piece; | ||
}).join('/'); | ||
} | ||
if (!module.parent) { | ||
var defaults = require('./ecstatic/defaults.json') | ||
var http = require('http'), | ||
opts = require('minimist')(process.argv.slice(2), { | ||
alias: require('./ecstatic/aliases.json'), | ||
default: defaults, | ||
boolean: Object.keys(defaults).filter(function (key) { | ||
return typeof defaults[key] === 'boolean' | ||
}) | ||
}), | ||
envPORT = parseInt(process.env.PORT, 10), | ||
port = envPORT > 1024 && envPORT <= 65536 ? envPORT : opts.port || opts.p || 8000, | ||
dir = opts.root || opts._[0] || process.cwd(); | ||
if (opts.help || opts.h) { | ||
var u = console.error; | ||
u('usage: ecstatic [dir] {options} --port PORT'); | ||
u('see https://npm.im/ecstatic for more docs'); | ||
return; | ||
} | ||
http.createServer(ecstatic(dir, opts)) | ||
.listen(port, function () { | ||
console.log('ecstatic serving ' + dir + ' at http://0.0.0.0:' + port); | ||
}); | ||
} |
@@ -9,3 +9,3 @@ { | ||
"cors": false, | ||
"gzip": false, | ||
"gzip": true, | ||
"defaultExt": ".html", | ||
@@ -15,5 +15,5 @@ "handleError": true, | ||
"contentType": "application/octet-stream", | ||
"weakEtags": false, | ||
"weakCompare": false, | ||
"weakEtags": true, | ||
"weakCompare": true, | ||
"handleOptionsMethod": false | ||
} |
@@ -1,7 +0,9 @@ | ||
module.exports = function (stat, weakEtag) { | ||
var etag = '"' + [stat.ino, stat.size, JSON.stringify(stat.mtime)].join('-') + '"'; | ||
'use strict'; | ||
module.exports = (stat, weakEtag) => { | ||
let etag = `"${[stat.ino, stat.size, JSON.stringify(stat.mtime)].join('-')}"`; | ||
if (weakEtag) { | ||
etag = 'W/' + etag; | ||
etag = `W/${etag}`; | ||
} | ||
return etag; | ||
} | ||
}; |
@@ -0,23 +1,25 @@ | ||
'use strict'; | ||
// This is so you can have options aliasing and defaults in one place. | ||
var defaults = require('./defaults.json'); | ||
var aliases = require('./aliases.json') | ||
const defaults = require('./defaults.json'); | ||
const aliases = require('./aliases.json'); | ||
module.exports = function (opts) { | ||
var autoIndex = defaults.autoIndex, | ||
showDir = defaults.showDir, | ||
showDotfiles = defaults.showDotfiles, | ||
humanReadable = defaults.humanReadable, | ||
si = defaults.si, | ||
cache = defaults.cache, | ||
gzip = defaults.gzip, | ||
defaultExt = defaults.defaultExt, | ||
handleError = defaults.handleError, | ||
headers = {}, | ||
serverHeader = defaults.serverHeader, | ||
contentType = defaults.contentType, | ||
mimeTypes, | ||
weakEtags = defaults.weakEtags, | ||
weakCompare = defaults.weakCompare, | ||
handleOptionsMethod = defaults.handleOptionsMethod; | ||
module.exports = (opts) => { | ||
let autoIndex = defaults.autoIndex; | ||
let showDir = defaults.showDir; | ||
let showDotfiles = defaults.showDotfiles; | ||
let humanReadable = defaults.humanReadable; | ||
let si = defaults.si; | ||
let cache = defaults.cache; | ||
let gzip = defaults.gzip; | ||
let defaultExt = defaults.defaultExt; | ||
let handleError = defaults.handleError; | ||
const headers = {}; | ||
let serverHeader = defaults.serverHeader; | ||
let contentType = defaults.contentType; | ||
let mimeTypes; | ||
let weakEtags = defaults.weakEtags; | ||
let weakCompare = defaults.weakCompare; | ||
let handleOptionsMethod = defaults.handleOptionsMethod; | ||
@@ -28,4 +30,14 @@ function isDeclared(k) { | ||
function setHeader(str) { | ||
const m = /^(.+?)\s*:\s*(.*)$/.exec(str); | ||
if (!m) { | ||
headers[str] = true; | ||
} else { | ||
headers[m[1]] = m[2]; | ||
} | ||
} | ||
if (opts) { | ||
aliases.autoIndex.some(function (k) { | ||
aliases.autoIndex.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -35,5 +47,6 @@ autoIndex = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.showDir.some(function (k) { | ||
aliases.showDir.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -43,5 +56,6 @@ showDir = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.showDotfiles.some(function (k) { | ||
aliases.showDotfiles.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -51,5 +65,6 @@ showDotfiles = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.humanReadable.some(function (k) { | ||
aliases.humanReadable.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -59,5 +74,6 @@ humanReadable = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.si.some(function (k) { | ||
aliases.si.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -67,2 +83,3 @@ si = opts[k]; | ||
} | ||
return false; | ||
}); | ||
@@ -77,9 +94,7 @@ | ||
cache = opts.cache; | ||
} else if (typeof opts.cache === 'number') { | ||
cache = `max-age=${opts.cache}`; | ||
} else if (typeof opts.cache === 'function') { | ||
cache = opts.cache; | ||
} | ||
else if (typeof opts.cache === 'number') { | ||
cache = 'max-age=' + opts.cache; | ||
} | ||
else if (typeof opts.cache === 'function') { | ||
cache = opts.cache | ||
} | ||
} | ||
@@ -91,3 +106,3 @@ | ||
aliases.handleError.some(function (k) { | ||
aliases.handleError.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -97,5 +112,6 @@ handleError = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.cors.forEach(function(k) { | ||
aliases.cors.forEach((k) => { | ||
if (isDeclared(k) && k) { | ||
@@ -108,22 +124,17 @@ handleOptionsMethod = true; | ||
aliases.headers.forEach(function (k) { | ||
if (!isDeclared(k)) return; | ||
if (Array.isArray(opts[k])) { | ||
opts[k].forEach(setHeader); | ||
aliases.headers.forEach((k) => { | ||
if (isDeclared(k)) { | ||
if (Array.isArray(opts[k])) { | ||
opts[k].forEach(setHeader); | ||
} else if (opts[k] && typeof opts[k] === 'object') { | ||
Object.keys(opts[k]).forEach((key) => { | ||
headers[key] = opts[k][key]; | ||
}); | ||
} else { | ||
setHeader(opts[k]); | ||
} | ||
} | ||
else if (opts[k] && typeof opts[k] === 'object') { | ||
Object.keys(opts[k]).forEach(function (key) { | ||
headers[key] = opts[k][key]; | ||
}); | ||
} | ||
else setHeader(opts[k]); | ||
function setHeader (str) { | ||
var m = /^(.+?)\s*:\s*(.*)$/.exec(str) | ||
if (!m) headers[str] = true | ||
else headers[m[1]] = m[2] | ||
} | ||
}); | ||
aliases.serverHeader.some(function (k) { | ||
aliases.serverHeader.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -133,5 +144,6 @@ serverHeader = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.contentType.some(function (k) { | ||
aliases.contentType.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -141,5 +153,6 @@ contentType = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.mimeType.some(function (k) { | ||
aliases.mimeType.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -149,5 +162,6 @@ mimeTypes = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.weakEtags.some(function (k) { | ||
aliases.weakEtags.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -157,5 +171,6 @@ weakEtags = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.weakCompare.some(function (k) { | ||
aliases.weakCompare.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -165,5 +180,6 @@ weakCompare = opts[k]; | ||
} | ||
return false; | ||
}); | ||
aliases.handleOptionsMethod.some(function (k) { | ||
aliases.handleOptionsMethod.some((k) => { | ||
if (isDeclared(k)) { | ||
@@ -173,2 +189,3 @@ handleOptionsMethod = handleOptionsMethod || opts[k]; | ||
} | ||
return false; | ||
}); | ||
@@ -178,20 +195,20 @@ } | ||
return { | ||
cache: cache, | ||
autoIndex: autoIndex, | ||
showDir: showDir, | ||
showDotfiles: showDotfiles, | ||
humanReadable: humanReadable, | ||
si: si, | ||
defaultExt: defaultExt, | ||
cache, | ||
autoIndex, | ||
showDir, | ||
showDotfiles, | ||
humanReadable, | ||
si, | ||
defaultExt, | ||
baseDir: (opts && opts.baseDir) || '/', | ||
gzip: gzip, | ||
handleError: handleError, | ||
headers: headers, | ||
serverHeader: serverHeader, | ||
contentType: contentType, | ||
mimeTypes: mimeTypes, | ||
weakEtags: weakEtags, | ||
weakCompare: weakCompare, | ||
handleOptionsMethod: handleOptionsMethod | ||
gzip, | ||
handleError, | ||
headers, | ||
serverHeader, | ||
contentType, | ||
mimeTypes, | ||
weakEtags, | ||
weakCompare, | ||
handleOptionsMethod, | ||
}; | ||
}; |
@@ -1,48 +0,63 @@ | ||
var styles = require('./styles'), | ||
supportedIcons = styles.icons, | ||
css = styles.css, | ||
permsToString = require('./perms-to-string'), | ||
sizeToString = require('./size-to-string'), | ||
sortFiles = require('./sort-files'), | ||
fs = require('fs'), | ||
path = require('path'), | ||
he = require('he'), | ||
etag = require('../etag'), | ||
url = require('url'), | ||
status = require('../status-handlers'); | ||
'use strict'; | ||
module.exports = function (opts, stat) { | ||
const styles = require('./styles'); | ||
const permsToString = require('./perms-to-string'); | ||
const sizeToString = require('./size-to-string'); | ||
const sortFiles = require('./sort-files'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const he = require('he'); | ||
const etag = require('../etag'); | ||
const url = require('url'); | ||
const status = require('../status-handlers'); | ||
const supportedIcons = styles.icons; | ||
const css = styles.css; | ||
module.exports = (opts) => { | ||
// opts are parsed by opts.js, defaults already applied | ||
var cache = opts.cache, | ||
root = path.resolve(opts.root), | ||
baseDir = opts.baseDir, | ||
humanReadable = opts.humanReadable, | ||
handleError = opts.handleError, | ||
showDotfiles = opts.showDotfiles, | ||
si = opts.si, | ||
weakEtags = opts.weakEtags; | ||
const cache = opts.cache; | ||
const root = path.resolve(opts.root); | ||
const baseDir = opts.baseDir; | ||
const humanReadable = opts.humanReadable; | ||
const handleError = opts.handleError; | ||
const showDotfiles = opts.showDotfiles; | ||
const si = opts.si; | ||
const weakEtags = opts.weakEtags; | ||
return function middleware (req, res, next) { | ||
return function middleware(req, res, next) { | ||
// Figure out the path for the file from the given url | ||
var parsed = url.parse(req.url), | ||
pathname = decodeURIComponent(parsed.pathname), | ||
dir = path.normalize( | ||
path.join(root, | ||
path.relative( | ||
path.join('/', baseDir), | ||
pathname | ||
) | ||
) | ||
); | ||
const parsed = url.parse(req.url); | ||
const pathname = decodeURIComponent(parsed.pathname); | ||
const dir = path.normalize( | ||
path.join( | ||
root, | ||
path.relative( | ||
path.join('/', baseDir), | ||
pathname | ||
) | ||
) | ||
); | ||
fs.stat(dir, function (err, stat) { | ||
if (err) { | ||
return handleError ? status[500](res, next, { error: err }) : next(); | ||
fs.stat(dir, (statErr, stat) => { | ||
if (statErr) { | ||
if (handleError) { | ||
status[500](res, next, { error: statErr }); | ||
} else { | ||
next(); | ||
} | ||
return; | ||
} | ||
// files are the listing of dir | ||
fs.readdir(dir, function (err, files) { | ||
if (err) { | ||
return handleError ? status[500](res, next, { error: err }) : next(); | ||
fs.readdir(dir, (readErr, _files) => { | ||
let files = _files; | ||
if (readErr) { | ||
if (handleError) { | ||
status[500](res, next, { error: readErr }); | ||
} else { | ||
next(); | ||
} | ||
return; | ||
} | ||
@@ -52,5 +67,3 @@ | ||
if (!showDotfiles) { | ||
files = files.filter(function(filename){ | ||
return filename.slice(0,1) !== '.'; | ||
}); | ||
files = files.filter(filename => filename.slice(0, 1) !== '.'); | ||
} | ||
@@ -63,28 +76,6 @@ | ||
sortFiles(dir, files, function (lolwuts, dirs, files) { | ||
// It's possible to get stat errors for all sorts of reasons here. | ||
// Unfortunately, our two choices are to either bail completely, | ||
// or just truck along as though everything's cool. In this case, | ||
// I decided to just tack them on as "??!?" items along with dirs | ||
// and files. | ||
// | ||
// Whatever. | ||
// if it makes sense to, add a .. link | ||
if (path.resolve(dir, '..').slice(0, root.length) == root) { | ||
return fs.stat(path.join(dir, '..'), function (err, s) { | ||
if (err) { | ||
return handleError ? status[500](res, next, { error: err }) : next(); | ||
} | ||
dirs.unshift([ '..', s ]); | ||
render(dirs, files, lolwuts); | ||
}); | ||
} | ||
render(dirs, files, lolwuts); | ||
}); | ||
function render(dirs, files, lolwuts) { | ||
function render(dirs, renderFiles, lolwuts) { | ||
// each entry in the array is a [name, stat] tuple | ||
var html = [ | ||
let html = `${[ | ||
'<!doctype html>', | ||
@@ -95,47 +86,46 @@ '<html>', | ||
' <meta name="viewport" content="width=device-width">', | ||
' <title>Index of ' + he.encode(pathname) +'</title>', | ||
' <style type="text/css">' + css + '</style>', | ||
` <title>Index of ${he.encode(pathname)}</title>`, | ||
` <style type="text/css">${css}</style>`, | ||
' </head>', | ||
' <body>', | ||
'<h1>Index of ' + he.encode(pathname) + '</h1>' | ||
].join('\n') + '\n'; | ||
`<h1>Index of ${he.encode(pathname)}</h1>`, | ||
].join('\n')}\n`; | ||
html += '<table>'; | ||
var failed = false; | ||
var writeRow = function (file, i) { | ||
const failed = false; | ||
const writeRow = (file) => { | ||
// render a row given a [name, stat] tuple | ||
var isDir = file[1].isDirectory && file[1].isDirectory(); | ||
var href = parsed.pathname.replace(/\/$/, '') + '/' + encodeURIComponent(file[0]); | ||
const isDir = file[1].isDirectory && file[1].isDirectory(); | ||
let href = `${parsed.pathname.replace(/\/$/, '')}/${encodeURIComponent(file[0])}`; | ||
// append trailing slash and query for dir entry | ||
if (isDir) { | ||
href += '/' + he.encode((parsed.search)? parsed.search:''); | ||
href += `/${he.encode((parsed.search) ? parsed.search : '')}`; | ||
} | ||
var displayName = he.encode(file[0]) + ((isDir)? '/':''); | ||
const displayName = he.encode(file[0]) + ((isDir) ? '/' : ''); | ||
const ext = file[0].split('.').pop(); | ||
const classForNonDir = supportedIcons[ext] ? ext : '_page'; | ||
const iconClass = `icon-${isDir ? '_blank' : classForNonDir}`; | ||
var ext = file[0].split('.').pop(); | ||
var iconClass = 'icon-' + (isDir ? '_blank' : (supportedIcons[ext] ? ext : '_page')); | ||
// TODO: use stylessheets? | ||
html += '<tr>' + | ||
'<td class="icon-parent"><i class="' + iconClass + '"></i></td>' + | ||
'<td class="perms"><code>(' + permsToString(file[1]) + ')</code></td>' + | ||
'<td class="file-size"><code>' + sizeToString(file[1], humanReadable, si) + '</code></td>' + | ||
'<td class="display-name"><a href="' + href + '">' + displayName + '</a></td>' + | ||
html += `${'<tr>' + | ||
'<td><i class="icon '}${iconClass}"></i></td>` + | ||
`<td class="perms"><code>(${permsToString(file[1])})</code></td>` + | ||
`<td class="file-size"><code>${sizeToString(file[1], humanReadable, si)}</code></td>` + | ||
`<td class="display-name"><a href="${href}">${displayName}</a></td>` + | ||
'</tr>\n'; | ||
}; | ||
dirs.sort(function (a, b) { return a[0].toString().localeCompare(b[0].toString()); }).forEach(writeRow); | ||
files.sort(function (a, b) { return a.toString().localeCompare(b.toString()); }).forEach(writeRow); | ||
lolwuts.sort(function (a, b) { return a[0].toString().localeCompare(b[0].toString()); }).forEach(writeRow); | ||
dirs.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow); | ||
renderFiles.sort((a, b) => a.toString().localeCompare(b.toString())).forEach(writeRow); | ||
lolwuts.sort((a, b) => a[0].toString().localeCompare(b[0].toString())).forEach(writeRow); | ||
html += '</table>\n'; | ||
html += '<br><address>Node.js ' + | ||
process.version + | ||
'/ <a href="https://github.com/jfhbrook/node-ecstatic">ecstatic</a> ' + | ||
'server running @ ' + | ||
he.encode(req.headers.host || '') + '</address>\n' + | ||
html += `<br><address>Node.js ${ | ||
process.version | ||
}/ <a href="https://github.com/jfhbrook/node-ecstatic">ecstatic</a> ` + | ||
`server running @ ${ | ||
he.encode(req.headers.host || '')}</address>\n` + | ||
'</body></html>' | ||
@@ -145,6 +135,34 @@ ; | ||
if (!failed) { | ||
res.writeHead(200, { "Content-Type": "text/html" }); | ||
res.writeHead(200, { 'Content-Type': 'text/html' }); | ||
res.end(html); | ||
} | ||
} | ||
sortFiles(dir, files, (lolwuts, dirs, sortedFiles) => { | ||
// It's possible to get stat errors for all sorts of reasons here. | ||
// Unfortunately, our two choices are to either bail completely, | ||
// or just truck along as though everything's cool. In this case, | ||
// I decided to just tack them on as "??!?" items along with dirs | ||
// and files. | ||
// | ||
// Whatever. | ||
// if it makes sense to, add a .. link | ||
if (path.resolve(dir, '..').slice(0, root.length) === root) { | ||
fs.stat(path.join(dir, '..'), (err, s) => { | ||
if (err) { | ||
if (handleError) { | ||
status[500](res, next, { error: err }); | ||
} else { | ||
next(); | ||
} | ||
return; | ||
} | ||
dirs.unshift(['..', s]); | ||
render(dirs, sortedFiles, lolwuts); | ||
}); | ||
} else { | ||
render(dirs, sortedFiles, lolwuts); | ||
} | ||
}); | ||
}); | ||
@@ -154,31 +172,1 @@ }); | ||
}; | ||
// given a file's stat, return the size of it in string | ||
// humanReadable: (boolean) whether to result is human readable | ||
// si: (boolean) whether to use si (1k = 1000), otherwise 1k = 1024 | ||
// adopted from http://stackoverflow.com/a/14919494/665507 | ||
function sizeToString(stat, humanReadable, si) { | ||
if (stat.isDirectory && stat.isDirectory()) { | ||
return ''; | ||
} | ||
var sizeString = ''; | ||
var bytes = stat.size; | ||
var threshold = si ? 1000 : 1024; | ||
if (!humanReadable || bytes < threshold) { | ||
return bytes + 'B'; | ||
} | ||
var units = [ 'k','M','G','T','P','E','Z','Y' ]; | ||
var u = -1; | ||
do { | ||
bytes /= threshold; | ||
++u; | ||
} while (bytes >= threshold); | ||
var b = bytes.toFixed(1); | ||
if (isNaN(b)) b = '??'; | ||
return b + units[u]; | ||
} |
@@ -0,3 +1,4 @@ | ||
'use strict'; | ||
module.exports = function permsToString(stat) { | ||
if (!stat.isDirectory || !stat.mode) { | ||
@@ -7,18 +8,16 @@ return '???!!!???'; | ||
var dir = stat.isDirectory() ? 'd' : '-', | ||
mode = stat.mode.toString(8); | ||
const dir = stat.isDirectory() ? 'd' : '-'; | ||
const mode = stat.mode.toString(8); | ||
return dir + mode.slice(-3).split('').map(function (n) { | ||
return [ | ||
'---', | ||
'--x', | ||
'-w-', | ||
'-wx', | ||
'r--', | ||
'r-x', | ||
'rw-', | ||
'rwx' | ||
][parseInt(n, 10)]; | ||
}).join(''); | ||
return dir + mode.slice(-3).split('').map(n => [ | ||
'---', | ||
'--x', | ||
'-w-', | ||
'-wx', | ||
'r--', | ||
'r-x', | ||
'rw-', | ||
'rwx', | ||
][parseInt(n, 10)]).join(''); | ||
}; | ||
@@ -0,1 +1,3 @@ | ||
'use strict'; | ||
// given a file's stat, return the size of it in string | ||
@@ -10,18 +12,17 @@ // humanReadable: (boolean) whether to result is human readable | ||
var sizeString = ''; | ||
var bytes = stat.size; | ||
var threshold = si ? 1000 : 1024; | ||
let bytes = stat.size; | ||
const threshold = si ? 1000 : 1024; | ||
if (!humanReadable || bytes < threshold) { | ||
return bytes + 'B'; | ||
return `${bytes}B`; | ||
} | ||
var units = [ 'k','M','G','T','P','E','Z','Y' ]; | ||
var u = -1; | ||
const units = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; | ||
let u = -1; | ||
do { | ||
bytes /= threshold; | ||
++u; | ||
bytes /= threshold; | ||
u += 1; | ||
} while (bytes >= threshold); | ||
var b = bytes.toFixed(1); | ||
let b = bytes.toFixed(1); | ||
if (isNaN(b)) b = '??'; | ||
@@ -28,0 +29,0 @@ |
@@ -1,4 +0,6 @@ | ||
var fs = require('fs'), | ||
path = require('path'); | ||
'use strict'; | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
module.exports = function sortByIsDirectory(dir, paths, cb) { | ||
@@ -8,24 +10,24 @@ // take the listing file names in `dir` | ||
// of the array a [name, stat] tuple | ||
var pending = paths.length, | ||
errs = [], | ||
dirs = [], | ||
files = []; | ||
let pending = paths.length; | ||
const errs = []; | ||
const dirs = []; | ||
const files = []; | ||
if (!pending) { | ||
return cb(errs, dirs, files); | ||
cb(errs, dirs, files); | ||
return; | ||
} | ||
paths.forEach(function (file) { | ||
fs.stat(path.join(dir, file), function (err, s) { | ||
paths.forEach((file) => { | ||
fs.stat(path.join(dir, file), (err, s) => { | ||
if (err) { | ||
errs.push([file, err]); | ||
} | ||
else if (s.isDirectory()) { | ||
} else if (s.isDirectory()) { | ||
dirs.push([file, s]); | ||
} | ||
else { | ||
} else { | ||
files.push([file, s]); | ||
} | ||
if (--pending === 0) { | ||
pending -= 1; | ||
if (pending === 0) { | ||
cb(errs, dirs, files); | ||
@@ -32,0 +34,0 @@ } |
@@ -1,8 +0,8 @@ | ||
var fs = require('fs'), | ||
icons = require('./icons.json'), | ||
path = require('path'); | ||
'use strict'; | ||
var IMG_SIZE = 16; | ||
const icons = require('./icons.json'); | ||
var css = 'td.icon-parent { height: ' + IMG_SIZE + 'px; width: ' + IMG_SIZE + 'px; }\n'; | ||
const IMG_SIZE = 16; | ||
let css = `i.icon { display: block; height: ${IMG_SIZE}px; width: ${IMG_SIZE}px; }\n`; | ||
css += 'td.perms {}\n'; | ||
@@ -12,6 +12,5 @@ css += 'td.file-size { text-align: right; padding-left: 1em; }\n'; | ||
Object.keys(icons).forEach(function(key) { | ||
css += 'i.icon-' + key + ' {\n'; | ||
css += ' display: block; width: 100%; height: 100%; background-repeat: no-repeat;\n'; | ||
css += ' background: url("data:image/png;base64,' + icons[key] + '");\n'; | ||
Object.keys(icons).forEach((key) => { | ||
css += `i.icon-${key} {\n`; | ||
css += ` background: url("data:image/png;base64,${icons[key]}");\n`; | ||
css += '}\n\n'; | ||
@@ -18,0 +17,0 @@ }); |
@@ -1,5 +0,7 @@ | ||
var he = require('he'); | ||
'use strict'; | ||
const he = require('he'); | ||
// not modified | ||
exports['304'] = function (res, next) { | ||
exports['304'] = (res) => { | ||
res.statusCode = 304; | ||
@@ -10,22 +12,18 @@ res.end(); | ||
// access denied | ||
exports['403'] = function (res, next) { | ||
exports['403'] = (res, next) => { | ||
res.statusCode = 403; | ||
if (typeof next === "function") { | ||
if (typeof next === 'function') { | ||
next(); | ||
} else if (res.writable) { | ||
res.setHeader('content-type', 'text/plain'); | ||
res.end('ACCESS DENIED'); | ||
} | ||
else { | ||
if (res.writable) { | ||
res.setHeader('content-type', 'text/plain'); | ||
res.end('ACCESS DENIED'); | ||
} | ||
} | ||
}; | ||
// disallowed method | ||
exports['405'] = function (res, next, opts) { | ||
exports['405'] = (res, next, opts) => { | ||
res.statusCode = 405; | ||
if (typeof next === "function") { | ||
if (typeof next === 'function') { | ||
next(); | ||
} | ||
else { | ||
} else { | ||
res.setHeader('allow', (opts && opts.allow) || 'GET, HEAD'); | ||
@@ -37,34 +35,28 @@ res.end(); | ||
// not found | ||
exports['404'] = function (res, next) { | ||
exports['404'] = (res, next) => { | ||
res.statusCode = 404; | ||
if (typeof next === "function") { | ||
if (typeof next === 'function') { | ||
next(); | ||
} else if (res.writable) { | ||
res.setHeader('content-type', 'text/plain'); | ||
res.end('File not found. :('); | ||
} | ||
else { | ||
if (res.writable) { | ||
res.setHeader('content-type', 'text/plain'); | ||
res.end('File not found. :('); | ||
} | ||
} | ||
}; | ||
exports['416'] = function (res, next) { | ||
exports['416'] = (res, next) => { | ||
res.statusCode = 416; | ||
if (typeof next === "function") { | ||
if (typeof next === 'function') { | ||
next(); | ||
} else if (res.writable) { | ||
res.setHeader('content-type', 'text/plain'); | ||
res.end('Requested range not satisfiable'); | ||
} | ||
else { | ||
if (res.writable) { | ||
res.setHeader('content-type', 'text/plain'); | ||
res.end('Requested range not satisfiable'); | ||
} | ||
} | ||
}; | ||
// flagrant error | ||
exports['500'] = function (res, next, opts) { | ||
exports['500'] = (res, next, opts) => { | ||
res.statusCode = 500; | ||
res.setHeader('content-type', 'text/html'); | ||
var error = String(opts.error.stack || opts.error || "No specified error"), | ||
html = [ | ||
const error = String(opts.error.stack || opts.error || 'No specified error'); | ||
const html = `${[ | ||
'<!doctype html>', | ||
@@ -78,7 +70,7 @@ '<html>', | ||
' <p>', | ||
' ' + he.encode(error), | ||
` ${he.encode(error)}`, | ||
' </p>', | ||
' </body>', | ||
'</html>' | ||
].join('\n') + '\n'; | ||
'</html>', | ||
].join('\n')}\n`; | ||
res.end(html); | ||
@@ -88,7 +80,7 @@ }; | ||
// bad request | ||
exports['400'] = function (res, next, opts) { | ||
exports['400'] = (res, next, opts) => { | ||
res.statusCode = 400; | ||
res.setHeader('content-type', 'text/html'); | ||
var error = opts && opts.error ? String(opts.error) : 'Malformed request.', | ||
html = [ | ||
const error = opts && opts.error ? String(opts.error) : 'Malformed request.'; | ||
const html = `${[ | ||
'<!doctype html>', | ||
@@ -102,8 +94,8 @@ '<html>', | ||
' <p>', | ||
' ' + he.encode(error), | ||
` ${he.encode(error)}`, | ||
' </p>', | ||
' </body>', | ||
'</html>' | ||
].join('\n') + '\n'; | ||
'</html>', | ||
].join('\n')}\n`; | ||
res.end(html); | ||
}; |
@@ -5,3 +5,3 @@ { | ||
"description": "A simple static file server middleware that works with both Express and Flatiron", | ||
"version": "2.2.1", | ||
"version": "3.0.0", | ||
"homepage": "https://github.com/jfhbrook/node-ecstatic", | ||
@@ -14,2 +14,4 @@ "repository": { | ||
"scripts": { | ||
"fix": "eslint --fix ./lib/ ./example/ ./test", | ||
"pretest": "eslint ./lib/ ./example/ ./test", | ||
"test": "tap --coverage test/*.js", | ||
@@ -29,3 +31,3 @@ "posttest": "tap --coverage-report=text-lcov | codecov" | ||
"he": "^1.1.1", | ||
"mime": "^1.2.11", | ||
"mime": "^1.4.0", | ||
"minimist": "^1.1.0", | ||
@@ -37,2 +39,5 @@ "url-join": "^2.0.2" | ||
"eol": "^0.9.0", | ||
"eslint": "^3.19.0", | ||
"eslint-config-airbnb-base": "^11.2.0", | ||
"eslint-plugin-import": "^2.3.0", | ||
"express": "^4.12.3", | ||
@@ -39,0 +44,0 @@ "mkdirp": "^0.5.0", |
104
README.md
@@ -13,8 +13,15 @@ # Ecstatic [![build status](https://secure.travis-ci.org/jfhbrook/node-ecstatic.png)](http://travis-ci.org/jfhbrook/node-ecstatic) [![codecov.io](https://codecov.io/github/jfhbrook/node-ecstatic/coverage.svg?branch=master)](https://codecov.io/github/jfhbrook/node-ecstatic?branch=master) | ||
``` js | ||
var http = require('http'); | ||
var express = require('express'); | ||
var ecstatic = require('ecstatic'); | ||
'use strict'; | ||
var app = express(); | ||
app.use(ecstatic({ root: __dirname + '/public' })); | ||
const express = require('express'); | ||
const ecstatic = require('../lib/ecstatic'); | ||
const http = require('http'); | ||
const app = express(); | ||
app.use(ecstatic({ | ||
root: `${__dirname}/public`, | ||
showdir: true, | ||
})); | ||
http.createServer(app).listen(8080); | ||
@@ -28,9 +35,14 @@ | ||
``` js | ||
var http = require('http'); | ||
var ecstatic = require('ecstatic'); | ||
'use strict'; | ||
http.createServer( | ||
ecstatic({ root: __dirname + '/public' }) | ||
).listen(8080); | ||
const http = require('http'); | ||
const ecstatic = require('../lib/ecstatic')({ | ||
root: `${__dirname}/public`, | ||
showDir: true, | ||
autoIndex: true, | ||
}); | ||
http.createServer(ecstatic).listen(8080); | ||
console.log('Listening on :8080'); | ||
@@ -78,20 +90,21 @@ ``` | ||
```js | ||
var opts = { | ||
root : __dirname + '/public', | ||
port : 8000, | ||
baseDir : '/', | ||
cache : 3600, | ||
showDir : true, | ||
showDotfiles : true, | ||
autoIndex : false, | ||
humanReadable : true, | ||
headers : {}, | ||
si : false, | ||
defaultExt : 'html', | ||
gzip : false, | ||
serverHeader : true, | ||
contentType : 'application/octet-stream', | ||
mimeTypes : undefined, | ||
handleOptionsMethod: false | ||
} | ||
const opts = { | ||
root: path.join(__dirname, 'public'), | ||
baseDir: '/', | ||
autoIndex: true, | ||
showDir: true, | ||
showDotfiles: true, | ||
humanReadable: true, | ||
si: false, | ||
cache: 'max-age=3600', | ||
cors: false, | ||
gzip: true, | ||
defaultExt: 'html', | ||
handleError: true, | ||
serverHeader: true, | ||
contentType: 'application/octet-stream', | ||
weakEtags: true, | ||
weakCompare: true, | ||
handleOptionsMethod: false, | ||
} | ||
``` | ||
@@ -188,7 +201,9 @@ | ||
### `opts.gzip` | ||
### `--gzip` | ||
### `--no-gzip` | ||
Set `opts.gzip === true` in order to turn on "gzip mode," wherein ecstatic will | ||
serve `./public/some-file.js.gz` in place of `./public/some-file.js` when the | ||
gzipped version exists and ecstatic determines that the behavior is appropriate. | ||
By default, ecstatic will serve `./public/some-file.js.gz` in place of | ||
`./public/some-file.js` when the gzipped version exists and ecstatic determines | ||
that the behavior is appropriate. If `./public/some-file.js.gz` is not valid | ||
gzip, this will fall back to `./public/some-file.js`. You can turn this off | ||
with `opts.gzip === false`. | ||
@@ -198,4 +213,4 @@ ### `opts.serverHeader` | ||
Set `opts.serverHeader` to false in order to turn off setting the `Server` header | ||
on all responses served by ecstatic. | ||
Set `opts.serverHeader` to false in order to turn off setting the `Server` | ||
header on all responses served by ecstatic. | ||
@@ -211,4 +226,6 @@ ### `opts.contentType` | ||
Add new or override one or more mime-types. This affects the HTTP Content-Type header. | ||
Can either be a path to a [`.types`](http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types) file or an object hash of type(s). | ||
Add new or override one or more mime-types. This affects the HTTP Content-Type | ||
header. Can either be a path to a | ||
[`.types`](http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types) | ||
file or an object hash of type(s). | ||
@@ -219,14 +236,17 @@ ecstatic({ mimeType: { 'mime-type': ['file_extension', 'file_extension'] } }) | ||
Turn **off** handleErrors to allow fall-through with `opts.handleError === false`, Defaults to **true**. | ||
Turn **off** handleErrors to allow fall-through with | ||
`opts.handleError === false`, Defaults to **true**. | ||
### `opts.weakEtags` | ||
### `--weak-etags` | ||
### `--no-weak-etags` | ||
Set `opts.weakEtags` to true in order to generate weak etags instead of strong etags. Defaults to **false**. See `opts.weakCompare` as well. | ||
Set `opts.weakEtags` to false in order to generate strong etags instead of | ||
weak etags. Defaults to **true**. See `opts.weakCompare` as well. | ||
### `opts.weakCompare` | ||
### `--weak-compare` | ||
### `--no-weak-compare` | ||
Turn **on** weakCompare to allow the weak comparison function for etag validation. Defaults to **false**. | ||
See https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3 for more details. | ||
Turn off weakCompare to disable the weak comparison function for etag | ||
validation. Defaults to **true**. See | ||
https://www.ietf.org/rfc/rfc2616.txt Section 13.3.3 for more details. | ||
@@ -233,0 +253,0 @@ ### `opts.handleOptionsMethod` |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3727589
85
1683
283
5
9
Updatedmime@^1.4.0