Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,12 @@ fastify.get('/favicon.ico', function (req, reply) {

### Options

#### `root` (required)
#### `serve`
Default: `true`

If set to `false`, the plugin will not serve files from the `root` directory.

#### `root` (required if `serve` is not false)

The absolute path of the directory containing the files to serve.
The file to serve is determined by combining `req.url` with the
Expand Down
8 changes: 6 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ send.mime.default_type = 'application/octet-stream'

/** @type {import("fastify").FastifyPluginAsync<import("./types").FastifyStaticOptions>} */
async function fastifyStatic (fastify, opts) {
opts.root = normalizeRoot(opts.root)
checkRootPathForErrors(fastify, opts.root)
if (opts.serve !== false || opts.root !== undefined) {
opts.root = normalizeRoot(opts.root)
checkRootPathForErrors(fastify, opts.root)
}

const setHeaders = opts.setHeaders
if (setHeaders !== undefined && typeof setHeaders !== 'function') {
Expand Down Expand Up @@ -199,6 +201,8 @@ async function fastifyStatic (fastify, opts) {
} else {
options.root = rootPath
}
} else if (path.isAbsolute(pathname) === false) {
return reply.callNotFound()
}

if (allowedPath && !allowedPath(pathname, options.root, request)) {
Expand Down
57 changes: 56 additions & 1 deletion test/static.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,51 @@ test('serving disabled', async (t) => {
})
})

test('serving disabled without root', async (t) => {
t.plan(2)

const pluginOptions = {
prefix: '/static/',
serve: false
}
const fastify = Fastify()
fastify.register(fastifyStatic, pluginOptions)

t.after(() => fastify.close())

fastify.get('/foo/bar/r', (_request, reply) => {
reply.sendFile('index.html')
})

fastify.get('/foo/bar/a', (_request, reply) => {
reply.sendFile(path.join(__dirname, pluginOptions.prefix, 'index.html'))
})

t.after(() => fastify.close())

await fastify.listen({ port: 0 })

fastify.server.unref()

await t.test('/static/index.html via sendFile not found', async (t) => {
t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)

const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/a')
t.assert.ok(response.ok)
t.assert.deepStrictEqual(response.status, 200)
t.assert.deepStrictEqual(await response.text(), indexContent)
genericResponseChecks(t, response)
})

await t.test('/static/index.html via sendFile with relative path not found', async (t) => {
t.plan(2)

const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/r')
t.assert.ok(!response.ok)
t.assert.deepStrictEqual(response.status, 404)
})
})

test('sendFile', async (t) => {
t.plan(4)

Expand Down Expand Up @@ -1215,7 +1260,7 @@ test('maxAge option', async (t) => {
})

test('errors', async (t) => {
t.plan(11)
t.plan(12)

await t.test('no root', async (t) => {
t.plan(1)
Expand Down Expand Up @@ -1280,6 +1325,16 @@ test('errors', async (t) => {
await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions))
})

await t.test('no root and serve: false', async (t) => {
t.plan(1)
const pluginOptions = {
serve: false,
root: []
}
const fastify = Fastify({ logger: false })
await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions))
})

await t.test('duplicate root paths are not allowed', async (t) => {
t.plan(1)
const pluginOptions = {
Expand Down
76 changes: 44 additions & 32 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,40 +84,52 @@ declare namespace fastifyStatic {
serveDotFiles?: boolean;
}

export interface FastifyStaticOptions extends SendOptions {
root: string | string[] | URL | URL[];
prefix?: string;
prefixAvoidTrailingSlash?: boolean;
serve?: boolean;
decorateReply?: boolean;
schemaHide?: boolean;
setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
redirect?: boolean;
wildcard?: boolean;
globIgnore?: string[];
list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
/**
* @description
* Opt-in to looking for pre-compressed files
*/
preCompressed?: boolean;

// Passed on to `send`
acceptRanges?: boolean;
contentType?: boolean;
cacheControl?: boolean;
dotfiles?: 'allow' | 'deny' | 'ignore';
etag?: boolean;
extensions?: string[];
immutable?: boolean;
index?: string[] | string | false;
lastModified?: boolean;
maxAge?: string | number;
constraints?: RouteOptions['constraints'];
logLevel?: RouteOptions['logLevel'];
type Root = string | string[] | URL | URL[]

type RootOptions = {
serve: true;
root: Root;
} | {
serve?: false;
root?: Root;
}

export type FastifyStaticOptions =
SendOptions
& RootOptions
& {
// Added by this plugin
prefix?: string;
prefixAvoidTrailingSlash?: boolean;
decorateReply?: boolean;
schemaHide?: boolean;
setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
redirect?: boolean;
wildcard?: boolean;
globIgnore?: string[];
list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
/**
* @description
* Opt-in to looking for pre-compressed files
*/
preCompressed?: boolean;

// Passed on to `send`
acceptRanges?: boolean;
contentType?: boolean;
cacheControl?: boolean;
dotfiles?: 'allow' | 'deny' | 'ignore';
etag?: boolean;
extensions?: string[];
immutable?: boolean;
index?: string[] | string | false;
lastModified?: boolean;
maxAge?: string | number;
constraints?: RouteOptions['constraints'];
logLevel?: RouteOptions['logLevel'];
}

export const fastifyStatic: FastifyStaticPlugin

export { fastifyStatic as default }
Expand Down
13 changes: 13 additions & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ expectAssignable<FastifyStaticOptions>({
root: [new URL('')]
})

expectError<FastifyStaticOptions>({
serve: true
})

expectAssignable<FastifyStaticOptions>({
serve: true,
root: ''
})

expectAssignable<FastifyStaticOptions>({
serve: false
})

appWithImplicitHttp
.register(fastifyStatic, options)
.after(() => {
Expand Down
Loading