fastify-static
Advanced tools
Comparing version 4.0.1 to 4.2.0
@@ -61,2 +61,7 @@ // Definitions by: Jannik <https://github.com/jannikkeye> | ||
allowedPath?: (pathName: string, root?: string) => boolean; | ||
/** | ||
* @description | ||
* Opt-in to looking for pre-compressed files | ||
*/ | ||
preCompressed?: boolean; | ||
@@ -63,0 +68,0 @@ // Passed on to `send` |
225
index.js
@@ -13,2 +13,3 @@ 'use strict' | ||
const globPromise = util.promisify(glob) | ||
const encodingNegotiator = require('encoding-negotiator') | ||
@@ -46,3 +47,11 @@ const dirList = require('./lib/dirList') | ||
function pumpSendToReply (request, reply, pathname, rootPath, rootPathOffset = 0, pumpOptions = {}) { | ||
function pumpSendToReply ( | ||
request, | ||
reply, | ||
pathname, | ||
rootPath, | ||
rootPathOffset = 0, | ||
pumpOptions = {}, | ||
checkedExtensions | ||
) { | ||
const options = Object.assign({}, sendOptions, pumpOptions) | ||
@@ -62,3 +71,22 @@ | ||
const stream = send(request.raw, pathname, options) | ||
let encodingExt | ||
let pathnameForSend = pathname | ||
if (opts.preCompressed) { | ||
/** | ||
* We conditionally create this structure to track our attempts | ||
* at sending pre-compressed assets | ||
*/ | ||
if (!checkedExtensions) { | ||
checkedExtensions = new Set() | ||
} | ||
encodingExt = checkEncodingHeaders(request.headers, checkedExtensions) | ||
if (encodingExt) { | ||
pathnameForSend = pathname + '.' + encodingExt | ||
} | ||
} | ||
const stream = send(request.raw, pathnameForSend, options) | ||
let resolvedFilename | ||
@@ -97,5 +125,12 @@ stream.on('file', function (file) { | ||
wrap.on('pipe', function () { | ||
reply.send(wrap) | ||
}) | ||
if (request.method === 'HEAD') { | ||
wrap.on('finish', reply.send.bind(reply)) | ||
} else { | ||
wrap.on('pipe', function () { | ||
if (encodingExt) { | ||
reply.header('content-encoding', encodingExt) | ||
} | ||
reply.send(wrap) | ||
}) | ||
} | ||
@@ -108,3 +143,8 @@ if (setHeaders !== undefined) { | ||
if (opts.list) { | ||
return dirList.send({ reply, dir: path, options: opts.list, route: pathname }) | ||
return dirList.send({ | ||
reply, | ||
dir: path, | ||
options: opts.list, | ||
route: pathname | ||
}) | ||
} | ||
@@ -122,18 +162,29 @@ | ||
stream.on('error', function (err) { | ||
if (err) { | ||
if (err.code === 'ENOENT') { | ||
// if file exists, send real file, otherwise send dir list if name match | ||
if (opts.list && dirList.handle(pathname, opts.list)) { | ||
return dirList.send({ reply, dir: dirList.path(opts.root, pathname), options: opts.list, route: pathname }) | ||
} | ||
if (err.code === 'ENOENT') { | ||
// if file exists, send real file, otherwise send dir list if name match | ||
if (opts.list && dirList.handle(pathname, opts.list)) { | ||
return dirList.send({ reply, dir: dirList.path(opts.root, pathname), options: opts.list, route: pathname }) | ||
} | ||
// root paths left to try? | ||
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) { | ||
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1) | ||
} | ||
// root paths left to try? | ||
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) { | ||
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1) | ||
} | ||
return reply.callNotFound() | ||
if (opts.preCompressed && !checkedExtensions.has(encodingExt)) { | ||
checkedExtensions.add(encodingExt) | ||
return pumpSendToReply( | ||
request, | ||
reply, | ||
pathname, | ||
rootPath, | ||
undefined, | ||
undefined, | ||
checkedExtensions | ||
) | ||
} | ||
reply.send(err) | ||
return reply.callNotFound() | ||
} | ||
reply.send(err) | ||
}) | ||
@@ -151,3 +202,6 @@ | ||
if (!opts.prefixAvoidTrailingSlash) { | ||
prefix = opts.prefix[opts.prefix.length - 1] === '/' ? opts.prefix : (opts.prefix + '/') | ||
prefix = | ||
opts.prefix[opts.prefix.length - 1] === '/' | ||
? opts.prefix | ||
: opts.prefix + '/' | ||
} | ||
@@ -166,3 +220,5 @@ | ||
const routeOpts = { | ||
schema: { hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true }, | ||
schema: { | ||
hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true | ||
}, | ||
errorHandler: fastify.errorHandler ? errorHandler : undefined | ||
@@ -173,22 +229,36 @@ } | ||
fastify.decorateReply('sendFile', function (filePath, rootPath) { | ||
pumpSendToReply(this.request, this, filePath, rootPath) | ||
pumpSendToReply( | ||
this.request, | ||
this, | ||
filePath, | ||
rootPath || sendOptions.root | ||
) | ||
return this | ||
}) | ||
fastify.decorateReply('download', function (filePath, fileName, options = {}) { | ||
const { root, ...opts } = typeof fileName === 'object' ? fileName : options | ||
fileName = typeof fileName === 'string' ? fileName : filePath | ||
fastify.decorateReply( | ||
'download', | ||
function (filePath, fileName, options = {}) { | ||
const { root, ...opts } = | ||
typeof fileName === 'object' ? fileName : options | ||
fileName = typeof fileName === 'string' ? fileName : filePath | ||
// Set content disposition header | ||
this.header('content-disposition', contentDisposition(fileName)) | ||
// Set content disposition header | ||
this.header('content-disposition', contentDisposition(fileName)) | ||
pumpSendToReply(this.request, this, filePath, root, 0, opts) | ||
pumpSendToReply(this.request, this, filePath, root, 0, opts) | ||
return this | ||
}) | ||
return this | ||
} | ||
) | ||
} | ||
if (opts.serve !== false) { | ||
if (opts.wildcard && typeof opts.wildcard !== 'boolean') throw new Error('"wildcard" option must be a boolean') | ||
if (opts.wildcard && typeof opts.wildcard !== 'boolean') { | ||
throw new Error('"wildcard" option must be a boolean') | ||
} | ||
if (opts.wildcard === undefined || opts.wildcard === true) { | ||
fastify.head(prefix + '*', routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root) | ||
}) | ||
fastify.get(prefix + '*', routeOpts, function (req, reply) { | ||
@@ -206,11 +276,22 @@ pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root) | ||
const globPattern = '**/*' | ||
const indexDirs = new Map() | ||
const routes = new Set() | ||
async function addGlobRoutes (rootPath) { | ||
for (const rootPath of Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]) { | ||
const files = await globPromise(path.join(rootPath, globPattern), { nodir: true }) | ||
const indexDirs = new Set() | ||
const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index || []) | ||
const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index) | ||
for (let file of files) { | ||
file = file.replace(rootPath.replace(/\\/g, '/'), '').replace(/^\//, '') | ||
file = file | ||
.replace(rootPath.replace(/\\/g, '/'), '') | ||
.replace(/^\//, '') | ||
const route = encodeURI(prefix + file).replace(/\/\//g, '/') | ||
if (routes.has(route)) { | ||
continue | ||
} | ||
routes.add(route) | ||
fastify.head(route, routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, '/' + file, rootPath) | ||
}) | ||
fastify.get(route, routeOpts, function (req, reply) { | ||
@@ -220,27 +301,29 @@ pumpSendToReply(req, reply, '/' + file, rootPath) | ||
if (indexes.includes(path.posix.basename(route))) { | ||
indexDirs.add(path.posix.dirname(route)) | ||
const key = path.posix.basename(route) | ||
if (indexes.includes(key) && !indexDirs.has(key)) { | ||
indexDirs.set(path.posix.dirname(route), rootPath) | ||
} | ||
} | ||
} | ||
indexDirs.forEach(function (dirname) { | ||
const pathname = dirname + (dirname.endsWith('/') ? '' : '/') | ||
const file = '/' + pathname.replace(prefix, '') | ||
for (const [dirname, rootPath] of indexDirs.entries()) { | ||
const pathname = dirname + (dirname.endsWith('/') ? '' : '/') | ||
const file = '/' + pathname.replace(prefix, '') | ||
fastify.get(pathname, routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, file, rootPath) | ||
}) | ||
fastify.head(pathname, routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, file, rootPath) | ||
}) | ||
if (opts.redirect === true) { | ||
fastify.get(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath) | ||
}) | ||
} | ||
fastify.get(pathname, routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, file, rootPath) | ||
}) | ||
} | ||
if (Array.isArray(sendOptions.root)) { | ||
await Promise.all(sendOptions.root.map(addGlobRoutes)) | ||
} else { | ||
await addGlobRoutes(sendOptions.root) | ||
if (opts.redirect === true) { | ||
fastify.head(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath) | ||
}) | ||
fastify.get(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) { | ||
pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath) | ||
}) | ||
} | ||
} | ||
@@ -257,10 +340,14 @@ } | ||
if (Array.isArray(rootPath)) { | ||
if (!rootPath.length) { throw new Error('"root" option array requires one or more paths') } | ||
if (!rootPath.length) { | ||
throw new Error('"root" option array requires one or more paths') | ||
} | ||
if ([...new Set(rootPath)].length !== rootPath.length) { | ||
throw new Error('"root" option array contains one or more duplicate paths') | ||
throw new Error( | ||
'"root" option array contains one or more duplicate paths' | ||
) | ||
} | ||
// check each path and fail at first invalid | ||
rootPath.map(path => checkPath(fastify, path)) | ||
rootPath.map((path) => checkPath(fastify, path)) | ||
return | ||
@@ -302,2 +389,30 @@ } | ||
const supportedEncodings = ['br', 'gzip', 'deflate'] | ||
// Adapted from https://github.com/fastify/fastify-compress/blob/fa5c12a5394285c86d9f438cb39ff44f3d5cde79/index.js#L442 | ||
function checkEncodingHeaders (headers, checked) { | ||
if (!('accept-encoding' in headers)) return | ||
let ext | ||
const header = headers['accept-encoding'].toLowerCase().replace('*', 'gzip') | ||
const accepted = encodingNegotiator.negotiate( | ||
header, | ||
supportedEncodings.filter((enc) => !checked.has(enc)) | ||
) | ||
switch (accepted) { | ||
case 'br': | ||
ext = 'br' | ||
break | ||
case 'gzip': | ||
if (!checked.has('gz')) { | ||
ext = 'gz' | ||
break | ||
} | ||
} | ||
return ext | ||
} | ||
module.exports = fp(fastifyStatic, { | ||
@@ -304,0 +419,0 @@ fastify: '3.x', |
@@ -78,3 +78,3 @@ 'use strict' | ||
htmlInfo: function (entry, route) { | ||
return { href: path.join(path.dirname(route), entry), name: entry } | ||
return { href: path.join(path.dirname(route), entry).replace(/\\/g, '/'), name: entry } | ||
}, | ||
@@ -81,0 +81,0 @@ |
{ | ||
"name": "fastify-static", | ||
"version": "4.0.1", | ||
"version": "4.2.0", | ||
"description": "Plugin for serving static files as fast as possible.", | ||
@@ -33,2 +33,3 @@ "main": "index.js", | ||
"content-disposition": "^0.5.3", | ||
"encoding-negotiator": "^2.0.1", | ||
"fastify-plugin": "^3.0.0", | ||
@@ -40,3 +41,3 @@ "glob": "^7.1.4", | ||
"devDependencies": { | ||
"@types/node": "^14.11.2", | ||
"@types/node": "^15.0.0", | ||
"@typescript-eslint/eslint-plugin": "^2.29.0", | ||
@@ -55,4 +56,4 @@ "@typescript-eslint/parser": "^2.29.0", | ||
"standard": "^16.0.2", | ||
"tap": "^14.10.8", | ||
"tsd": "^0.14.0", | ||
"tap": "^15.0.0", | ||
"tsd": "^0.15.0", | ||
"typescript": "^4.0.2" | ||
@@ -59,0 +60,0 @@ }, |
# fastify-static | ||
![CI workflow](https://github.com/fastify/fastify-static/workflows/CI%20workflow/badge.svg) [![Known Vulnerabilities](https://snyk.io/test/github/fastify/fastify-static/badge.svg)](https://snyk.io/test/github/fastify/fastify-static) | ||
![CI](https://github.com/fastify/fastify-static/workflows/CI/badge.svg) | ||
[![NPM version](https://img.shields.io/npm/v/fastify-static.svg?style=flat)](https://www.npmjs.com/package/fastify-static) | ||
[![Known Vulnerabilities](https://snyk.io/test/github/fastify/fastify-static/badge.svg)](https://snyk.io/test/github/fastify/fastify-static) | ||
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) | ||
Plugin for serving static files as fast as possible. Supports Fastify version `3.x`. | ||
@@ -146,3 +150,3 @@ | ||
If this option is set to `false`, then requesting directories without trailing | ||
If this option is set to `false`, then requesting directories without trailing | ||
slash will trigger your app's 404 handler using `reply.callNotFound()`. | ||
@@ -289,5 +293,23 @@ | ||
#### `preCompressed` | ||
Default: `false` | ||
Try to send the brotli encoded asset first (when supported within the `Accept-Encoding` headers), retry for gzip, then the fall back to the original `pathname`. You may choose to skip compression for smaller files that don't benefit from it. | ||
Assume this structure with the compressed asset as a sibling of the un-compressed counterpart: | ||
``` | ||
./public | ||
├── main.js | ||
├── main.js.br | ||
├── main.js.gz | ||
├── crit.css | ||
├── crit.css.gz | ||
└── index.html | ||
``` | ||
#### Disable serving | ||
If you'd just like to use the reply decorator and not serve whole directories automatically, you can simply pass the option `{ serve: false }`. This will prevent the plugin from serving everything under `root`. | ||
If you would just like to use the reply decorator and not serve whole directories automatically, you can simply pass the option `{ serve: false }`. This will prevent the plugin from serving everything under `root`. | ||
@@ -294,0 +316,0 @@ #### Disabling reply decorator |
@@ -17,3 +17,3 @@ 'use strict' | ||
fastify.register(fastifyStatic, options) | ||
t.tearDown(fastify.close.bind(fastify)) | ||
t.teardown(fastify.close.bind(fastify)) | ||
fastify.listen(0, err => { | ||
@@ -96,4 +96,4 @@ t.error(err) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), JSON.stringify(content)) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), JSON.stringify(content)) | ||
}) | ||
@@ -125,4 +125,4 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), JSON.stringify(content)) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), JSON.stringify(content)) | ||
}) | ||
@@ -234,4 +234,4 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), template.output) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), template.output) | ||
}) | ||
@@ -269,4 +269,4 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), JSON.stringify(content)) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), JSON.stringify(content)) | ||
}) | ||
@@ -297,4 +297,4 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), JSON.stringify(content)) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), JSON.stringify(content)) | ||
}) | ||
@@ -330,4 +330,4 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), '<html>\n <body>\n the body\n </body>\n</html>\n') | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), '<html>\n <body>\n the body\n </body>\n</html>\n') | ||
}) | ||
@@ -341,4 +341,4 @@ | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 200) | ||
t.strictEqual(body.toString(), 'dir list index') | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), 'dir list index') | ||
}) | ||
@@ -367,3 +367,3 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 404) | ||
t.equal(response.statusCode, 404) | ||
}) | ||
@@ -394,3 +394,3 @@ }) | ||
t.error(err) | ||
t.strictEqual(response.statusCode, 404) | ||
t.equal(response.statusCode, 404) | ||
}) | ||
@@ -397,0 +397,0 @@ }) |
@@ -27,2 +27,3 @@ import fastify from 'fastify' | ||
}, | ||
preCompressed: false | ||
} | ||
@@ -29,0 +30,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
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
233801
40
3747
347
6
+ Addedencoding-negotiator@^2.0.1
+ Addedencoding-negotiator@2.0.1(transitive)