Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: fastify/fastify-static
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.2.2
Choose a base ref
...
head repository: fastify/fastify-static
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.5.0
Choose a head ref

Commits on May 27, 2021

  1. Bump fastify/github-action-merge-dependabot from 2.0.0 to 2.1.0 (#207)

    Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 2.0.0 to 2.1.0.
    - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases)
    - [Commits](fastify/github-action-merge-dependabot@v2.0.0...v2.1.0)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored May 27, 2021
    Copy the full SHA
    a8fb02d View commit details

Commits on May 31, 2021

  1. Bump tsd from 0.15.1 to 0.16.0 (#209)

    Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.15.1 to 0.16.0.
    - [Release notes](https://github.com/SamVerschueren/tsd/releases)
    - [Commits](tsdjs/tsd@v0.15.1...v0.16.0)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored May 31, 2021
    Copy the full SHA
    5f10868 View commit details
  2. Bump fastify/github-action-merge-dependabot from 2.1.0 to 2.1.1 (#210)

    Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 2.1.0 to 2.1.1.
    - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases)
    - [Commits](fastify/github-action-merge-dependabot@v2.1.0...v2.1.1)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored May 31, 2021
    Copy the full SHA
    ab59c02 View commit details

Commits on Jun 4, 2021

  1. Bump tsd from 0.16.0 to 0.17.0 (#211)

    Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.16.0 to 0.17.0.
    - [Release notes](https://github.com/SamVerschueren/tsd/releases)
    - [Commits](tsdjs/tsd@v0.16.0...v0.17.0)
    
    ---
    updated-dependencies:
    - dependency-name: tsd
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jun 4, 2021
    Copy the full SHA
    fd28156 View commit details

Commits on Jul 1, 2021

  1. Bump actions/setup-node from 2.1.5 to 2.2.0 (#213)

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.1.5 to 2.2.0.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](actions/setup-node@v2.1.5...v2.2.0)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jul 1, 2021
    Copy the full SHA
    3b295fe View commit details

Commits on Jul 5, 2021

  1. Bump @types/node from 15.14.1 to 16.0.0 (#215)

    Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 15.14.1 to 16.0.0.
    - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
    - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)
    
    ---
    updated-dependencies:
    - dependency-name: "@types/node"
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jul 5, 2021
    Copy the full SHA
    03e36a5 View commit details
  2. Bump fastify/github-action-merge-dependabot from 2.1.1 to 2.2.0 (#216)

    Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 2.1.1 to 2.2.0.
    - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases)
    - [Commits](fastify/github-action-merge-dependabot@v2.1.1...v2.2.0)
    
    ---
    updated-dependencies:
    - dependency-name: fastify/github-action-merge-dependabot
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jul 5, 2021
    Copy the full SHA
    a170820 View commit details

Commits on Jul 6, 2021

  1. Update README.md

    corected readme
    olmesm authored Jul 6, 2021
    Copy the full SHA
    d1a1bc2 View commit details
  2. Merge pull request #217 from olmesm/patch-1

    Update README.md
    jsumners authored Jul 6, 2021
    Copy the full SHA
    ac7dd13 View commit details

Commits on Jul 21, 2021

  1. Bump actions/setup-node from 2.2.0 to 2.3.0 (#223)

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.2.0 to 2.3.0.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](actions/setup-node@v2.2.0...v2.3.0)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Jul 21, 2021
    Copy the full SHA
    05be32e View commit details

Commits on Aug 4, 2021

  1. Bump actions/setup-node from 2.3.0 to 2.3.1 (#224)

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.3.0 to 2.3.1.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](actions/setup-node@v2.3.0...v2.3.1)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Aug 4, 2021
    Copy the full SHA
    5656185 View commit details

Commits on Aug 5, 2021

  1. fix: call 404 handler if requested path is a dotfile (#225)

    * fix: handle NotFoundError
    
    * fix: Add regression test to call 404 handler when send ignores a dotfile (#218)
    
    Co-authored-by: BlackGlory <woshenmedoubuzhidao@blackglory.me>
    Co-authored-by: Gorman Fletcher <git@gormanfletcher.com>
    3 people authored Aug 5, 2021
    Copy the full SHA
    fa907a0 View commit details
  2. Bumped v4.2.3

    Eomm committed Aug 5, 2021
    Copy the full SHA
    526d154 View commit details
  3. Bump actions/setup-node from 2.3.1 to 2.3.2 (#226)

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.3.1 to 2.3.2.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](actions/setup-node@v2.3.1...v2.3.2)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Aug 5, 2021
    Copy the full SHA
    652252b View commit details

Commits on Aug 6, 2021

  1. Bump actions/setup-node from 2.3.2 to 2.4.0 (#227)

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.3.2 to 2.4.0.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](actions/setup-node@v2.3.2...v2.4.0)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Aug 6, 2021
    Copy the full SHA
    27e7035 View commit details

Commits on Aug 9, 2021

  1. Bump fastify/github-action-merge-dependabot from 2.2.0 to 2.3.0 (#228)

    Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 2.2.0 to 2.3.0.
    - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases)
    - [Commits](fastify/github-action-merge-dependabot@v2.2.0...v2.3.0)
    
    ---
    updated-dependencies:
    - dependency-name: fastify/github-action-merge-dependabot
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Aug 9, 2021
    Copy the full SHA
    587ef15 View commit details

Commits on Aug 10, 2021

  1. Bump fastify/github-action-merge-dependabot from 2.3.0 to 2.4.0 (#229)

    Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 2.3.0 to 2.4.0.
    - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases)
    - [Commits](fastify/github-action-merge-dependabot@v2.3.0...v2.4.0)
    
    ---
    updated-dependencies:
    - dependency-name: fastify/github-action-merge-dependabot
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Aug 10, 2021
    Copy the full SHA
    da1c3f0 View commit details

Commits on Sep 20, 2021

  1. Bump fastify/github-action-merge-dependabot from 2.4.0 to 2.5.0 (#233)

    Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 2.4.0 to 2.5.0.
    - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases)
    - [Commits](fastify/github-action-merge-dependabot@v2.4.0...v2.5.0)
    
    ---
    updated-dependencies:
    - dependency-name: fastify/github-action-merge-dependabot
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 20, 2021
    Copy the full SHA
    a1a02cd View commit details

Commits on Sep 28, 2021

  1. Copy the full SHA
    2e97bdd View commit details
  2. build(deps): bump actions/setup-node from 2.4.0 to 2.4.1 (#236)

    Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.4.0 to 2.4.1.
    - [Release notes](https://github.com/actions/setup-node/releases)
    - [Commits](actions/setup-node@v2.4.0...v2.4.1)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-node
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 28, 2021
    Copy the full SHA
    905468d View commit details

Commits on Oct 2, 2021

  1. Copy the full SHA
    521b641 View commit details

Commits on Oct 5, 2021

  1. Merge pull request from GHSA-p6vg-p826-qp3v

    * fix redirect
    
    * add missing domain
    Eomm authored Oct 5, 2021
    Copy the full SHA
    861e0e9 View commit details
  2. Bumped v4.2.4

    mcollina committed Oct 5, 2021
    Copy the full SHA
    d97b2cf View commit details
  3. Add options overload parameter to sendFile function (#238) (#239)

    * docs(readme): fix the 'download' method examples
    
    * feat: add options overload parameter to sendFile
    
    * feat: add options overload parameter to sendFile
    
    * docs(readme): add sendMethod override options version
    
    * fix(minor): remove default value for options from sendFile
    
    Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
    
    Co-authored-by: ArtemM <artemm@rvmode.com>
    Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
    3 people authored Oct 5, 2021
    Copy the full SHA
    8da6140 View commit details

Commits on Oct 6, 2021

  1. Bumped v4.3.0

    mcollina committed Oct 6, 2021
    Copy the full SHA
    33bc265 View commit details

Commits on Oct 7, 2021

  1. Extended dir-list information (#241)

    * -refactored dirlist to use promises
    -added stats and more info to dir list
    
    * -added lastModified
    
    * -added format url parameter
    
    * -added docs
    
    * -fixed compatibility with node 10
    
    * -removed unnecessary request
    -used p-map
    
    * -replaced p-map with p-limit
    
    * -removed logging
    
    * -added error catch
    
    * -improved test
    Jelenkee authored Oct 7, 2021
    Copy the full SHA
    9e3286c View commit details
  2. Bumped v4.4.0

    mcollina committed Oct 7, 2021
    Copy the full SHA
    bbdf96f View commit details

Commits on Oct 11, 2021

  1. Merge pull request from GHSA-pgh6-m65r-2rhq

    * fix redirect injection
    
    * remove console.log
    
    * fix extra case
    Eomm authored Oct 11, 2021
    Copy the full SHA
    c31f17d View commit details
  2. Bumped v4.4.1

    mcollina committed Oct 11, 2021
    Copy the full SHA
    f324f8b View commit details

Commits on Oct 12, 2021

  1. fix: url handle (#247)

    climba03003 authored Oct 12, 2021
    Copy the full SHA
    d871592 View commit details
  2. -fixed href (#244)

    Jelenkee authored Oct 12, 2021
    Copy the full SHA
    ad66ca1 View commit details

Commits on Oct 13, 2021

  1. build(deps-dev): bump tsd from 0.17.0 to 0.18.0 (#248)

    Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.17.0 to 0.18.0.
    - [Release notes](https://github.com/SamVerschueren/tsd/releases)
    - [Commits](tsdjs/tsd@v0.17.0...v0.18.0)
    
    ---
    updated-dependencies:
    - dependency-name: tsd
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Oct 13, 2021
    Copy the full SHA
    e99c9bf View commit details

Commits on Oct 18, 2021

  1. build(deps): bump actions/checkout from 2.3.4 to 2.3.5 (#249)

    Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5.
    - [Release notes](https://github.com/actions/checkout/releases)
    - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
    - [Commits](actions/checkout@v2.3.4...v2.3.5)
    
    ---
    updated-dependencies:
    - dependency-name: actions/checkout
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Oct 18, 2021
    Copy the full SHA
    520f2d9 View commit details

Commits on Nov 3, 2021

  1. build(deps): bump actions/checkout from 2.3.5 to 2.4.0 (#251)

    Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
    - [Release notes](https://github.com/actions/checkout/releases)
    - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
    - [Commits](actions/checkout@v2.3.5...v2.4.0)
    
    ---
    updated-dependencies:
    - dependency-name: actions/checkout
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 3, 2021
    Copy the full SHA
    eec47e8 View commit details
  2. fix(index): replace all * directives with gzip (#250)

    * fix(index): replace all `*` directives with `gzip`
    
    * test(static): check response for `*` directives
    Fdawgs authored Nov 3, 2021
    Copy the full SHA
    2eb1beb View commit details
  3. Bumped v4.5.0

    mcollina committed Nov 3, 2021
    Copy the full SHA
    ac66bf1 View commit details
Showing with 810 additions and 98 deletions.
  1. +3 −3 .github/workflows/ci.yml
  2. +95 −3 README.md
  3. +17 −0 index.d.ts
  4. +73 −26 index.js
  5. +105 −40 lib/dirList.js
  6. +6 −5 package.json
  7. +273 −18 test/dir-list.test.js
  8. +230 −3 test/static.test.js
  9. +8 −0 test/types/index.ts
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -20,10 +20,10 @@ jobs:
os: [macos-latest, ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0

- name: Use Node.js
uses: actions/setup-node@v2.1.5
uses: actions/setup-node@v2.4.1
with:
node-version: ${{ matrix.node-version }}

@@ -39,6 +39,6 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- uses: fastify/github-action-merge-dependabot@v2.0.0
- uses: fastify/github-action-merge-dependabot@v2.5.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
98 changes: 95 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -33,6 +33,9 @@ fastify.get('/path/with/different/root', function (req, reply) {
return reply.sendFile('myHtml.html', path.join(__dirname, 'build')) // serving a file from a different root location
})

fastify.get('/another/path', function (req, reply) {
return reply.sendFile('myHtml.html', { cacheControl: false }) // overriding the options disabling cache-control headers
})
```

### Multiple prefixed roots
@@ -71,11 +74,11 @@ fastify.get('/another/path', function (req, reply) {
})

fastify.get('/path/without/cache/control', function (req, reply) {
return reply.sendFile('myHtml.html', { cacheControl: false }) // serving a file disabling cache-control headers
return reply.download('myHtml.html', { cacheControl: false }) // serving a file disabling cache-control headers
})

fastify.get('/path/without/cache/control', function (req, reply) {
return reply.sendFile('myHtml.html', 'custom-filename.html', { cacheControl: false })
return reply.download('myHtml.html', 'custom-filename.html', { cacheControl: false })
})

```
@@ -97,7 +100,7 @@ Default: `'/'`

A URL path prefix used to create a virtual mount path for the static directory.

### `prefixAvoidTrailingSlash`
#### `prefixAvoidTrailingSlash`

Default: `false`

@@ -173,6 +176,13 @@ This function allows filtering the served files.
If the function returns `true`, the file will be served.
If the function returns `false`, Fastify's 404 handler will be called.

#### `index`

Default: `undefined`

Under the hood we use [send](https://github.com/pillarjs/send#index) lib that by default supports "index.html" files.
To disable this set false or to supply a new index pass a string or an array in preferred order.

#### `list`

Default: `undefined`
@@ -187,6 +197,7 @@ Default response is json.
fastify.register(require('fastify-static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/',
index: false
list: true
})
```
@@ -211,6 +222,14 @@ Options: `html`, `json`

Directory list can be also in `html` format; in that case, `list.render` function is required.

You can override the option with URL parameter `format`. Options are `html` and `json`.

```bash
GET .../public/assets?format=json
```

will return the response as json independent of `list.format`.

**Example:**

```js
@@ -289,6 +308,79 @@ GET .../public/index
GET .../public/index.json
```

#### `list.extendedFolderInfo`

Default: `undefined`

If `true` some extended information for folders will be accessible in `list.render` and in the json response.

```js
render(dirs, files) {
const dir = dirs[0];
dir.fileCount // number of files in this folder
dir.totalFileCount // number of files in this folder (recursive)
dir.folderCount // number of folders in this folder
dir.totalFolderCount // number of folders in this folder (recursive)
dir.totalSize // size of all files in this folder (recursive)
dir.lastModified // most recent last modified timestamp of all files in this folder (recursive)
}
```

Warning: This will slightly decrease the performance, especially for deeply nested file structures.

#### `list.jsonFormat`

Default: `names`

Options: `names`, `extended`

This option determines the output format when `json` is selected.

`names`:
```json
{
"dirs": [
"dir1",
"dir2"
],
"files": [
"file1.txt",
"file2.txt"
]
}
```

`extended`:
```json
{
"dirs": [
{
"name": "dir1",
"stats": {
"dev": 2100,
"size": 4096,
...
},
"extendedInfo": {
"fileCount": 4,
"totalSize": 51233,
...
}
}
],
"files": [
{
"name": "file1.txt",
"stats": {
"dev": 2200,
"size": 554,
...
}
}
]
}
```

#### `preCompressed`

Default: `false`
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -3,24 +3,39 @@
/// <reference types="node" />

import { FastifyPluginCallback, FastifyReply } from 'fastify';
import { Stats } from 'fs';

declare module "fastify" {
interface FastifyReply {
sendFile(filename: string, rootPath?: string): FastifyReply;
sendFile(filename: string, options?: SendOptions): FastifyReply;
sendFile(filename: string, rootPath?: string, options?: SendOptions): FastifyReply;
download(filepath: string, options?: SendOptions): FastifyReply;
download(filepath: string, filename?: string): FastifyReply;
download(filepath: string, filename?: string, options?: SendOptions): FastifyReply;
}
}

interface ExtendedInformation {
fileCount: number;
totalFileCount: number;
folderCount: number;
totalFolderCount: number;
totalSize: number;
lastModified: number;
}

interface ListDir {
href: string;
name: string;
stats: Stats;
extendedInfo?: ExtendedInformation;
}

interface ListFile {
href: string;
name: string;
stats: Stats;
}

interface ListRender {
@@ -31,6 +46,8 @@ interface ListOptions {
format: 'json' | 'html';
names: string[];
render: ListRender;
extendedFolderInfo?: boolean;
jsonFormat?: 'names' | 'extended';
}

// Passed on to `send`
99 changes: 73 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict'

const path = require('path')
const url = require('url')
const statSync = require('fs').statSync
const { PassThrough } = require('readable-stream')
const glob = require('glob')
@@ -43,6 +42,17 @@ async function fastifyStatic (fastify, opts) {

const allowedPath = opts.allowedPath

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

let prefix = opts.prefix

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

function pumpSendToReply (
request,
reply,
@@ -143,18 +153,24 @@ async function fastifyStatic (fastify, opts) {

stream.on('directory', function (_, path) {
if (opts.list) {
return dirList.send({
dirList.send({
reply,
dir: path,
options: opts.list,
route: pathname
})
route: pathname,
prefix
}).catch((err) => reply.send(err))
return
}

if (opts.redirect === true) {
/* eslint node/no-deprecated-api: "off" */
const parsed = url.parse(request.raw.url)
reply.redirect(301, parsed.pathname + '/' + (parsed.search || ''))
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 {
reply.callNotFound()
}
@@ -164,7 +180,14 @@ async function fastifyStatic (fastify, opts) {
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 })
dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix
}).catch((err) => reply.send(err))
return
}

// root paths left to try?
@@ -187,6 +210,17 @@ async function fastifyStatic (fastify, opts) {

return reply.callNotFound()
}

// 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()
}

reply.send(err)
})

@@ -195,17 +229,6 @@ async function fastifyStatic (fastify, opts) {
stream.pipe(wrap)
}

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

let prefix = opts.prefix

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

const errorHandler = (error, request, reply) => {
if (error && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
reply.request.raw.destroy()
@@ -224,12 +247,16 @@ async function fastifyStatic (fastify, opts) {
}

if (opts.decorateReply !== false) {
fastify.decorateReply('sendFile', function (filePath, rootPath) {
fastify.decorateReply('sendFile', function (filePath, rootPath, options) {
const opts = typeof rootPath === 'object' ? rootPath : options
const root = typeof rootPath === 'string' ? rootPath : opts && opts.root
pumpSendToReply(
this.request,
this,
filePath,
rootPath || sendOptions.root
root || sendOptions.root,
0,
opts
)
return this
})
@@ -264,9 +291,7 @@ async function fastifyStatic (fastify, opts) {
})
if (opts.redirect === true && prefix !== opts.prefix) {
fastify.get(opts.prefix, routeOpts, function (req, reply) {
/* eslint node/no-deprecated-api: "off" */
const parsed = url.parse(req.raw.url)
reply.redirect(301, parsed.pathname + '/' + (parsed.search || ''))
reply.redirect(301, getRedirectUrl(req.raw.url))
})
}
} else {
@@ -404,11 +429,11 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
})
}

// Adapted from https://github.com/fastify/fastify-compress/blob/fa5c12a5394285c86d9f438cb39ff44f3d5cde79/index.js#L442
// 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('*', 'gzip')
const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip')
return encodingNegotiator.negotiate(
header,
supportedEncodings.filter((enc) => !checked.has(enc))
@@ -425,6 +450,28 @@ function getEncodingExtension (encoding) {
}
}

function getRedirectUrl (url) {
let i = 0
// we detech how many slash before a valid path
for (i; 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 || '')
} catch (error) {
// 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
}
}

module.exports = fp(fastifyStatic, {
fastify: '3.x',
name: 'fastify-static'
145 changes: 105 additions & 40 deletions lib/dirList.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict'

const path = require('path')
const fs = require('fs')
const fs = require('fs').promises
const pLimit = require('p-limit')

const dirList = {
/**
@@ -10,36 +11,84 @@ const dirList = {
* @param {function(error, entries)} callback
* note: can't use glob because don't get error on non existing dir
*/
list: function (dir, callback) {
list: async function (dir, options) {
const entries = { dirs: [], files: [] }
fs.readdir(dir, (err, files) => {
if (err) {
return callback(err)
}
if (files.length < 1) {
callback(null, entries)
const files = await fs.readdir(dir)
if (files.length < 1) {
return entries
}

const limit = pLimit(4)
await Promise.all(files.map(filename => limit(async () => {
let stats
try {
stats = await fs.stat(path.join(dir, filename))
} catch (error) {
return
}
let j = 0
for (let i = 0; i < files.length; i++) {
const filename = files[i]
fs.stat(path.join(dir, filename), (err, file) => {
if (!err) {
if (file.isDirectory()) {
entries.dirs.push(filename)
} else {
entries.files.push(filename)
}
const entry = { name: filename, stats }
if (stats.isDirectory()) {
if (options.extendedFolderInfo) {
entry.extendedInfo = await getExtendedInfo(path.join(dir, filename))
}
entries.dirs.push(entry)
} else {
entries.files.push(entry)
}
})))

async function getExtendedInfo (folderPath) {
const depth = folderPath.split(path.sep).length
let totalSize = 0
let fileCount = 0
let totalFileCount = 0
let folderCount = 0
let totalFolderCount = 0
let lastModified = 0

async function walk (dir) {
const files = await fs.readdir(dir)
const limit = pLimit(4)
await Promise.all(files.map(filename => limit(async () => {
const filePath = path.join(dir, filename)
let stats
try {
stats = await fs.stat(filePath)
} catch (error) {
return
}

if (j++ >= files.length - 1) {
entries.dirs.sort()
entries.files.sort()
callback(null, entries)
if (stats.isDirectory()) {
totalFolderCount++
if (filePath.split(path.sep).length === depth + 1) {
folderCount++
}
await walk(filePath)
} else {
totalSize += stats.size
totalFileCount++
if (filePath.split(path.sep).length === depth + 1) {
fileCount++
}
lastModified = Math.max(lastModified, stats.mtimeMs)
}
})
})))
}

await walk(folderPath)
return {
totalSize,
fileCount,
totalFileCount,
folderCount,
totalFolderCount,
lastModified
}
})
}

entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
entries.files.sort((a, b) => a.name.localeCompare(b.name))
return entries
},

/**
@@ -49,33 +98,49 @@ const dirList = {
* @param {ListOptions} options
* @param {string} route request route
*/
send: function ({ reply, dir, options, route }) {
dirList.list(dir, (err, entries) => {
if (err) {
reply.callNotFound()
return
}
send: async function ({ reply, dir, options, route, prefix }) {
let entries
try {
entries = await dirList.list(dir, options)
} catch (error) {
return reply.callNotFound()
}
const format = reply.request.query.format || options.format
if (format !== 'html') {
if (options.jsonFormat !== 'extended') {
const nameEntries = { dirs: [], files: [] }
entries.dirs.forEach(entry => nameEntries.dirs.push(entry.name))
entries.files.forEach(entry => nameEntries.files.push(entry.name))

if (options.format !== 'html') {
reply.send(nameEntries)
} else {
reply.send(entries)
return
}
return
}

const html = options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route)),
entries.files.map(entry => dirList.htmlInfo(entry, route)))
reply.type('text/html').send(html)
})
const html = options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route, prefix, options)),
entries.files.map(entry => dirList.htmlInfo(entry, route, prefix, options)))
reply.type('text/html').send(html)
},

/**
* provide the html information about entry and route, to get name and full route
* @param {string} entry file or dir name
* @param entry file or dir name and stats
* @param {string} route request route
* @return {ListFile}
*/
htmlInfo: function (entry, route) {
return { href: path.join(path.dirname(route), entry).replace(/\\/g, '/'), name: entry }
htmlInfo: function (entry, route, prefix, options) {
if (options.names && options.names.includes(path.basename(route))) {
route = path.normalize(path.join(route, '..'))
}
return {
href: path.join(prefix, route, entry.name).replace(/\\/g, '/'),
name: entry.name,
stats: entry.stats,
extendedInfo: entry.extendedInfo
}
},

/**
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "fastify-static",
"version": "4.2.2",
"version": "4.5.0",
"description": "Plugin for serving static files as fast as possible.",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"lint": "standard | snazzy",
"lint:fix": "standard --fix",
"unit": "tap test/*.test.js",
"unit": "tap --no-check-coverage test/*.test.js",
"typescript": "tsd",
"test": "npm run lint && npm run unit && npm run typescript",
"example": "node example/server.js",
"coverage": "tap --cov --coverage-report=html test",
"coverage": "tap --cov --coverage-report=html --no-check-coverage test",
"coveralls": "tap test/*test.js test/*/*.test.js --cov"
},
"repository": {
@@ -33,11 +33,12 @@
"encoding-negotiator": "^2.0.1",
"fastify-plugin": "^3.0.0",
"glob": "^7.1.4",
"p-limit": "^3.1.0",
"readable-stream": "^3.4.0",
"send": "^0.17.1"
},
"devDependencies": {
"@types/node": "^15.0.0",
"@types/node": "^16.0.0",
"@typescript-eslint/eslint-plugin": "^2.29.0",
"@typescript-eslint/parser": "^2.29.0",
"concat-stream": "^2.0.0",
@@ -52,7 +53,7 @@
"snazzy": "^9.0.0",
"standard": "^16.0.2",
"tap": "^15.0.0",
"tsd": "^0.15.0",
"tsd": "^0.18.0",
"typescript": "^4.0.2"
},
"tsd": {
291 changes: 273 additions & 18 deletions test/dir-list.test.js
Original file line number Diff line number Diff line change
@@ -12,8 +12,11 @@ const fastifyStatic = require('..')

const helper = {
arrange: function (t, options, f) {
return helper.arrangeModule(t, options, fastifyStatic, f)
},
arrangeModule: function (t, options, mock, f) {
const fastify = Fastify()
fastify.register(fastifyStatic, options)
fastify.register(mock, options)
t.teardown(fastify.close.bind(fastify))
fastify.listen(0, err => {
t.error(err)
@@ -47,7 +50,7 @@ t.test('dir list wrong options', t => {
root: path.join(__dirname, '/static'),
list: {
format: 'html'
// no render function
// no render function
}
},
error: new TypeError('The `list.render` option must be a function and is required with html format')
@@ -156,16 +159,16 @@ t.test('dir list html format', t => {
output: `
<html><body>
<ul>
<li><a href="/deep">deep</a></li>
<li><a href="/shallow">shallow</a></li>
<li><a href="/public/deep">deep</a></li>
<li><a href="/public/shallow">shallow</a></li>
</ul>
<ul>
<li><a href="/.example" target="_blank">.example</a></li>
<li><a href="/a .md" target="_blank">a .md</a></li>
<li><a href="/foo.html" target="_blank">foo.html</a></li>
<li><a href="/foobar.html" target="_blank">foobar.html</a></li>
<li><a href="/index.css" target="_blank">index.css</a></li>
<li><a href="/index.html" target="_blank">index.html</a></li>
<li><a href="/public/.example" target="_blank">.example</a></li>
<li><a href="/public/a .md" target="_blank">a .md</a></li>
<li><a href="/public/foo.html" target="_blank">foo.html</a></li>
<li><a href="/public/foobar.html" target="_blank">foobar.html</a></li>
<li><a href="/public/index.css" target="_blank">index.css</a></li>
<li><a href="/public/index.html" target="_blank">index.html</a></li>
</ul>
</body></html>
`
@@ -187,16 +190,16 @@ t.test('dir list html format', t => {
output: `
<html><body>
<ul>
<li><a href="/deep">deep</a></li>
<li><a href="/shallow">shallow</a></li>
<li><a href="/public/deep">deep</a></li>
<li><a href="/public/shallow">shallow</a></li>
</ul>
<ul>
<li><a href="/.example" target="_blank">.example</a></li>
<li><a href="/a .md" target="_blank">a .md</a></li>
<li><a href="/foo.html" target="_blank">foo.html</a></li>
<li><a href="/foobar.html" target="_blank">foobar.html</a></li>
<li><a href="/index.css" target="_blank">index.css</a></li>
<li><a href="/index.html" target="_blank">index.html</a></li>
<li><a href="/public/.example" target="_blank">.example</a></li>
<li><a href="/public/a .md" target="_blank">a .md</a></li>
<li><a href="/public/foo.html" target="_blank">foo.html</a></li>
<li><a href="/public/foobar.html" target="_blank">foobar.html</a></li>
<li><a href="/public/index.css" target="_blank">index.css</a></li>
<li><a href="/public/index.html" target="_blank">index.html</a></li>
</ul>
</body></html>
`
@@ -237,6 +240,133 @@ t.test('dir list html format', t => {
}
})

t.test('dir list href nested structure', t => {
t.plan(6)

const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'html',
names: ['index', 'index.htm'],
render (dirs, files) {
return dirs[0].href
}
}
}

const routes = [
{ path: '/public/', response: '/public/deep' },
{ path: '/public/index', response: '/public/deep' },
{ path: '/public/deep/', response: '/public/deep/path' },
{ path: '/public/deep/index.htm', response: '/public/deep/path' },
{ path: '/public/deep/path/', response: '/public/deep/path/for' }
]
helper.arrange(t, options, (url) => {
for (const route of routes) {
t.test(route.path, t => {
t.plan(5)
simple.concat({
method: 'GET',
url: url + route.path
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), route.response)
simple.concat({
method: 'GET',
url: url + body.toString()
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
})
})
})
}
})
})

t.test('dir list html format - stats', t => {
t.plan(7)

const options1 = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'html',
render (dirs, files) {
t.ok(dirs.length > 0)
t.ok(files.length > 0)

t.ok(dirs.every(every))
t.ok(files.every(every))

function every (value) {
return value.stats &&
value.stats.atime &&
!value.extendedInfo
}
}
}
}

const route = '/public/'

helper.arrange(t, options1, (url) => {
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
})
})
})

t.test('dir list html format - extended info', t => {
t.plan(4)

const route = '/public/'

const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'html',
extendedFolderInfo: true,
render (dirs, files) {
t.test('dirs', t => {
t.plan(dirs.length * 7)

for (const value of dirs) {
t.ok(value.extendedInfo)

t.equal(typeof value.extendedInfo.fileCount, 'number')
t.equal(typeof value.extendedInfo.totalFileCount, 'number')
t.equal(typeof value.extendedInfo.folderCount, 'number')
t.equal(typeof value.extendedInfo.totalFolderCount, 'number')
t.equal(typeof value.extendedInfo.totalSize, 'number')
t.equal(typeof value.extendedInfo.lastModified, 'number')
}
})
}
}
}

helper.arrange(t, options, (url) => {
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
})
})
})

t.test('dir list json format', t => {
t.plan(2)

@@ -270,6 +400,92 @@ t.test('dir list json format', t => {
})
})

t.test('dir list json format - extended info', t => {
t.plan(2)

const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
prefixAvoidTrailingSlash: true,
list: {
format: 'json',
names: ['index', 'index.json', '/'],
extendedFolderInfo: true,
jsonFormat: 'extended'

}
}
const routes = ['/public/shallow/']

helper.arrange(t, options, (url) => {
for (const route of routes) {
t.test(route, t => {
t.plan(5)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
const bodyObject = JSON.parse(body.toString())
t.equal(bodyObject.dirs[0].name, 'empty')
t.equal(typeof bodyObject.dirs[0].stats.atime, 'string')
t.equal(typeof bodyObject.dirs[0].extendedInfo.totalSize, 'number')
})
})
}
})
})

t.test('dir list - url parameter format', t => {
t.plan(13)

const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'html',
render (dirs, files) {
return 'html'
}
}
}
const route = '/public/'

helper.arrange(t, options, (url) => {
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), 'html')
t.ok(response.headers['content-type'].includes('text/html'))
})

simple.concat({
method: 'GET',
url: url + route + '?format=html'
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), 'html')
t.ok(response.headers['content-type'].includes('text/html'))
})

simple.concat({
method: 'GET',
url: url + route + '?format=json'
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.ok(body.toString())
t.ok(response.headers['content-type'].includes('application/json'))
})
})
})

t.test('dir list on empty dir', t => {
t.plan(2)

@@ -387,3 +603,42 @@ t.test('serve a non existent dir and get error', t => {
})
})
})

t.test('dir list error', t => {
t.plan(7)

const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
prefixAvoidTrailingSlash: true,
index: false,
list: {
format: 'html',
names: ['index', 'index.htm'],
render: () => ''
}
}

const errorMessage = 'mocking send'
const dirList = require('../lib/dirList')
dirList.send = async () => { throw new Error(errorMessage) }

const mock = t.mock('..', {
'../lib/dirList.js': dirList
})

const routes = ['/public/', '/public/index.htm']

helper.arrangeModule(t, options, mock, (url) => {
for (const route of routes) {
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(JSON.parse(body.toString()).message, errorMessage)
t.equal(response.statusCode, 500)
})
}
})
})
233 changes: 230 additions & 3 deletions test/static.test.js
Original file line number Diff line number Diff line change
@@ -35,6 +35,9 @@ const innerIndex = fs
const allThreeBr = fs.readFileSync(
'./test/static-pre-compressed/all-three.html.br'
)
const allThreeGzip = fs.readFileSync(
'./test/static-pre-compressed/all-three.html.gz'
)
const gzipOnly = fs.readFileSync(
'./test/static-pre-compressed/gzip-only.html.gz'
)
@@ -748,6 +751,49 @@ t.test('not found responses can be customized with fastify.setNotFoundHandler()'
})
})

t.test('fastify.setNotFoundHandler() is called for dotfiles when when send is configured to ignore dotfiles', t => {
t.plan(2)

const pluginOptions = {
root: path.join(__dirname, '/static'),
send: {
dotfiles: 'ignore'
}
}
const fastify = Fastify()

fastify.setNotFoundHandler(function notFoundHandler (request, reply) {
reply.code(404).type('text/plain').send(request.raw.url + ' Not Found')
})

fastify.register(fastifyStatic, pluginOptions)

t.teardown(fastify.close.bind(fastify))

fastify.listen(0, err => {
t.error(err)

fastify.server.unref()

// Requesting files with a leading dot doesn't follow the same code path as
// other 404 errors
t.test('/path/does/not/.exist.html', t => {
t.plan(4)

simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/path/does/not/.exist.html',
followRedirect: false
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 404)
t.equal(response.headers['content-type'], 'text/plain')
t.equal(body.toString(), '/path/does/not/.exist.html Not Found')
})
})
})
})

t.test('serving disabled', (t) => {
t.plan(3)

@@ -797,13 +843,14 @@ t.test('serving disabled', (t) => {
})

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

const pluginOptions = {
root: path.join(__dirname, '/static'),
prefix: '/static'
}
const fastify = Fastify()
const maxAge = Math.round(Math.random() * 10) * 10000
fastify.register(fastifyStatic, pluginOptions)

fastify.get('/foo/bar', function (req, reply) {
@@ -817,6 +864,10 @@ t.test('sendFile', (t) => {
)
})

fastify.get('/foo/bar/options/override/test', function (req, reply) {
return reply.sendFile('/index.html', { maxAge })
})

fastify.listen(0, (err) => {
t.error(err)

@@ -863,6 +914,21 @@ t.test('sendFile', (t) => {
genericResponseChecks(t, response)
})
})

t.test('reply.sendFile() with options', (t) => {
t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/foo/bar/options/override/test',
followRedirect: false
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(response.headers['cache-control'], `public, max-age=${maxAge / 1000}`)
t.equal(body.toString(), indexContent)
genericResponseChecks(t, response)
})
})
})
})

@@ -2952,6 +3018,66 @@ t.test(
}
)

t.test(
'will serve pre-compressed files with .gzip if * directive used',
async (t) => {
const pluginOptions = {
root: path.join(__dirname, '/static-pre-compressed'),
prefix: '/static-pre-compressed/',
preCompressed: true
}

const fastify = Fastify()

fastify.register(fastifyStatic, pluginOptions)
t.teardown(fastify.close.bind(fastify))

const response = await fastify.inject({
method: 'GET',
url: '/static-pre-compressed/all-three.html',
headers: {
'accept-encoding': '*'
}
})

genericResponseChecks(t, response)
t.equal(response.headers['content-encoding'], 'gzip')
t.equal(response.statusCode, 200)
t.same(response.rawPayload, allThreeGzip)
t.end()
}
)

t.test(
'will serve pre-compressed files with .gzip if multiple * directives used',
async (t) => {
const pluginOptions = {
root: path.join(__dirname, '/static-pre-compressed'),
prefix: '/static-pre-compressed/',
preCompressed: true
}

const fastify = Fastify()

fastify.register(fastifyStatic, pluginOptions)
t.teardown(fastify.close.bind(fastify))

const response = await fastify.inject({
method: 'GET',
url: '/static-pre-compressed/all-three.html',
headers: {
'accept-encoding': '*, *'
}
})

genericResponseChecks(t, response)
t.equal(response.headers['content-encoding'], 'gzip')
t.equal(response.statusCode, 200)
t.same(response.rawPayload, allThreeGzip)
t.end()
}
)

t.test(
'will serve uncompressed files if there are no compressed variants on disk',
async (t) => {
@@ -3014,7 +3140,7 @@ t.test(
)

t.test(
'will serve pre-compressed files and fallback to .gz if .br is not on disk (with wildcard: false) ',
'will serve pre-compressed files and fallback to .gz if .br is not on disk (with wildcard: false)',
async (t) => {
const pluginOptions = {
root: path.join(__dirname, '/static-pre-compressed'),
@@ -3045,7 +3171,69 @@ t.test(
)

t.test(
'will serve uncompressed files if there are no compressed variants on disk (with wildcard: false)',
'will serve pre-compressed files with .gzip if * directive used (with wildcard: false)',
async (t) => {
const pluginOptions = {
root: path.join(__dirname, '/static-pre-compressed'),
prefix: '/static-pre-compressed/',
preCompressed: true,
wildcard: false
}

const fastify = Fastify()

fastify.register(fastifyStatic, pluginOptions)
t.teardown(fastify.close.bind(fastify))

const response = await fastify.inject({
method: 'GET',
url: '/static-pre-compressed/all-three.html',
headers: {
'accept-encoding': '*'
}
})

genericResponseChecks(t, response)
t.equal(response.headers['content-encoding'], 'gzip')
t.equal(response.statusCode, 200)
t.same(response.rawPayload, allThreeGzip)
t.end()
}
)

t.test(
'will serve pre-compressed files with .gzip if multiple * directives used (with wildcard: false)',
async (t) => {
const pluginOptions = {
root: path.join(__dirname, '/static-pre-compressed'),
prefix: '/static-pre-compressed/',
preCompressed: true,
wildcard: false
}

const fastify = Fastify()

fastify.register(fastifyStatic, pluginOptions)
t.teardown(fastify.close.bind(fastify))

const response = await fastify.inject({
method: 'GET',
url: '/static-pre-compressed/all-three.html',
headers: {
'accept-encoding': '*, *'
}
})

genericResponseChecks(t, response)
t.equal(response.headers['content-encoding'], 'gzip')
t.equal(response.statusCode, 200)
t.same(response.rawPayload, allThreeGzip)
t.end()
}
)

t.test(
'will serve uncompressed files if there are no compressed variants on disk (with wildcard: false)',
async (t) => {
const pluginOptions = {
root: path.join(__dirname, '/static-pre-compressed'),
@@ -3219,3 +3407,42 @@ t.test(
t.end()
}
)

t.test('should not redirect to protocol-relative locations', (t) => {
const urls = [
['//^/..', '/', 301],
['//^/.', null, 404], // it is NOT recognized as a directory by pillarjs/send
['//:/..', '/', 301],
['/\\\\a//google.com/%2e%2e%2f%2e%2e', '/a//google.com/%2e%2e%2f%2e%2e/', 301],
['//a//youtube.com/%2e%2e%2f%2e%2e', '/a//youtube.com/%2e%2e%2f%2e%2e/', 301],
['/^', null, 404], // it is NOT recognized as a directory by pillarjs/send
['//google.com/%2e%2e', '/', 301],
['//users/%2e%2e', '/', 301],
['//users', null, 404],
['///deep/path//for//test//index.html', null, 200]
]

t.plan(1 + urls.length * 2)
const fastify = Fastify()
fastify.register(fastifyStatic, {
root: path.join(__dirname, '/static'),
redirect: true
})
t.teardown(fastify.close.bind(fastify))
fastify.listen(0, (err, address) => {
t.error(err)
urls.forEach(([testUrl, expected, status]) => {
const req = http.request(url.parse(address + testUrl), res => {
t.equal(res.statusCode, status, `status ${testUrl}`)

if (expected) {
t.equal(res.headers.location, expected)
} else {
t.notOk(res.headers.location)
}
})
req.on('error', t.error)
req.end()
})
})
})
8 changes: 8 additions & 0 deletions test/types/index.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,14 @@ multiRootAppWithImplicitHttp
reply.sendFile('some-file-name')
})

multiRootAppWithImplicitHttp.get('/', (request, reply) => {
reply.sendFile('some-file-name', { cacheControl: false, acceptRanges: true })
})

multiRootAppWithImplicitHttp.get('/', (request, reply) => {
reply.sendFile('some-file-name', 'some-root-name', { cacheControl: false, acceptRanges: true })
})

multiRootAppWithImplicitHttp.get('/download', (request, reply) => {
reply.download('some-file-name')
})