Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

31
backend/node_modules/@fastify/static/.eslintrc.json generated vendored Normal file
View File

@@ -0,0 +1,31 @@
{
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "standard"],
"overrides": [
{
"files": ["types/*.test-d.ts", "types/*.d.ts"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.eslint.json"]
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-misused-promises": ["error", {
"checksVoidReturn": false
}]
}
}
]
}

5
backend/node_modules/@fastify/static/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Set the default behavior, in case people don't have core.autocrlf set
* text=auto
# Require Unix line endings
* text eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

21
backend/node_modules/@fastify/static/.github/stale.yml generated vendored Normal file
View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,23 @@
name: CI
on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
with:
license-check: true
lint: true

2
backend/node_modules/@fastify/static/.taprc generated vendored Normal file
View File

@@ -0,0 +1,2 @@
files:
- test/**/*.test.js

21
backend/node_modules/@fastify/static/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-2023 Fastify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

493
backend/node_modules/@fastify/static/README.md generated vendored Normal file
View File

@@ -0,0 +1,493 @@
# @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)
[![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 `4.x`.
| Fastify version | branch |
| --------------- | -------------------------------------------------------------------- |
| `^4.x` | This branch |
| `^3.x` | [`v5.x`](https://github.com/fastify/fastify-static/tree/v5.x) |
| `^2.x` | [`2.x`](https://github.com/fastify/fastify-static/tree/2.x) |
| `^1.11.x` | [`1.x`](https://github.com/fastify/fastify-static/tree/1.x) |
## Install
`npm i @fastify/static`
## Usage
```js
const fastify = require('fastify')({logger: true})
const path = require('node:path')
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/', // optional: default '/'
constraints: { host: 'example.com' } // optional: default {}
})
fastify.get('/another/path', function (req, reply) {
reply.sendFile('myHtml.html') // serving path.join(__dirname, 'public', 'myHtml.html') directly
})
fastify.get('/another/patch-async', async function (req, reply) {
return reply.sendFile('myHtml.html')
})
fastify.get('/path/with/different/root', function (req, reply) {
reply.sendFile('myHtml.html', path.join(__dirname, 'build')) // serving a file from a different root location
})
fastify.get('/another/path', function (req, reply) {
reply.sendFile('myHtml.html', { cacheControl: false }) // overriding the options disabling cache-control headers
})
// Run the server!
fastify.listen({ port: 3000 }, (err, address) => {
if (err) throw err
// Server is now listening on ${address}
})
```
### Multiple prefixed roots
```js
const fastify = require('fastify')()
const fastifyStatic = require('@fastify/static')
const path = require('node:path')
// first plugin
fastify.register(fastifyStatic, {
root: path.join(__dirname, 'public')
})
// second plugin
fastify.register(fastifyStatic, {
root: path.join(__dirname, 'node_modules'),
prefix: '/node_modules/',
decorateReply: false // the reply decorator has been added by the first plugin registration
})
```
### Sending a file with `content-disposition` header
```js
const fastify = require('fastify')()
const path = require('node:path')
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/', // optional: default '/'
})
fastify.get('/another/path', function (req, reply) {
reply.download('myHtml.html', 'custom-filename.html') // sending path.join(__dirname, 'public', 'myHtml.html') directly with custom filename
})
fastify.get('another/patch-async', async function (req, reply) {
// an async handler must always return the reply object
return reply.download('myHtml.html', 'custom-filename.html')
})
fastify.get('/path/without/cache/control', function (req, reply) {
reply.download('myHtml.html', { cacheControl: false }) // serving a file disabling cache-control headers
})
fastify.get('/path/without/cache/control', function (req, reply) {
reply.download('myHtml.html', 'custom-filename.html', { cacheControl: false })
})
```
### Options
#### `root` (required)
The absolute path of the directory that contains the files to serve.
The file to serve will be determined by combining `req.url` with the
provided root directory.
You can also provide an array of directories containing files to serve.
This is useful for serving multiple static directories under a single prefix. Files are served in a "first found, first served" manner, so the order in which you list the directories is important. For best performance, you should always list your main asset directory first. Duplicate paths will raise an error.
#### `prefix`
Default: `'/'`
A URL path prefix used to create a virtual mount path for the static directory.
#### `constraints`
Default: `{}`
Constraints that will be added to registered routes. See Fastify's documentation for
[route constraints](https://fastify.dev/docs/latest/Reference/Routes/#constraints).
#### `prefixAvoidTrailingSlash`
Default: `false`
If set to false prefix will get trailing "/" at the end. If set to true, prefix will not append "/" to prefix.
#### `schemaHide`
Default: `true`
A flag that define if the fastify route hide-schema attribute is hidden or not
#### `setHeaders`
Default: `undefined`
A function to set custom headers on the response. Alterations to the headers
must be done synchronously. The function is called as `fn(res, path, stat)`,
where the arguments are:
- `res` The response object.
- `path` The path of the file that is being sent.
- `stat` The stat object of the file that is being sent.
#### `send` Options
The following options are also supported and will be passed directly to the
[`send`](https://www.npmjs.com/package/send) module:
- [`acceptRanges`](https://www.npmjs.com/package/send#acceptranges)
- [`cacheControl`](https://www.npmjs.com/package/send#cachecontrol)
- [`dotfiles`](https://www.npmjs.com/package/send#dotfiles)
- [`etag`](https://www.npmjs.com/package/send#etag)
- [`extensions`](https://www.npmjs.com/package/send#extensions)
- [`immutable`](https://www.npmjs.com/package/send#immutable)
- [`index`](https://www.npmjs.com/package/send#index)
- [`lastModified`](https://www.npmjs.com/package/send#lastmodified)
- [`maxAge`](https://www.npmjs.com/package/send#maxage)
You're able to alter this options when calling `reply.download('filename.html', options)` or `reply.download('filename.html', 'otherfilename.html', options)` on each response to a request.
#### `redirect`
Default: `false`
If set to `true`, `@fastify/static` redirects to the directory with a trailing slash.
This option cannot be set to `true` with `wildcard` set to `false` on a server
with `ignoreTrailingSlash` set to `true`.
If this option is set to `false`, then requesting directories without trailing
slash will trigger your app's 404 handler using `reply.callNotFound()`.
#### `wildcard`
Default: `true`
If set to `true`, `@fastify/static` adds a wildcard route to serve files.
If set to `false`, `@fastify/static` globs the filesystem for all defined
files in the served folder (`${root}/**/**`), and just creates the routes needed for
those and it will not serve the newly added file on the filesystem.
The default options of https://www.npmjs.com/package/glob are applied
for getting the file list.
This option cannot be set to `false` with `redirect` set to `true` on a server
with `ignoreTrailingSlash` set to `true`.
#### `allowedPath`
Default: `(pathName, root, request) => true`
This function allows filtering the served files. Also, with the help of the request object a more complex path authentication is possible.
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.
#### `serveDotFiles`
Default: `false`
When `true`, files in hidden directories (e.g. `.foo`) will be served.
#### `list`
Default: `undefined`
If set, it provides the directory list calling the directory path.
Default response is json.
Note:
- Multi-root is not supported within the `list` option.
- If `dotfiles` option value is `deny` or `ignore`, dotfiles will be excluded.
**Example:**
```js
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/',
index: false
list: true
})
```
Request
```bash
GET .../public
```
Response
```json
{ "dirs": ["dir1", "dir2"], "files": ["file1.png", "file2.txt"] }
```
#### `list.format`
Default: `json`
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
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/',
list: {
format: 'html',
render: (dirs, files) => {
return `
<html><body>
<ul>
${dirs.map(dir => `<li><a href="${dir.href}">${dir.name}</a></li>`).join('\n ')}
</ul>
<ul>
${files.map(file => `<li><a href="${file.href}" target="_blank">${file.name}</a></li>`).join('\n ')}
</ul>
</body></html>
`
},
}
})
```
Request
```bash
GET .../public
```
Response
```html
<html><body>
<ul>
<li><a href="/dir1">dir1</a></li>
<li><a href="/dir1">dir2</a></li>
</ul>
<ul>
<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>
</ul>
</body></html>
```
#### `list.names`
Default: `['']`
Directory list can respond to different routes, declared in `list.names` options.
Note: if a file with the same name exists, the actual file is sent.
**Example:**
```js
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, '/static'),
prefix: '/public',
prefixAvoidTrailingSlash: true,
list: {
format: 'json',
names: ['index', 'index.json', '/']
}
})
```
Dir list respond with the same content to
```bash
GET .../public
GET .../public/
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`
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 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`.
#### Disabling reply decorator
The reply object is decorated with a `sendFile` function by default. If you want to
disable this, pass the option `{ decorateReply: false }`. If @fastify/static is
registered to multiple prefixes in the same route only one can initialize reply
decorators.
#### Handling 404s
If a request matches the URL `prefix` but a file cannot be found for the
request, Fastify's 404 handler will be called. You can set a custom 404
handler with [`fastify.setNotFoundHandler()`](https://fastify.dev/docs/latest/Reference/Server/#setnotfoundhandler).
When registering `@fastify/static` within an encapsulated context, the `wildcard` option may need to be set to `false` in order to support index resolution and nested not-found-handler:
```js
const app = require('fastify')();
app.register((childContext, _, done) => {
childContext.register(require('@fastify/static'), {
root: path.join(__dirname, 'docs'), // docs is a folder that contains `index.html` and `404.html`
wildcard: false
});
childContext.setNotFoundHandler((_, reply) => {
return reply.code(404).type('text/html').sendFile('404.html');
});
done();
}, { prefix: 'docs' });
```
This code will send the `index.html` for the paths `docs`, `docs/`, and `docs/index.html`. For all other `docs/<undefined-routes>` it will reply with `404.html`.
### Handling Errors
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`
If you need to access the filename inside the `onSend` hook, you can use `payload.filename`.
```js
fastify.addHook('onSend', function (req, reply, payload, next) {
console.log(payload.filename)
next()
})
```
## License
Licensed under [MIT](./LICENSE)

View File

@@ -0,0 +1 @@
{"hello": "world"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,9 @@
#my-button {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 28px;
margin-top: -14px;
margin-left: -100px;
}

View File

@@ -0,0 +1,11 @@
<html>
<head>
<script src="index.js"></script>
<link rel="stylesheet" type="text/css" href="index.css"></link>
</head>
<body>
<button id="my-button">
The button
</button>
</body>
</html>

View File

@@ -0,0 +1,8 @@
'use strict'
window.onload = function () {
const b = document.getElementById('my-button')
b.onclick = function () {
window.alert('foo')
}
}

View File

@@ -0,0 +1,4 @@
body {
background-color: black;
color: white;
}

View File

@@ -0,0 +1,8 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="test.css"></link>
</head>
<body>
<h1>Test 2</h1>
</body>
</html>

View File

@@ -0,0 +1,15 @@
'use strict'
const path = require('node:path')
const fastify = require('fastify')({ logger: { level: 'trace' } })
fastify
// Compress everything.
.register(require('@fastify/compress'), { threshold: 0 })
.register(require('../'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public')
})
.listen({ port: 3000 }, err => {
if (err) throw err
})

View File

@@ -0,0 +1,49 @@
'use strict'
const path = require('node:path')
const Handlebars = require('handlebars')
const fastify = require('fastify')({ logger: { level: 'trace' } })
// Handlebar template for listing files and directories.
const template = `
<html>
<body>
dirs
<ul>
{{#dirs}}
<li><a href="{{href}}">{{name}}</a></li>
{{/dirs}}
</ul>
list
<ul>
{{#files}}
<li><a href="{{href}}" target="_blank">{{name}}</a></li>
{{/files}}
</ul>
</body>
</html>
`
const handlebarTemplate = Handlebars.compile(template)
fastify
.register(require('..'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public'),
// Do not append a trailing slash to prefixes.
prefixAvoidTrailingSlash: true,
// Return a directory listing with a handlebar template.
list: {
// html or json response? html requires a render method.
format: 'html',
// A list of filenames that trigger a directory list response.
names: ['index', 'index.html', 'index.htm', '/'],
// You can provide your own render method as needed.
render: (dirs, files) => handlebarTemplate({ dirs, files })
}
})
.listen({ port: 3000 }, err => {
if (err) throw err
})

View File

@@ -0,0 +1,15 @@
'use strict'
const path = require('node:path')
const fastify = require('fastify')({ logger: { level: 'trace' } })
fastify
.register(require('../'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public'),
wildcard: false,
serveDotFiles: true
})
.listen({ port: 3000 }, err => {
if (err) throw err
})

13
backend/node_modules/@fastify/static/example/server.js generated vendored Normal file
View File

@@ -0,0 +1,13 @@
'use strict'
const path = require('node:path')
const fastify = require('fastify')({ logger: { level: 'trace' } })
fastify
.register(require('../'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public')
})
.listen({ port: 3000 }, err => {
if (err) throw err
})

560
backend/node_modules/@fastify/static/index.js generated vendored Normal file
View File

@@ -0,0 +1,560 @@
'use strict'
const { PassThrough } = require('node:stream')
const path = require('node:path')
const { fileURLToPath } = require('node:url')
const { statSync } = require('node:fs')
const { glob } = require('glob')
const fp = require('fastify-plugin')
const send = require('@fastify/send')
const encodingNegotiator = require('@fastify/accept-negotiator')
const contentDisposition = require('content-disposition')
const dirList = require('./lib/dirList')
const endForwardSlashRegex = /\/$/u
const asteriskRegex = /\*/gu
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')
}
const invalidDirListOpts = dirList.validateOptions(opts)
if (invalidDirListOpts) {
throw invalidDirListOpts
}
if (opts.dotfiles === undefined) {
opts.dotfiles = 'allow'
}
const sendOptions = {
root: opts.root,
acceptRanges: opts.acceptRanges,
cacheControl: opts.cacheControl,
dotfiles: opts.dotfiles,
etag: opts.etag,
extensions: opts.extensions,
immutable: opts.immutable,
index: opts.index,
lastModified: opts.lastModified,
maxAge: opts.maxAge
}
let prefix = opts.prefix ?? (opts.prefix = '/')
if (!opts.prefixAvoidTrailingSlash) {
prefix =
prefix[prefix.length - 1] === '/'
? prefix
: prefix + '/'
}
// Set the schema hide property if defined in opts or true by default
const routeOpts = {
constraints: opts.constraints,
schema: {
hide: opts.schemaHide !== undefined ? opts.schemaHide : true
},
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) {
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,
root || sendOptions.root,
0,
opts
)
return this
})
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))
pumpSendToReply(this.request, this, filePath, root, 0, opts)
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 === undefined || opts.wildcard === true) {
fastify.route({
...routeOpts,
method: ['HEAD', 'GET'],
path: prefix + '*',
handler (req, reply) {
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
}
})
if (opts.redirect === true && prefix !== opts.prefix) {
fastify.get(opts.prefix, routeOpts, (req, reply) => {
reply.redirect(301, getRedirectUrl(req.raw.url))
})
}
} else {
const indexes = opts.index === undefined ? ['index.html'] : [].concat(opts.index)
const indexDirs = new Map()
const routes = new Set()
const roots = Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]
for (let rootPath of roots) {
rootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
!rootPath.endsWith('/') && (rootPath += '/')
const files = await glob('**/**', {
cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles
})
for (let file of files) {
file = file.split(path.win32.sep).join(path.posix.sep)
const route = prefix + file
if (routes.has(route)) {
continue
}
routes.add(route)
setUpHeadAndGet(routeOpts, route, `/${file}`, rootPath)
const key = path.posix.basename(route)
if (indexes.includes(key) && !indexDirs.has(key)) {
indexDirs.set(path.posix.dirname(route), rootPath)
}
}
}
for (const [dirname, rootPath] of indexDirs.entries()) {
const pathname = dirname + (dirname.endsWith('/') ? '' : '/')
const file = '/' + pathname.replace(prefix, '')
setUpHeadAndGet(routeOpts, pathname, file, rootPath)
if (opts.redirect === true) {
setUpHeadAndGet(routeOpts, pathname.replace(endForwardSlashRegex, ''), file.replace(endForwardSlashRegex, ''), rootPath)
}
}
}
}
const allowedPath = opts.allowedPath
function pumpSendToReply (
request,
reply,
pathname,
rootPath,
rootPathOffset = 0,
pumpOptions,
checkedEncodings
) {
const pathnameOrig = pathname
const options = Object.assign({}, sendOptions, pumpOptions)
if (rootPath) {
if (Array.isArray(rootPath)) {
options.root = rootPath[rootPathOffset]
} else {
options.root = rootPath
}
}
if (allowedPath && !allowedPath(pathname, options.root, request)) {
return reply.callNotFound()
}
let encoding
let pathnameForSend = pathname
if (opts.preCompressed) {
/**
* We conditionally create this structure to track our attempts
* at sending pre-compressed assets
*/
if (!checkedEncodings) {
checkedEncodings = new Set()
}
encoding = getEncodingHeader(request.headers, checkedEncodings)
if (encoding) {
if (pathname.endsWith('/')) {
pathname = findIndexFile(pathname, options.root, options.index)
if (!pathname) {
return reply.callNotFound()
}
pathnameForSend = pathnameForSend + pathname + '.' + getEncodingExtension(encoding)
} else {
pathnameForSend = pathname + '.' + getEncodingExtension(encoding)
}
}
}
// `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('')
}
cb()
}
})
wrap.getHeader = reply.getHeader.bind(reply)
wrap.setHeader = reply.header.bind(reply)
wrap.removeHeader = () => {}
wrap.finished = false
Object.defineProperty(wrap, 'filename', {
get () {
return resolvedFilename
}
})
Object.defineProperty(wrap, 'statusCode', {
get () {
return reply.raw.statusCode
},
set (code) {
reply.code(code)
}
})
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)
}
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 (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
)
}
reply.callNotFound()
}
})
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) {
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)) {
dirList.send({
reply,
dir: dirList.path(opts.root, pathname),
options: opts.list,
route: pathname,
prefix,
dotfiles: opts.dotfiles
}).catch((err) => reply.send(err))
return
}
// root paths left to try?
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
}
if (opts.preCompressed && !checkedEncodings.has(encoding)) {
checkedEncodings.add(encoding)
return pumpSendToReply(
request,
reply,
pathnameOrig,
rootPath,
rootPathOffset,
undefined,
checkedEncodings
)
}
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)
})
// we cannot use pump, because send error
// handling is not compatible
stream.pipe(wrap)
}
function setUpHeadAndGet (routeOpts, route, file, rootPath) {
const toSetUp = Object.assign({}, routeOpts, {
method: ['HEAD', 'GET'],
url: route,
handler: serveFileHandler
})
toSetUp.config = toSetUp.config || {}
toSetUp.config.file = file
toSetUp.config.rootPath = rootPath
fastify.route(toSetUp)
}
function serveFileHandler (req, reply) {
// TODO: remove the fallback branch when bump major
/* istanbul ignore next */
const routeConfig = req.routeOptions?.config || req.routeConfig
pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
}
}
function normalizeRoot (root) {
if (root === undefined) {
return root
}
if (root instanceof URL && root.protocol === 'file:') {
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(fileURLToPath(root[i]))
} else {
result.push(root[i])
}
}
return result
}
return root
}
function checkRootPathForErrors (fastify, rootPath) {
if (rootPath === undefined) {
throw new Error('"root" option is required')
}
if (Array.isArray(rootPath)) {
if (!rootPath.length) {
throw new Error('"root" option array requires one or more paths')
}
if (new Set(rootPath).size !== rootPath.length) {
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))
return
}
if (typeof rootPath === 'string') {
return checkPath(fastify, rootPath)
}
throw new Error('"root" option must be a string or array of strings')
}
function checkPath (fastify, rootPath) {
if (typeof rootPath !== 'string') {
throw new Error('"root" option must be a string')
}
if (path.isAbsolute(rootPath) === false) {
throw new Error('"root" option must be an absolute path')
}
let pathStat
try {
pathStat = statSync(rootPath)
} catch (e) {
if (e.code === 'ENOENT') {
fastify.log.warn(`"root" path "${rootPath}" must exist`)
return
}
throw e
}
if (pathStat.isDirectory() === false) {
throw new Error('"root" option must point to a directory')
}
}
function getContentType (path) {
const type = send.mime.getType(path) || send.mime.default_type
if (!send.isUtf8MimeType(type)) {
return type
}
return `${type}; charset=UTF-8`
}
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)
try {
const stats = statSync(p)
return !stats.isDirectory()
} catch {
return false
}
})
}
/* istanbul ignore next */
return false
}
// Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
function getEncodingHeader (headers, checked) {
if (!('accept-encoding' in headers)) return
// consider the no-preference token as gzip for downstream compat
const header = headers['accept-encoding'].toLowerCase().replace(asteriskRegex, 'gzip')
return encodingNegotiator.negotiate(
header,
supportedEncodings.filter((enc) => !checked.has(enc))
)
}
function getEncodingExtension (encoding) {
switch (encoding) {
case 'br':
return 'br'
case 'gzip':
return 'gz'
}
}
function getRedirectUrl (url) {
let i = 0
// we detect how many slash before a valid path
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/')
const parsedPathname = parsed.pathname
return parsedPathname + (parsedPathname[parsedPathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
} 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
}
}
module.exports = fp(fastifyStatic, {
fastify: '4.x',
name: '@fastify/static'
})
module.exports.default = fastifyStatic
module.exports.fastifyStatic = fastifyStatic

211
backend/node_modules/@fastify/static/lib/dirList.js generated vendored Normal file
View File

@@ -0,0 +1,211 @@
'use strict'
const os = require('node:os')
const path = require('node:path')
const fs = require('node:fs/promises')
const fastq = require('fastq')
const fastqConcurrency = Math.max(1, os.cpus().length - 1)
const dirList = {
_getExtendedInfo: async function (dir, info) {
const depth = dir.split(path.sep).length
const files = await fs.readdir(dir)
const worker = async (filename) => {
const filePath = path.join(dir, filename)
let stats
try {
stats = await fs.stat(filePath)
} catch {
return
}
if (stats.isDirectory()) {
info.totalFolderCount++
filePath.split(path.sep).length === depth + 1 && info.folderCount++
await dirList._getExtendedInfo(filePath, info)
} else {
info.totalSize += stats.size
info.totalFileCount++
filePath.split(path.sep).length === depth + 1 && info.fileCount++
info.lastModified = Math.max(info.lastModified, stats.mtimeMs)
}
}
const queue = fastq.promise(worker, fastqConcurrency)
await Promise.all(files.map(filename => queue.push(filename)))
},
/**
* get extended info about a folder
* @param {string} folderPath full path fs dir
* @return {Promise<ExtendedInfo>}
*/
getExtendedInfo: async function (folderPath) {
const info = {
totalSize: 0,
fileCount: 0,
totalFileCount: 0,
folderCount: 0,
totalFolderCount: 0,
lastModified: 0
}
await dirList._getExtendedInfo(folderPath, info)
return info
},
/**
* get files and dirs from dir, or error
* @param {string} dir full path fs dir
* @param {(boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
* @param {string} dotfiles
* note: can't use glob because don't get error on non existing dir
*/
list: async function (dir, options, dotfiles) {
const entries = { dirs: [], files: [] }
let files = await fs.readdir(dir)
if (dotfiles === 'deny' || dotfiles === 'ignore') {
files = files.filter(file => file.charAt(0) !== '.')
}
if (files.length < 1) {
return entries
}
const worker = async (filename) => {
let stats
try {
stats = await fs.stat(path.join(dir, filename))
} catch {
return
}
const entry = { name: filename, stats }
if (stats.isDirectory()) {
if (options.extendedFolderInfo) {
entry.extendedInfo = await dirList.getExtendedInfo(path.join(dir, filename))
}
entries.dirs.push(entry)
} else {
entries.files.push(entry)
}
}
const queue = fastq.promise(worker, fastqConcurrency)
await Promise.all(files.map(filename => queue.push(filename)))
entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
entries.files.sort((a, b) => a.name.localeCompare(b.name))
return entries
},
/**
* send dir list content, or 404 on error
* @param {Fastify.Reply} reply
* @param {string} dir full path fs dir
* @param {(boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
* @param {string} route request route
* @param {string} dotfiles
*/
send: async function ({ reply, dir, options, route, prefix, dotfiles }) {
if (reply.request.query.format === 'html' && typeof options.render !== 'function') {
throw new Error('The `list.render` option must be a function and is required with the URL parameter `format=html`')
}
let entries
try {
entries = await dirList.list(dir, options, dotfiles)
} catch {
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))
reply.send(nameEntries)
} else {
reply.send(entries)
}
return
}
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 entry file or dir name and stats
* @param {string} route request route
* @return {ListFile}
*/
htmlInfo: function (entry, route, prefix, options) {
if (options.names?.includes(path.basename(route))) {
route = path.normalize(path.join(route, '..'))
}
return {
href: encodeURI(path.join(prefix, route, entry.name).replace(/\\/gu, '/')),
name: entry.name,
stats: entry.stats,
extendedInfo: entry.extendedInfo
}
},
/**
* say if the route can be handled by dir list or not
* @param {string} route request route
* @param {(boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
* @return {boolean}
*/
handle: function (route, options) {
return options.names?.includes(path.basename(route)) ||
// match trailing slash
((options.names?.includes('/') && route[route.length - 1] === '/') ?? false)
},
/**
* get path from route and fs root paths, considering trailing slash
* @param {string} root fs root path
* @param {string} route request route
*/
path: function (root, route) {
const _route = route[route.length - 1] === '/'
? route + 'none'
: route
return path.dirname(path.join(root, _route))
},
/**
* validate options
* @return {Error}
*/
validateOptions: function (options) {
if (!options.list) {
return
}
if (Array.isArray(options.root)) {
return new TypeError('multi-root with list option is not supported')
}
if (options.list.format && options.list.format !== 'json' && options.list.format !== 'html') {
return new TypeError('The `list.format` option must be json or html')
}
if (options.list.names && !Array.isArray(options.list.names)) {
return new TypeError('The `list.names` option must be an array')
}
if (options.list.jsonFormat != null && options.list.jsonFormat !== 'names' && options.list.jsonFormat !== 'extended') {
return new TypeError('The `list.jsonFormat` option must be name or extended')
}
if (options.list.format === 'html' && typeof options.list.render !== 'function') {
return new TypeError('The `list.render` option must be a function and is required with html format')
}
}
}
module.exports = dirList

73
backend/node_modules/@fastify/static/package.json generated vendored Normal file
View File

@@ -0,0 +1,73 @@
{
"name": "@fastify/static",
"version": "7.0.4",
"description": "Plugin for serving static files as fast as possible.",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"coverage": "npm run test:unit -- --coverage-report=html",
"lint": "npm run lint:javascript && npm run lint:typescript",
"lint:javascript": "standard | snazzy",
"lint:fix": "standard --fix && npm run lint:typescript -- --fix",
"lint:typescript": "eslint -c .eslintrc.json types/**/*.d.ts types/**/*.test-d.ts",
"test": "npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "tap",
"example": "node example/server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-static.git"
},
"keywords": [
"fastify",
"static"
],
"author": "Tommaso Allevi - @allevo",
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-static/issues"
},
"homepage": "https://github.com/fastify/fastify-static",
"dependencies": {
"@fastify/accept-negotiator": "^1.0.0",
"@fastify/send": "^2.0.0",
"content-disposition": "^0.5.3",
"fastify-plugin": "^4.0.0",
"fastq": "^1.17.0",
"glob": "^10.3.4"
},
"devDependencies": {
"@fastify/compress": "^7.0.0",
"@fastify/pre-commit": "^2.0.2",
"@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"concat-stream": "^2.0.0",
"coveralls": "^3.0.4",
"eslint-plugin-typescript": "^0.14.0",
"fastify": "^4.0.0-rc.2",
"handlebars": "^4.7.6",
"pino": "^8.4.2",
"proxyquire": "^2.1.0",
"simple-get": "^4.0.0",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"tap": "^16.0.0",
"tsd": "^0.31.0",
"typescript": "^5.1.6"
},
"tsd": {
"directory": "test/types"
},
"eslintConfig": {
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,174 @@
'use strict'
/* eslint n/no-deprecated-api: "off" */
const path = require('node:path')
const { test } = require('tap')
const simple = require('simple-get')
const Fastify = require('fastify')
const fastifyStatic = require('../')
test('register /content-type', t => {
t.plan(6)
const pluginOptions = {
root: path.join(__dirname, '/content-type'),
prefix: '/content-type'
}
const fastify = Fastify()
fastify.register(fastifyStatic, pluginOptions)
t.teardown(fastify.close.bind(fastify))
fastify.listen({ port: 0 }, (err) => {
t.error(err)
fastify.server.unref()
t.test('/content-type/index.html', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/index.html'
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'text/html; charset=UTF-8')
})
})
t.test('/content-type/index.css', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/index.css'
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'text/css; charset=UTF-8')
})
})
t.test('/content-type/sample.jpg', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/sample.jpg'
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'image/jpeg')
})
})
t.test('/content-type/test.txt', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/test.txt'
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'text/plain; charset=UTF-8')
})
})
t.test('/content-type/binary', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/binary'
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'application/octet-stream')
})
})
})
})
test('register /content-type preCompressed', t => {
t.plan(6)
const pluginOptions = {
root: path.join(__dirname, '/content-type'),
prefix: '/content-type',
preCompressed: true
}
const fastify = Fastify()
fastify.register(fastifyStatic, pluginOptions)
t.teardown(fastify.close.bind(fastify))
fastify.listen({ port: 0 }, (err) => {
t.error(err)
fastify.server.unref()
t.test('/content-type/index.html', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/index.html',
headers: {
'accept-encoding': 'gzip, deflate, br'
}
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'text/html; charset=UTF-8')
})
})
t.test('/content-type/index.css', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/index.css',
headers: {
'accept-encoding': 'gzip, deflate, br'
}
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'text/css; charset=UTF-8')
})
})
t.test('/content-type/sample.jpg', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/sample.jpg',
headers: {
'accept-encoding': 'gzip, deflate, br'
}
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'image/jpeg')
})
})
t.test('/content-type/test.txt', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/test.txt',
headers: {
'accept-encoding': 'gzip, deflate, br'
}
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'text/plain; charset=UTF-8')
})
})
t.test('/content-type/binary', (t) => {
t.plan(2)
simple.concat({
method: 'GET',
url: 'http://localhost:' + fastify.server.address().port + '/content-type/binary',
headers: {
'accept-encoding': 'gzip, deflate, br'
}
}, (err, response) => {
t.error(err)
t.equal(response.headers['content-type'], 'application/octet-stream')
})
})
})
})

View File

View File

@@ -0,0 +1 @@
<EFBFBD>

View File

View File

@@ -0,0 +1 @@
<EFBFBD>

View File

@@ -0,0 +1,5 @@
<html>
<body>
the body
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

View File

@@ -0,0 +1 @@
<EFBFBD>

View File

@@ -0,0 +1,866 @@
'use strict'
/* eslint n/no-deprecated-api: "off" */
const fs = require('node:fs')
const path = require('node:path')
const t = require('tap')
const simple = require('simple-get')
const Fastify = require('fastify')
const fastifyStatic = require('..')
const dirList = require('../lib/dirList')
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(mock, options)
t.teardown(fastify.close.bind(fastify))
fastify.listen({ port: 0 }, err => {
t.error(err)
fastify.server.unref()
f('http://localhost:' + fastify.server.address().port)
})
return f
}
}
try {
fs.mkdirSync(path.join(__dirname, 'static/shallow/empty'))
} catch (error) {}
t.test('throws when `root` is an array', t => {
t.plan(2)
const err = dirList.validateOptions({ root: ['hello', 'world'], list: true })
t.type(err, TypeError)
t.equal(err.message, 'multi-root with list option is not supported')
})
t.test('throws when `list.format` option is invalid', t => {
t.plan(2)
const err = dirList.validateOptions({ list: { format: 'hello' } })
t.type(err, TypeError)
t.equal(err.message, 'The `list.format` option must be json or html')
})
t.test('throws when `list.names option` is not an array', t => {
t.plan(2)
const err = dirList.validateOptions({ list: { names: 'hello' } })
t.type(err, TypeError)
t.equal(err.message, 'The `list.names` option must be an array')
})
t.test('throws when `list.jsonFormat` option is invalid', t => {
t.plan(2)
const err = dirList.validateOptions({ list: { jsonFormat: 'hello' } })
t.type(err, TypeError)
t.equal(err.message, 'The `list.jsonFormat` option must be name or extended')
})
t.test('throws when `list.format` is html and `list render` is not a function', t => {
t.plan(2)
const err = dirList.validateOptions({ list: { format: 'html', render: 'hello' } })
t.type(err, TypeError)
t.equal(err.message, 'The `list.render` option must be a function and is required with html format')
})
t.test('dir list wrong options', t => {
t.plan(3)
const cases = [
{
options: {
root: path.join(__dirname, '/static'),
prefix: '/public',
list: {
format: 'no-json,no-html'
}
},
error: new TypeError('The `list.format` option must be json or html')
},
{
options: {
root: path.join(__dirname, '/static'),
list: {
format: 'html'
// no render function
}
},
error: new TypeError('The `list.render` option must be a function and is required with html format')
},
{
options: {
root: path.join(__dirname, '/static'),
list: {
names: 'not-an-array'
}
},
error: new TypeError('The `list.names` option must be an array')
}
]
for (const case_ of cases) {
const fastify = Fastify()
fastify.register(fastifyStatic, case_.options)
fastify.listen({ port: 0 }, err => {
t.equal(err.message, case_.error.message)
fastify.server.unref()
})
}
})
t.test('dir list default options', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
list: true
}
const route = '/public/shallow'
const content = { dirs: ['empty'], files: ['sample.jpg'] }
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
})
})
t.test('dir list, custom options', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: true
}
const route = '/public/'
const content = { dirs: ['deep', 'shallow'], files: ['.example', '100%.txt', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
})
})
t.test('dir list html format', t => {
t.plan(6)
// render html in 2 ways: one with handlebars and one with template string
const Handlebars = require('handlebars')
const source = `
<html><body>
<ul>
{{#dirs}}
<li><a href="{{href}}">{{name}}</a></li>
{{/dirs}}
</ul>
<ul>
{{#files}}
<li><a href="{{href}}" target="_blank">{{name}}</a></li>
{{/files}}
</ul>
</body></html>
`
const handlebarTemplate = Handlebars.compile(source)
const templates = [
{
render: (dirs, files) => {
return handlebarTemplate({ dirs, files })
},
output: `
<html><body>
<ul>
<li><a href="/public/deep">deep</a></li>
<li><a href="/public/shallow">shallow</a></li>
</ul>
<ul>
<li><a href="/public/.example" target="_blank">.example</a></li>
<li><a href="/public/100%25.txt" target="_blank">100%.txt</a></li>
<li><a href="/public/a%20.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>
`
},
{
render: (dirs, files) => {
return `
<html><body>
<ul>
${dirs.map(dir => `<li><a href="${dir.href}">${dir.name}</a></li>`).join('\n ')}
</ul>
<ul>
${files.map(file => `<li><a href="${file.href}" target="_blank">${file.name}</a></li>`).join('\n ')}
</ul>
</body></html>
`
},
output: `
<html><body>
<ul>
<li><a href="/public/deep">deep</a></li>
<li><a href="/public/shallow">shallow</a></li>
</ul>
<ul>
<li><a href="/public/.example" target="_blank">.example</a></li>
<li><a href="/public/100%25.txt" target="_blank">100%.txt</a></li>
<li><a href="/public/a%20.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>
`
}
]
for (const template of templates) {
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'html',
names: ['index', 'index.htm'],
render: template.render
}
}
const routes = ['/public/index.htm', '/public/index']
// check all routes by names
helper.arrange(t, options, (url) => {
for (const route of routes) {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), template.output)
})
})
}
})
}
})
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)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
prefixAvoidTrailingSlash: true,
list: {
format: 'json',
names: ['index', 'index.json', '/']
}
}
const routes = ['/public/shallow/']
const content = { dirs: ['empty'], files: ['sample.jpg'] }
helper.arrange(t, options, (url) => {
for (const route of routes) {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
}
})
})
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('json format with url parameter format', t => {
t.plan(13)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'json',
render (dirs, files) {
return 'html'
}
}
}
const route = '/public/'
const jsonContent = { dirs: ['deep', 'shallow'], files: ['.example', '100%.txt', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
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(), JSON.stringify(jsonContent))
t.ok(response.headers['content-type'].includes('application/json'))
})
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.equal(body.toString(), JSON.stringify(jsonContent))
t.ok(response.headers['content-type'].includes('application/json'))
})
})
})
t.test('json format with url parameter format and without render option', t => {
t.plan(12)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'json'
}
}
const route = '/public/'
const jsonContent = { dirs: ['deep', 'shallow'], files: ['.example', '100%.txt', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
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(), JSON.stringify(jsonContent))
t.ok(response.headers['content-type'].includes('application/json'))
})
simple.concat({
method: 'GET',
url: url + route + '?format=html'
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 500)
t.equal(JSON.parse(body.toString()).message, 'The `list.render` option must be a function and is required with the URL parameter `format=html`')
})
simple.concat({
method: 'GET',
url: url + route + '?format=json'
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(jsonContent))
t.ok(response.headers['content-type'].includes('application/json'))
})
})
})
t.test('html format with 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/'
const jsonContent = { dirs: ['deep', 'shallow'], files: ['.example', '100%.txt', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
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.equal(body.toString(), JSON.stringify(jsonContent))
t.ok(response.headers['content-type'].includes('application/json'))
})
})
})
t.test('dir list on empty dir', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
list: true
}
const route = '/public/shallow/empty'
const content = { dirs: [], files: [] }
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
})
})
t.test('dir list serve index.html on index option', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
index: false,
list: {
format: 'html',
names: ['index', 'index.html'],
render: () => 'dir list index'
}
}
helper.arrange(t, options, (url) => {
t.test('serve index.html from fs', t => {
t.plan(6)
let route = '/public/index.html'
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), '<html>\n <body>\n the body\n </body>\n</html>\n')
})
route = '/public/index'
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), 'dir list index')
})
})
})
})
t.test('serve a non existent dir and get error', t => {
t.plan(2)
const options = {
root: '/none',
prefix: '/public',
list: true
}
const route = '/public/'
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(2)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 404)
})
})
})
})
t.test('serve a non existent dir and get error', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
list: {
names: ['index']
}
}
const route = '/public/none/index'
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(2)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 404)
})
})
})
})
t.test('dir list with dotfiles allow option', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static-dotfiles'),
prefix: '/public',
dotfiles: 'allow',
index: false,
list: true
}
const route = '/public/'
const content = { dirs: ['dir'], files: ['.aaa', 'test.txt'] }
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
})
})
t.test('dir list with dotfiles deny option', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static-dotfiles'),
prefix: '/public',
dotfiles: 'deny',
index: false,
list: true
}
const route = '/public/'
const content = { dirs: ['dir'], files: ['test.txt'] }
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
})
})
t.test('dir list with dotfiles ignore option', t => {
t.plan(2)
const options = {
root: path.join(__dirname, '/static-dotfiles'),
prefix: '/public',
dotfiles: 'ignore',
index: false,
list: true
}
const route = '/public/'
const content = { dirs: ['dir'], files: ['test.txt'] }
helper.arrange(t, options, (url) => {
t.test(route, t => {
t.plan(3)
simple.concat({
method: 'GET',
url: url + route
}, (err, response, body) => {
t.error(err)
t.equal(response.statusCode, 200)
t.equal(body.toString(), JSON.stringify(content))
})
})
})
})
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'
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)
})
}
})
})

View File

View File

View File

@@ -0,0 +1 @@
example

View File

@@ -0,0 +1 @@
{"hello": "world"}

View File

@@ -0,0 +1,5 @@
<html>
<body>
the body
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
<html>
<body>
dir-gz index
</body>
</html>

View File

@@ -0,0 +1,5 @@
<html>
<body>
dir index
</body>
</html>

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<html>
<body>foo</body>
</html>

Binary file not shown.

View File

@@ -0,0 +1,5 @@
<html>
<body>
index
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<html>
<body>foobar</body>
</html>

View File

@@ -0,0 +1,3 @@
<html>
<body>index</body>
</html>

4025
backend/node_modules/@fastify/static/test/static.test.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
contents of a dotfile

View File

@@ -0,0 +1 @@
100%

View File

@@ -0,0 +1 @@
example

View File

@@ -0,0 +1,5 @@
<html>
<body>
inner index.html
</body>
</html>

View File

@@ -0,0 +1,5 @@
<html>
<body>
the deep path for test purpose body
</body>
</html>

View File

@@ -0,0 +1,3 @@
<html>
<body>foo</body>
</html>

View File

@@ -0,0 +1,3 @@
<html>
<body>foobar</body>
</html>

View File

View File

@@ -0,0 +1,5 @@
<html>
<body>
the body
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,3 @@
<html>
<body>bar</body>
</html>

View File

@@ -0,0 +1,3 @@
<html>
<body>index2</body>
</html>

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["ES2018"],
"module": "commonjs",
"noEmit": true,
"strict": true
},
"include": ["types/*.test-d.ts", "types/*.d.ts"]
}

124
backend/node_modules/@fastify/static/types/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,124 @@
// Definitions by: Jannik <https://github.com/jannikkeye>
// Leo <https://github.com/leomelzer>
/// <reference types="node" />
import { FastifyPluginAsync, FastifyReply, FastifyRequest, RouteOptions } from 'fastify'
import { Stats } from 'fs'
declare module 'fastify' {
interface FastifyReply {
sendFile(filename: string, rootPath?: string): FastifyReply;
sendFile(filename: string, options?: fastifyStatic.SendOptions): FastifyReply;
sendFile(filename: string, rootPath?: string, options?: fastifyStatic.SendOptions): FastifyReply;
download(filepath: string, options?: fastifyStatic.SendOptions): FastifyReply;
download(filepath: string, filename?: string): FastifyReply;
download(filepath: string, filename?: string, options?: fastifyStatic.SendOptions): FastifyReply;
}
}
type FastifyStaticPlugin = FastifyPluginAsync<NonNullable<fastifyStatic.FastifyStaticOptions>>;
declare namespace fastifyStatic {
export interface SetHeadersResponse {
getHeader: FastifyReply['getHeader'];
setHeader: FastifyReply['header'];
readonly filename: string;
statusCode: number;
}
export interface ExtendedInformation {
fileCount: number;
totalFileCount: number;
folderCount: number;
totalFolderCount: number;
totalSize: number;
lastModified: number;
}
export interface ListDir {
href: string;
name: string;
stats: Stats;
extendedInfo?: ExtendedInformation;
}
export interface ListFile {
href: string;
name: string;
stats: Stats;
}
export interface ListRender {
(dirs: ListDir[], files: ListFile[]): string;
}
export interface ListOptions {
names?: string[];
extendedFolderInfo?: boolean;
jsonFormat?: 'names' | 'extended';
}
export interface ListOptionsJsonFormat extends ListOptions {
format: 'json';
// Required when the URL parameter `format=html` exists
render?: ListRender;
}
export interface ListOptionsHtmlFormat extends ListOptions {
format: 'html';
render: ListRender;
}
// Passed on to `send`
export interface SendOptions {
acceptRanges?: boolean;
cacheControl?: boolean;
dotfiles?: 'allow' | 'deny' | 'ignore';
etag?: boolean;
extensions?: string[];
immutable?: boolean;
index?: string[] | string | false;
lastModified?: boolean;
maxAge?: string | number;
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;
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;
cacheControl?: boolean;
dotfiles?: 'allow' | 'deny' | 'ignore';
etag?: boolean;
extensions?: string[];
immutable?: boolean;
index?: string[] | string | false;
lastModified?: boolean;
maxAge?: string | number;
constraints?: RouteOptions['constraints'];
}
export const fastifyStatic: FastifyStaticPlugin
export { fastifyStatic as default }
}
declare function fastifyStatic(...params: Parameters<FastifyStaticPlugin>): ReturnType<FastifyStaticPlugin>;
export = fastifyStatic;

View File

@@ -0,0 +1,212 @@
import fastify, { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'
import { Server } from 'http'
import { Stats } from 'fs'
import { expectAssignable, expectError, expectType } from 'tsd'
import * as fastifyStaticStar from '..'
import fastifyStatic, {
FastifyStaticOptions,
fastifyStatic as fastifyStaticNamed
} from '..'
import fastifyStaticCjsImport = require('..');
const fastifyStaticCjs = require('..')
const app: FastifyInstance = fastify()
app.register(fastifyStatic)
app.register(fastifyStaticNamed)
app.register(fastifyStaticCjs)
app.register(fastifyStaticCjsImport.default)
app.register(fastifyStaticCjsImport.fastifyStatic)
app.register(fastifyStaticStar.default)
app.register(fastifyStaticStar.fastifyStatic)
expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStatic)
expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticNamed)
expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.default)
expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.fastifyStatic)
expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticStar.default)
expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(
fastifyStaticStar.fastifyStatic
)
expectType<any>(fastifyStaticCjs)
const appWithImplicitHttp = fastify()
const options: FastifyStaticOptions = {
acceptRanges: true,
cacheControl: true,
decorateReply: true,
dotfiles: 'allow',
etag: true,
extensions: ['.js'],
immutable: true,
index: ['1'],
lastModified: true,
maxAge: '',
prefix: '',
prefixAvoidTrailingSlash: false,
root: '',
schemaHide: true,
serve: true,
wildcard: true,
list: false,
setHeaders: (res, path, stat) => {
expectType<string>(res.filename)
expectType<number>(res.statusCode)
expectType<ReturnType<FastifyReply['getHeader']>>(res.getHeader('X-Test'))
res.setHeader('X-Test', 'string')
expectType<string>(path)
expectType<Stats>(stat)
},
preCompressed: false,
allowedPath: (pathName: string, root: string, request: FastifyRequest) => {
return true
},
constraints: {
host: /.*\.example\.com/,
version: '1.0.2'
}
}
expectError<FastifyStaticOptions>({
root: '',
wildcard: '**/**'
})
expectAssignable<FastifyStaticOptions>({
root: '',
list: {
format: 'json'
}
})
expectAssignable<FastifyStaticOptions>({
root: '',
list: {
format: 'json',
render: () => ''
}
})
expectAssignable<FastifyStaticOptions>({
root: '',
list: {
format: 'html',
render: () => ''
}
})
expectError<FastifyStaticOptions>({
root: '',
list: {
format: 'html'
}
})
expectAssignable<FastifyStaticOptions>({
root: ['']
})
expectAssignable<FastifyStaticOptions>({
root: new URL('')
})
expectAssignable<FastifyStaticOptions>({
root: [new URL('')]
})
appWithImplicitHttp
.register(fastifyStatic, options)
.after(() => {
appWithImplicitHttp.get('/', (request, reply) => {
reply.sendFile('some-file-name')
})
})
const appWithHttp2 = fastify({ http2: true })
appWithHttp2
.register(fastifyStatic, options)
.after(() => {
appWithHttp2.get('/', (request, reply) => {
reply.sendFile('some-file-name')
})
appWithHttp2.get('/download', (request, reply) => {
reply.download('some-file-name')
})
appWithHttp2.get('/download/1', (request, reply) => {
reply.download('some-file-name', { maxAge: '2 days' })
})
appWithHttp2.get('/download/2', (request, reply) => {
reply.download('some-file-name', 'some-filename', { cacheControl: false, acceptRanges: true })
})
})
const multiRootAppWithImplicitHttp = fastify()
options.root = ['']
multiRootAppWithImplicitHttp
.register(fastifyStatic, options)
.after(() => {
multiRootAppWithImplicitHttp.get('/', (request, reply) => {
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')
})
multiRootAppWithImplicitHttp.get('/download/1', (request, reply) => {
reply.download('some-file-name', { maxAge: '2 days' })
})
multiRootAppWithImplicitHttp.get('/download/2', (request, reply) => {
reply.download('some-file-name', 'some-filename', { cacheControl: false, acceptRanges: true })
})
})
const noIndexApp = fastify()
options.root = ''
options.index = false
noIndexApp
.register(fastifyStatic, options)
.after(() => {
noIndexApp.get('/', (request, reply) => {
reply.send('<h1>fastify-static</h1>')
})
})
options.root = new URL('')
const URLRootApp = fastify()
URLRootApp.register(fastifyStatic, options)
.after(() => {
URLRootApp.get('/', (request, reply) => {
reply.send('<h1>fastify-static</h1>')
})
})
const defaultIndexApp = fastify()
options.index = 'index.html'
defaultIndexApp
.register(fastifyStatic, options)
.after(() => {
defaultIndexApp.get('/', (request, reply) => {
reply.send('<h1>fastify-static</h1>')
})
})