Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(more) small performance/clarity improvements #405

Merged
merged 21 commits into from
Sep 7, 2023
Merged
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 48 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
'use strict'

const path = require('path')
const url = require('url')
const statSync = require('fs').statSync
const { PassThrough } = require('readable-stream')
const { fileURLToPath } = require('url')
const { statSync } = require('fs')
const { promisify } = require('util')
const glob = require('glob')
const globPromise = promisify(glob)
const { PassThrough } = require('readable-stream')
const send = require('@fastify/send')
const encodingNegotiator = require('@fastify/accept-negotiator')
const contentDisposition = require('content-disposition')
const fp = require('fastify-plugin')
const util = require('util')
const globPromise = util.promisify(glob)
const encodingNegotiator = require('@fastify/accept-negotiator')

send.mime.default_type = 'application/octet-stream'

const dirList = require('./lib/dirList')

const supportedEncodings = ['br', 'gzip', 'deflate']
send.mime.default_type = 'application/octet-stream'

async function fastifyStatic (fastify, opts) {
opts.root = normalizeRoot(opts.root)
checkRootPathForErrors(fastify, opts.root)

const setHeaders = opts.setHeaders

if (setHeaders !== undefined && typeof setHeaders !== 'function') {
throw new TypeError('The `setHeaders` option must be a function')
}
Expand All @@ -48,19 +48,19 @@ async function fastifyStatic (fastify, opts) {
maxAge: opts.maxAge
}

const allowedPath = opts.allowedPath

if (opts.prefix === undefined) opts.prefix = '/'

let prefix = opts.prefix
if (prefix === undefined) {
prefix = (opts.prefix = '/')
}
gurgunday marked this conversation as resolved.
Show resolved Hide resolved

if (!opts.prefixAvoidTrailingSlash) {
prefix =
opts.prefix[opts.prefix.length - 1] === '/'
? opts.prefix
: opts.prefix + '/'
prefix[prefix.length - 1] === '/'
? prefix
: prefix + '/'
}

const allowedPath = opts.allowedPath
function pumpSendToReply (
gurgunday marked this conversation as resolved.
Show resolved Hide resolved
request,
reply,
Expand Down Expand Up @@ -271,22 +271,20 @@ async function fastifyStatic (fastify, opts) {
stream.pipe(wrap)
}

const errorHandler = (error, request, reply) => {
if (error?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
reply.request.raw.destroy()
return
}

fastify.errorHandler(error, request, reply)
}

// Set the schema hide property if defined in opts or true by default
const routeOpts = {
constraints: opts.constraints,
schema: {
hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true
hide: opts.schemaHide !== undefined ? opts.schemaHide : true
},
errorHandler
errorHandler (error, request, reply) {
if (error?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
reply.request.raw.destroy()
return
}

fastify.errorHandler(error, request, reply)
}
}

if (opts.decorateReply !== false) {
Expand Down Expand Up @@ -341,21 +339,27 @@ async function fastifyStatic (fastify, opts) {
const globPattern = '**/**'
const indexDirs = new Map()
const routes = new Set()

const winSeparatorRegex = new RegExp(`\\${path.win32.sep}`, 'g')

for (const rootPath of Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]) {
const backslashRegex = /\\/g
const startForwardSlashRegex = /^\//
const endForwardSlashRegex = /\/$/
const doubleForwardSlashRegex = /\/\//g
gurgunday marked this conversation as resolved.
Show resolved Hide resolved

const roots = Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]
for (let i = 0; i < roots.length; ++i) {
const rootPath = roots[i]
const files = await globPromise(path.join(rootPath, globPattern).replace(winSeparatorRegex, path.posix.sep), { nodir: true, dot: opts.serveDotFiles })
const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index)
const indexes = opts.index === undefined ? ['index.html'] : [].concat(opts.index)

for (let i = 0; i < files.length; ++i) {
const file = files[i].replace(rootPath.replace(backslashRegex, '/'), '')
.replace(startForwardSlashRegex, '')
const route = (prefix + file).replace(doubleForwardSlashRegex, '/')

for (let file of files) {
file = file
.replace(rootPath.replace(/\\/g, '/'), '')
.replace(/^\//, '')
const route = (prefix + file).replace(/\/\//g, '/')
if (routes.has(route)) {
continue
}

routes.add(route)

setUpHeadAndGet(routeOpts, route, '/' + file, rootPath)
Expand All @@ -373,7 +377,7 @@ async function fastifyStatic (fastify, opts) {
setUpHeadAndGet(routeOpts, pathname, file, rootPath)

if (opts.redirect === true) {
setUpHeadAndGet(routeOpts, pathname.replace(/\/$/, ''), file.replace(/\/$/, ''), rootPath)
setUpHeadAndGet(routeOpts, pathname.replace(endForwardSlashRegex, ''), file.replace(endForwardSlashRegex, ''), rootPath)
}
}
}
Expand Down Expand Up @@ -404,13 +408,13 @@ function normalizeRoot (root) {
return root
}
if (root instanceof URL && root.protocol === 'file:') {
return url.fileURLToPath(root)
return fileURLToPath(root)
}
if (Array.isArray(root)) {
const result = []
for (let i = 0, il = root.length; i < il; ++i) {
if (root[i] instanceof URL && root[i].protocol === 'file:') {
result.push(url.fileURLToPath(root[i]))
result.push(fileURLToPath(root[i]))
} else {
result.push(root[i])
}
Expand Down Expand Up @@ -503,13 +507,12 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
return false
}

const supportedEncodings = ['br', 'gzip', 'deflate']

// Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
function getEncodingHeader (headers, checked) {
if (!('accept-encoding' in headers)) return

const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip')
const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip') // consider the no-preference token as gzip for downstream compat

gurgunday marked this conversation as resolved.
Show resolved Hide resolved
return encodingNegotiator.negotiate(
header,
supportedEncodings.filter((enc) => !checked.has(enc))
Expand All @@ -529,14 +532,15 @@ function getEncodingExtension (encoding) {
function getRedirectUrl (url) {
let i = 0
// we detect how many slash before a valid path
for (i; i < url.length; i++) {
for (; i < url.length; ++i) {
if (url[i] !== '/' && url[i] !== '\\') break
}
// turns all leading / or \ into a single /
url = '/' + url.substr(i)
try {
const parsed = new URL(url, 'http://localhost.com/')
return parsed.pathname + (parsed.pathname[parsed.pathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
const parsedPathname = parsed.pathname
return parsedPathname + (parsedPathname[parsedPathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
} catch (error) {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
/* istanbul ignore next */
Expand Down