Skip to content

Commit

Permalink
Merge next into master (#454)
Browse files Browse the repository at this point in the history
* feat: preparation for fastify@5 (#444)

* Update for v5

* c8 ignore

* remove dep

* c8

---------

Co-authored-by: Gürgün Dayıoğlu <[email protected]>

* update fastify deps

* chore!: bump dependency send@3 (#455)

* chore!: bump dependency send@3

* fixup

* fixup

* chore: use v5 workflow

* chore: bump dependency [email protected]

* fixup

* update plugin config

* fix deprecation warning

---------

Co-authored-by: Mohammed Bilal Shareef <[email protected]>
Co-authored-by: Gürgün Dayıoğlu <[email protected]>
Co-authored-by: KaKa <[email protected]>
  • Loading branch information
4 people authored Jul 12, 2024
1 parent c569cf3 commit 1e041d7
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 182 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:

jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5.0.0
with:
license-check: true
lint: true
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,13 +477,13 @@ If an error occurs while trying to send a file, the error will be passed
to Fastify's error handler. You can set a custom error handler with
[`fastify.setErrorHandler()`](https://fastify.dev/docs/latest/Reference/Server/#seterrorhandler).

### Payload `stream.filename`
### Payload `stream.path`

If you need to access the filename inside the `onSend` hook, you can use `payload.filename`.
If you need to access the file path inside the `onSend` hook, you can use `payload.path`.

```js
fastify.addHook('onSend', function (req, reply, payload, next) {
console.log(payload.filename)
console.log(payload.path)
next()
})
```
Expand Down
276 changes: 127 additions & 149 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const { PassThrough } = require('node:stream')
const path = require('node:path')
const { fileURLToPath } = require('node:url')
const { statSync } = require('node:fs')
Expand Down Expand Up @@ -121,7 +120,7 @@ async function fastifyStatic (fastify, opts) {
})
if (opts.redirect === true && prefix !== opts.prefix) {
fastify.get(opts.prefix, routeOpts, (req, reply) => {
reply.redirect(301, getRedirectUrl(req.raw.url))
reply.redirect(getRedirectUrl(req.raw.url), 301)
})
}
} else {
Expand Down Expand Up @@ -170,7 +169,7 @@ async function fastifyStatic (fastify, opts) {

const allowedPath = opts.allowedPath

function pumpSendToReply (
async function pumpSendToReply (
request,
reply,
pathname,
Expand Down Expand Up @@ -222,164 +221,148 @@ async function fastifyStatic (fastify, opts) {
}

// `send(..., path, ...)` will URI-decode path so we pass an encoded path here
const stream = send(request.raw, encodeURI(pathnameForSend), options)
let resolvedFilename
stream.on('file', function (file) {
resolvedFilename = file
})

const wrap = new PassThrough({
flush (cb) {
this.finished = true
if (reply.raw.statusCode === 304) {
reply.send('')
const {
statusCode,
headers,
stream,
type,
metadata
} = await send(request.raw, encodeURI(pathnameForSend), options)
switch (type) {
case 'directory': {
const path = metadata.path
if (opts.list) {
await dirList.send({
reply,
dir: path,
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
}
cb()
}
})

wrap.getHeader = reply.getHeader.bind(reply)
wrap.setHeader = reply.header.bind(reply)
wrap.removeHeader = () => {}
wrap.finished = false
if (opts.redirect === true) {
try {
reply.redirect(getRedirectUrl(request.raw.url), 301)
} /* c8 ignore start */ catch (error) {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
await reply.send(error)
} /* c8 ignore stop */
} else {
// if is a directory path without a trailing slash, and has an index file, reply as if it has a trailing slash
if (!pathname.endsWith('/') && findIndexFile(pathname, options.root, options.index)) {
return pumpSendToReply(
request,
reply,
pathname + '/',
rootPath,
undefined,
undefined,
checkedEncodings
)
}

Object.defineProperty(wrap, 'filename', {
get () {
return resolvedFilename
}
})
Object.defineProperty(wrap, 'statusCode', {
get () {
return reply.raw.statusCode
},
set (code) {
reply.code(code)
reply.callNotFound()
}
break
}
})

if (request.method === 'HEAD') {
wrap.on('finish', reply.send.bind(reply))
} else {
wrap.on('pipe', function () {
if (encoding) {
reply.header('content-type', getContentType(pathname))
reply.header('content-encoding', encoding)
case 'error': {
if (
statusCode === 403 &&
(!options.index || !options.index.length) &&
pathnameForSend[pathnameForSend.length - 1] === '/'
) {
if (opts.list) {
await dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
}
}
reply.send(wrap)
})
}

if (setHeaders !== undefined) {
stream.on('headers', setHeaders)
}

stream.on('directory', function (_, path) {
if (opts.list) {
dirList.send({
reply,
dir: path,
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
}
if (metadata.error.code === 'ENOENT') {
// when preCompress is enabled and the path is a directory without a trailing slash
if (opts.preCompressed && encoding) {
const indexPathname = findIndexFile(pathname, options.root, options.index)
if (indexPathname) {
return pumpSendToReply(
request,
reply,
pathname + '/',
rootPath,
undefined,
undefined,
checkedEncodings
)
}
}

if (opts.redirect === true) {
try {
reply.redirect(301, getRedirectUrl(request.raw.url))
} catch (error) {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
/* istanbul ignore next */
reply.send(error)
}
} else {
// if is a directory path without a trailing slash, and has an index file, reply as if it has a trailing slash
if (!pathname.endsWith('/') && findIndexFile(pathname, options.root, options.index)) {
return pumpSendToReply(
request,
reply,
pathname + '/',
rootPath,
undefined,
undefined,
checkedEncodings
)
}
// if file exists, send real file, otherwise send dir list if name match
if (opts.list && dirList.handle(pathname, opts.list)) {
await dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
}

reply.callNotFound()
}
})
// root paths left to try?
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
}

stream.on('error', function (err) {
if (err.code === 'ENOENT') {
// when preCompress is enabled and the path is a directory without a trailing slash
if (opts.preCompressed && encoding) {
const indexPathname = findIndexFile(pathname, options.root, options.index)
if (indexPathname) {
if (opts.preCompressed && !checkedEncodings.has(encoding)) {
checkedEncodings.add(encoding)
return pumpSendToReply(
request,
reply,
pathname + '/',
pathnameOrig,
rootPath,
undefined,
rootPathOffset,
undefined,
checkedEncodings
)
}
}

// if file exists, send real file, otherwise send dir list if name match
if (opts.list && dirList.handle(pathname, opts.list)) {
dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
return reply.callNotFound()
}

// root paths left to try?
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
// The `send` library terminates the request with a 404 if the requested
// path contains a dotfile and `send` is initialized with `{dotfiles:
// 'ignore'}`. `send` aborts the request before getting far enough to
// check if the file exists (hence, a 404 `NotFoundError` instead of
// `ENOENT`).
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L582
if (metadata.error.status === 404) {
return reply.callNotFound()
}

if (opts.preCompressed && !checkedEncodings.has(encoding)) {
checkedEncodings.add(encoding)
return pumpSendToReply(
request,
reply,
pathnameOrig,
rootPath,
rootPathOffset,
undefined,
checkedEncodings
)
}

return reply.callNotFound()
await reply.send(metadata.error)
break
}

// The `send` library terminates the request with a 404 if the requested
// path contains a dotfile and `send` is initialized with `{dotfiles:
// 'ignore'}`. `send` aborts the request before getting far enough to
// check if the file exists (hence, a 404 `NotFoundError` instead of
// `ENOENT`).
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L582
if (err.status === 404) {
return reply.callNotFound()
case 'file': {
reply.code(statusCode)
if (setHeaders !== undefined) {
setHeaders(reply.raw, metadata.path, metadata.stat)
}
reply.headers(headers)
if (encoding) {
reply.header('content-type', getContentType(pathname))
reply.header('content-encoding', encoding)
}
await reply.send(stream)
break
}

reply.send(err)
})

// we cannot use pump, because send error
// handling is not compatible
stream.pipe(wrap)
}
}

function setUpHeadAndGet (routeOpts, route, file, rootPath) {
Expand All @@ -394,11 +377,11 @@ async function fastifyStatic (fastify, opts) {
fastify.route(toSetUp)
}

function serveFileHandler (req, reply) {
async function serveFileHandler (req, reply) {
// TODO: remove the fallback branch when bump major
/* istanbul ignore next */
/* c8 ignore next */
const routeConfig = req.routeOptions?.config || req.routeConfig
pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
return pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
}
}

Expand Down Expand Up @@ -489,8 +472,6 @@ function getContentType (path) {
}

function findIndexFile (pathname, root, indexFiles = ['index.html']) {
// TODO remove istanbul ignore
/* istanbul ignore else */
if (Array.isArray(indexFiles)) {
return indexFiles.find(filename => {
const p = path.join(root, pathname, filename)
Expand All @@ -502,7 +483,7 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
}
})
}
/* istanbul ignore next */
/* c8 ignore next */
return false
}

Expand Down Expand Up @@ -541,19 +522,16 @@ function getRedirectUrl (url) {
const parsed = new URL(url, 'http://localhost.com/')
const parsedPathname = parsed.pathname
return parsedPathname + (parsedPathname[parsedPathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
} catch {
} /* c8 ignore start */ catch {
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
/* istanbul ignore next */
const err = new Error(`Invalid redirect URL: ${url}`)
/* istanbul ignore next */
err.statusCode = 400
/* istanbul ignore next */
throw err
}
} /* c8 ignore stop */
}

module.exports = fp(fastifyStatic, {
fastify: '4.x',
fastify: '5.x',
name: '@fastify/static'
})
module.exports.default = fastifyStatic
Expand Down
Loading

0 comments on commit 1e041d7

Please sign in to comment.