Comparing version
@@ -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 [](http://travis-ci.org/jfhbrook/node-ecstatic) [](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
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3727589
0.46%85
1.19%1683
2.5%283
7.6%6
-25%9
50%Updated