@fastify/static 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +5 -0
- package/.github/dependabot.yml +13 -0
- package/.github/stale.yml +21 -0
- package/.github/workflows/ci.yml +46 -0
- package/LICENSE +21 -0
- package/README.md +440 -0
- package/example/public/images/sample.jpg +0 -0
- package/example/public/index.css +9 -0
- package/example/public/index.html +11 -0
- package/example/public/index.js +8 -0
- package/example/public2/test.css +4 -0
- package/example/public2/test.html +8 -0
- package/example/server-compress.js +15 -0
- package/example/server-dir-list.js +49 -0
- package/example/server.js +13 -0
- package/index.d.ts +98 -0
- package/index.js +509 -0
- package/lib/dirList.js +199 -0
- package/package.json +71 -0
- package/test/dir-list.test.js +675 -0
- package/test/static/.example +1 -0
- package/test/static/a .md +1 -0
- package/test/static/deep/path/for/test/index.html +5 -0
- package/test/static/deep/path/for/test/purpose/foo.html +5 -0
- package/test/static/foo.html +3 -0
- package/test/static/foobar.html +3 -0
- package/test/static/index.css +0 -0
- package/test/static/index.html +5 -0
- package/test/static/shallow/sample.jpg +0 -0
- package/test/static-pre-compressed/all-three.html +5 -0
- package/test/static-pre-compressed/all-three.html.br +0 -0
- package/test/static-pre-compressed/all-three.html.gz +0 -0
- package/test/static-pre-compressed/dir/index.html +5 -0
- package/test/static-pre-compressed/dir/index.html.br +0 -0
- package/test/static-pre-compressed/empty/.gitkeep +0 -0
- package/test/static-pre-compressed/gzip-only.html +3 -0
- package/test/static-pre-compressed/gzip-only.html.gz +0 -0
- package/test/static-pre-compressed/index.html +5 -0
- package/test/static-pre-compressed/index.html.br +0 -0
- package/test/static-pre-compressed/sample.jpg +0 -0
- package/test/static-pre-compressed/sample.jpg.br +0 -0
- package/test/static-pre-compressed/uncompressed.html +3 -0
- package/test/static.test.js +3482 -0
- package/test/static2/bar.html +3 -0
- package/test/static2/index.html +3 -0
- package/test/types/index.ts +105 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Definitions by: Jannik <https://github.com/jannikkeye>
|
|
2
|
+
// Leo <https://github.com/leomelzer>
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
|
|
5
|
+
import { FastifyPluginCallback, FastifyReply } from 'fastify';
|
|
6
|
+
import { Stats } from 'fs';
|
|
7
|
+
|
|
8
|
+
declare module "fastify" {
|
|
9
|
+
interface FastifyReply {
|
|
10
|
+
sendFile(filename: string, rootPath?: string): FastifyReply;
|
|
11
|
+
sendFile(filename: string, options?: SendOptions): FastifyReply;
|
|
12
|
+
sendFile(filename: string, rootPath?: string, options?: SendOptions): FastifyReply;
|
|
13
|
+
download(filepath: string, options?: SendOptions): FastifyReply;
|
|
14
|
+
download(filepath: string, filename?: string): FastifyReply;
|
|
15
|
+
download(filepath: string, filename?: string, options?: SendOptions): FastifyReply;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ExtendedInformation {
|
|
20
|
+
fileCount: number;
|
|
21
|
+
totalFileCount: number;
|
|
22
|
+
folderCount: number;
|
|
23
|
+
totalFolderCount: number;
|
|
24
|
+
totalSize: number;
|
|
25
|
+
lastModified: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ListDir {
|
|
29
|
+
href: string;
|
|
30
|
+
name: string;
|
|
31
|
+
stats: Stats;
|
|
32
|
+
extendedInfo?: ExtendedInformation;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ListFile {
|
|
36
|
+
href: string;
|
|
37
|
+
name: string;
|
|
38
|
+
stats: Stats;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ListRender {
|
|
42
|
+
(dirs: ListDir[], files: ListFile[]): string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ListOptions {
|
|
46
|
+
format: 'json' | 'html';
|
|
47
|
+
names: string[];
|
|
48
|
+
render: ListRender;
|
|
49
|
+
extendedFolderInfo?: boolean;
|
|
50
|
+
jsonFormat?: 'names' | 'extended';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Passed on to `send`
|
|
54
|
+
interface SendOptions {
|
|
55
|
+
acceptRanges?: boolean;
|
|
56
|
+
cacheControl?: boolean;
|
|
57
|
+
dotfiles?: 'allow' | 'deny' | 'ignore';
|
|
58
|
+
etag?: boolean;
|
|
59
|
+
extensions?: string[];
|
|
60
|
+
immutable?: boolean;
|
|
61
|
+
index?: string[] | false;
|
|
62
|
+
lastModified?: boolean;
|
|
63
|
+
maxAge?: string | number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FastifyStaticOptions extends SendOptions {
|
|
67
|
+
root: string | string[];
|
|
68
|
+
prefix?: string;
|
|
69
|
+
prefixAvoidTrailingSlash?: boolean;
|
|
70
|
+
serve?: boolean;
|
|
71
|
+
decorateReply?: boolean;
|
|
72
|
+
schemaHide?: boolean;
|
|
73
|
+
setHeaders?: (...args: any[]) => void;
|
|
74
|
+
redirect?: boolean;
|
|
75
|
+
wildcard?: boolean;
|
|
76
|
+
list?: boolean | ListOptions;
|
|
77
|
+
allowedPath?: (pathName: string, root?: string) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* @description
|
|
80
|
+
* Opt-in to looking for pre-compressed files
|
|
81
|
+
*/
|
|
82
|
+
preCompressed?: boolean;
|
|
83
|
+
|
|
84
|
+
// Passed on to `send`
|
|
85
|
+
acceptRanges?: boolean;
|
|
86
|
+
cacheControl?: boolean;
|
|
87
|
+
dotfiles?: 'allow' | 'deny' | 'ignore';
|
|
88
|
+
etag?: boolean;
|
|
89
|
+
extensions?: string[];
|
|
90
|
+
immutable?: boolean;
|
|
91
|
+
index?: string[] | false;
|
|
92
|
+
lastModified?: boolean;
|
|
93
|
+
maxAge?: string | number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
declare const fastifyStatic: FastifyPluginCallback<FastifyStaticOptions>
|
|
97
|
+
|
|
98
|
+
export default fastifyStatic;
|
package/index.js
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const statSync = require('fs').statSync
|
|
5
|
+
const { PassThrough } = require('readable-stream')
|
|
6
|
+
const glob = require('glob')
|
|
7
|
+
const send = require('send')
|
|
8
|
+
const contentDisposition = require('content-disposition')
|
|
9
|
+
const fp = require('fastify-plugin')
|
|
10
|
+
const util = require('util')
|
|
11
|
+
const globPromise = util.promisify(glob)
|
|
12
|
+
const encodingNegotiator = require('encoding-negotiator')
|
|
13
|
+
|
|
14
|
+
const dirList = require('./lib/dirList')
|
|
15
|
+
|
|
16
|
+
async function fastifyStatic (fastify, opts) {
|
|
17
|
+
checkRootPathForErrors(fastify, opts.root)
|
|
18
|
+
|
|
19
|
+
const setHeaders = opts.setHeaders
|
|
20
|
+
|
|
21
|
+
if (setHeaders !== undefined && typeof setHeaders !== 'function') {
|
|
22
|
+
throw new TypeError('The `setHeaders` option must be a function')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const invalidDirListOpts = dirList.validateOptions(opts)
|
|
26
|
+
if (invalidDirListOpts) {
|
|
27
|
+
throw invalidDirListOpts
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sendOptions = {
|
|
31
|
+
root: opts.root,
|
|
32
|
+
acceptRanges: opts.acceptRanges,
|
|
33
|
+
cacheControl: opts.cacheControl,
|
|
34
|
+
dotfiles: opts.dotfiles,
|
|
35
|
+
etag: opts.etag,
|
|
36
|
+
extensions: opts.extensions,
|
|
37
|
+
immutable: opts.immutable,
|
|
38
|
+
index: opts.index,
|
|
39
|
+
lastModified: opts.lastModified,
|
|
40
|
+
maxAge: opts.maxAge
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allowedPath = opts.allowedPath
|
|
44
|
+
|
|
45
|
+
if (opts.prefix === undefined) opts.prefix = '/'
|
|
46
|
+
|
|
47
|
+
let prefix = opts.prefix
|
|
48
|
+
|
|
49
|
+
if (!opts.prefixAvoidTrailingSlash) {
|
|
50
|
+
prefix =
|
|
51
|
+
opts.prefix[opts.prefix.length - 1] === '/'
|
|
52
|
+
? opts.prefix
|
|
53
|
+
: opts.prefix + '/'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pumpSendToReply (
|
|
57
|
+
request,
|
|
58
|
+
reply,
|
|
59
|
+
pathname,
|
|
60
|
+
rootPath,
|
|
61
|
+
rootPathOffset = 0,
|
|
62
|
+
pumpOptions = {},
|
|
63
|
+
checkedEncodings
|
|
64
|
+
) {
|
|
65
|
+
const options = Object.assign({}, sendOptions, pumpOptions)
|
|
66
|
+
|
|
67
|
+
if (rootPath) {
|
|
68
|
+
if (Array.isArray(rootPath)) {
|
|
69
|
+
options.root = rootPath[rootPathOffset]
|
|
70
|
+
} else {
|
|
71
|
+
options.root = rootPath
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (allowedPath && !allowedPath(pathname, options.root)) {
|
|
76
|
+
return reply.callNotFound()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let encoding
|
|
80
|
+
let pathnameForSend = pathname
|
|
81
|
+
|
|
82
|
+
if (opts.preCompressed) {
|
|
83
|
+
/**
|
|
84
|
+
* We conditionally create this structure to track our attempts
|
|
85
|
+
* at sending pre-compressed assets
|
|
86
|
+
*/
|
|
87
|
+
if (!checkedEncodings) {
|
|
88
|
+
checkedEncodings = new Set()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
encoding = getEncodingHeader(request.headers, checkedEncodings)
|
|
92
|
+
|
|
93
|
+
if (encoding) {
|
|
94
|
+
if (pathname.endsWith('/')) {
|
|
95
|
+
pathname = findIndexFile(pathname, options.root, options.index)
|
|
96
|
+
if (!pathname) {
|
|
97
|
+
return reply.callNotFound()
|
|
98
|
+
}
|
|
99
|
+
pathnameForSend = pathnameForSend + pathname + '.' + getEncodingExtension(encoding)
|
|
100
|
+
} else {
|
|
101
|
+
pathnameForSend = pathname + '.' + getEncodingExtension(encoding)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stream = send(request.raw, pathnameForSend, options)
|
|
107
|
+
let resolvedFilename
|
|
108
|
+
stream.on('file', function (file) {
|
|
109
|
+
resolvedFilename = file
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const wrap = new PassThrough({
|
|
113
|
+
flush (cb) {
|
|
114
|
+
this.finished = true
|
|
115
|
+
if (reply.raw.statusCode === 304) {
|
|
116
|
+
reply.send('')
|
|
117
|
+
}
|
|
118
|
+
cb()
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
wrap.getHeader = reply.getHeader.bind(reply)
|
|
123
|
+
wrap.setHeader = reply.header.bind(reply)
|
|
124
|
+
wrap.finished = false
|
|
125
|
+
|
|
126
|
+
Object.defineProperty(wrap, 'filename', {
|
|
127
|
+
get () {
|
|
128
|
+
return resolvedFilename
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
Object.defineProperty(wrap, 'statusCode', {
|
|
132
|
+
get () {
|
|
133
|
+
return reply.raw.statusCode
|
|
134
|
+
},
|
|
135
|
+
set (code) {
|
|
136
|
+
reply.code(code)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (request.method === 'HEAD') {
|
|
141
|
+
wrap.on('finish', reply.send.bind(reply))
|
|
142
|
+
} else {
|
|
143
|
+
wrap.on('pipe', function () {
|
|
144
|
+
if (encoding) {
|
|
145
|
+
reply.header('content-type', getContentType(pathname))
|
|
146
|
+
reply.header('content-encoding', encoding)
|
|
147
|
+
}
|
|
148
|
+
reply.send(wrap)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (setHeaders !== undefined) {
|
|
153
|
+
stream.on('headers', setHeaders)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
stream.on('directory', function (_, path) {
|
|
157
|
+
if (opts.list) {
|
|
158
|
+
dirList.send({
|
|
159
|
+
reply,
|
|
160
|
+
dir: path,
|
|
161
|
+
options: opts.list,
|
|
162
|
+
route: pathname,
|
|
163
|
+
prefix
|
|
164
|
+
}).catch((err) => reply.send(err))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (opts.redirect === true) {
|
|
169
|
+
try {
|
|
170
|
+
reply.redirect(301, getRedirectUrl(request.raw.url))
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
|
|
173
|
+
/* istanbul ignore next */
|
|
174
|
+
reply.send(error)
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// if is a directory path without a trailing slash, and has an index file, reply as if it has a trailing slash
|
|
178
|
+
if (!pathname.endsWith('/') && findIndexFile(pathname, options.root, options.index)) {
|
|
179
|
+
return pumpSendToReply(
|
|
180
|
+
request,
|
|
181
|
+
reply,
|
|
182
|
+
pathname + '/',
|
|
183
|
+
rootPath,
|
|
184
|
+
undefined,
|
|
185
|
+
undefined,
|
|
186
|
+
checkedEncodings
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
reply.callNotFound()
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
stream.on('error', function (err) {
|
|
195
|
+
if (err.code === 'ENOENT') {
|
|
196
|
+
// when preCompress is enabled and the path is a directoy without a trailing shash
|
|
197
|
+
if (opts.preCompressed && encoding) {
|
|
198
|
+
const indexPathname = findIndexFile(pathname, options.root, options.index)
|
|
199
|
+
if (indexPathname) {
|
|
200
|
+
return pumpSendToReply(
|
|
201
|
+
request,
|
|
202
|
+
reply,
|
|
203
|
+
pathname + '/',
|
|
204
|
+
rootPath,
|
|
205
|
+
undefined,
|
|
206
|
+
undefined,
|
|
207
|
+
checkedEncodings
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// if file exists, send real file, otherwise send dir list if name match
|
|
213
|
+
if (opts.list && dirList.handle(pathname, opts.list)) {
|
|
214
|
+
dirList.send({
|
|
215
|
+
reply,
|
|
216
|
+
dir: dirList.path(opts.root, pathname),
|
|
217
|
+
options: opts.list,
|
|
218
|
+
route: pathname,
|
|
219
|
+
prefix
|
|
220
|
+
}).catch((err) => reply.send(err))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// root paths left to try?
|
|
225
|
+
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
|
|
226
|
+
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (opts.preCompressed && !checkedEncodings.has(encoding)) {
|
|
230
|
+
checkedEncodings.add(encoding)
|
|
231
|
+
return pumpSendToReply(
|
|
232
|
+
request,
|
|
233
|
+
reply,
|
|
234
|
+
pathname,
|
|
235
|
+
rootPath,
|
|
236
|
+
undefined,
|
|
237
|
+
undefined,
|
|
238
|
+
checkedEncodings
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return reply.callNotFound()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The `send` library terminates the request with a 404 if the requested
|
|
246
|
+
// path contains a dotfile and `send` is initialized with `{dotfiles:
|
|
247
|
+
// 'ignore'}`. `send` aborts the request before getting far enough to
|
|
248
|
+
// check if the file exists (hence, a 404 `NotFoundError` instead of
|
|
249
|
+
// `ENOENT`).
|
|
250
|
+
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L582
|
|
251
|
+
if (err.status === 404) {
|
|
252
|
+
return reply.callNotFound()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
reply.send(err)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// we cannot use pump, because send error
|
|
259
|
+
// handling is not compatible
|
|
260
|
+
stream.pipe(wrap)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const errorHandler = (error, request, reply) => {
|
|
264
|
+
if (error && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
265
|
+
reply.request.raw.destroy()
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fastify.errorHandler(error, request, reply)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Set the schema hide property if defined in opts or true by default
|
|
273
|
+
const routeOpts = {
|
|
274
|
+
schema: {
|
|
275
|
+
hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true
|
|
276
|
+
},
|
|
277
|
+
errorHandler: fastify.errorHandler ? errorHandler : undefined
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (opts.decorateReply !== false) {
|
|
281
|
+
fastify.decorateReply('sendFile', function (filePath, rootPath, options) {
|
|
282
|
+
const opts = typeof rootPath === 'object' ? rootPath : options
|
|
283
|
+
const root = typeof rootPath === 'string' ? rootPath : opts && opts.root
|
|
284
|
+
pumpSendToReply(
|
|
285
|
+
this.request,
|
|
286
|
+
this,
|
|
287
|
+
filePath,
|
|
288
|
+
root || sendOptions.root,
|
|
289
|
+
0,
|
|
290
|
+
opts
|
|
291
|
+
)
|
|
292
|
+
return this
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
fastify.decorateReply(
|
|
296
|
+
'download',
|
|
297
|
+
function (filePath, fileName, options = {}) {
|
|
298
|
+
const { root, ...opts } =
|
|
299
|
+
typeof fileName === 'object' ? fileName : options
|
|
300
|
+
fileName = typeof fileName === 'string' ? fileName : filePath
|
|
301
|
+
|
|
302
|
+
// Set content disposition header
|
|
303
|
+
this.header('content-disposition', contentDisposition(fileName))
|
|
304
|
+
|
|
305
|
+
pumpSendToReply(this.request, this, filePath, root, 0, opts)
|
|
306
|
+
|
|
307
|
+
return this
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (opts.serve !== false) {
|
|
313
|
+
if (opts.wildcard && typeof opts.wildcard !== 'boolean') {
|
|
314
|
+
throw new Error('"wildcard" option must be a boolean')
|
|
315
|
+
}
|
|
316
|
+
if (opts.wildcard === undefined || opts.wildcard === true) {
|
|
317
|
+
fastify.head(prefix + '*', routeOpts, function (req, reply) {
|
|
318
|
+
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
|
|
319
|
+
})
|
|
320
|
+
fastify.get(prefix + '*', routeOpts, function (req, reply) {
|
|
321
|
+
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
|
|
322
|
+
})
|
|
323
|
+
if (opts.redirect === true && prefix !== opts.prefix) {
|
|
324
|
+
fastify.get(opts.prefix, routeOpts, function (req, reply) {
|
|
325
|
+
reply.redirect(301, getRedirectUrl(req.raw.url))
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
const globPattern = '**/*'
|
|
330
|
+
const indexDirs = new Map()
|
|
331
|
+
const routes = new Set()
|
|
332
|
+
|
|
333
|
+
for (const rootPath of Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]) {
|
|
334
|
+
const files = await globPromise(path.join(rootPath, globPattern), { nodir: true })
|
|
335
|
+
const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index)
|
|
336
|
+
|
|
337
|
+
for (let file of files) {
|
|
338
|
+
file = file
|
|
339
|
+
.replace(rootPath.replace(/\\/g, '/'), '')
|
|
340
|
+
.replace(/^\//, '')
|
|
341
|
+
const route = encodeURI(prefix + file).replace(/\/\//g, '/')
|
|
342
|
+
if (routes.has(route)) {
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
routes.add(route)
|
|
346
|
+
fastify.head(route, routeOpts, function (req, reply) {
|
|
347
|
+
pumpSendToReply(req, reply, '/' + file, rootPath)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
fastify.get(route, routeOpts, function (req, reply) {
|
|
351
|
+
pumpSendToReply(req, reply, '/' + file, rootPath)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const key = path.posix.basename(route)
|
|
355
|
+
if (indexes.includes(key) && !indexDirs.has(key)) {
|
|
356
|
+
indexDirs.set(path.posix.dirname(route), rootPath)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const [dirname, rootPath] of indexDirs.entries()) {
|
|
362
|
+
const pathname = dirname + (dirname.endsWith('/') ? '' : '/')
|
|
363
|
+
const file = '/' + pathname.replace(prefix, '')
|
|
364
|
+
|
|
365
|
+
fastify.head(pathname, routeOpts, function (req, reply) {
|
|
366
|
+
pumpSendToReply(req, reply, file, rootPath)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
fastify.get(pathname, routeOpts, function (req, reply) {
|
|
370
|
+
pumpSendToReply(req, reply, file, rootPath)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
if (opts.redirect === true) {
|
|
374
|
+
fastify.head(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) {
|
|
375
|
+
pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath)
|
|
376
|
+
})
|
|
377
|
+
fastify.get(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) {
|
|
378
|
+
pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath)
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function checkRootPathForErrors (fastify, rootPath) {
|
|
387
|
+
if (rootPath === undefined) {
|
|
388
|
+
throw new Error('"root" option is required')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (Array.isArray(rootPath)) {
|
|
392
|
+
if (!rootPath.length) {
|
|
393
|
+
throw new Error('"root" option array requires one or more paths')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if ([...new Set(rootPath)].length !== rootPath.length) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
'"root" option array contains one or more duplicate paths'
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// check each path and fail at first invalid
|
|
403
|
+
rootPath.map((path) => checkPath(fastify, path))
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (typeof rootPath === 'string') {
|
|
408
|
+
return checkPath(fastify, rootPath)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
throw new Error('"root" option must be a string or array of strings')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function checkPath (fastify, rootPath) {
|
|
415
|
+
if (typeof rootPath !== 'string') {
|
|
416
|
+
throw new Error('"root" option must be a string')
|
|
417
|
+
}
|
|
418
|
+
if (path.isAbsolute(rootPath) === false) {
|
|
419
|
+
throw new Error('"root" option must be an absolute path')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let pathStat
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
pathStat = statSync(rootPath)
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (e.code === 'ENOENT') {
|
|
428
|
+
fastify.log.warn(`"root" path "${rootPath}" must exist`)
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
throw e
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (pathStat.isDirectory() === false) {
|
|
436
|
+
throw new Error('"root" option must point to a directory')
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const supportedEncodings = ['br', 'gzip', 'deflate']
|
|
441
|
+
|
|
442
|
+
function getContentType (path) {
|
|
443
|
+
const type = send.mime.lookup(path)
|
|
444
|
+
const charset = send.mime.charsets.lookup(type)
|
|
445
|
+
if (!charset) {
|
|
446
|
+
return type
|
|
447
|
+
}
|
|
448
|
+
return `${type}; charset=${charset}`
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function findIndexFile (pathname, root, indexFiles = ['index.html']) {
|
|
452
|
+
return indexFiles.find(filename => {
|
|
453
|
+
const p = path.join(root, pathname, filename)
|
|
454
|
+
try {
|
|
455
|
+
const stats = statSync(p)
|
|
456
|
+
return !stats.isDirectory()
|
|
457
|
+
} catch (e) {
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
|
|
464
|
+
function getEncodingHeader (headers, checked) {
|
|
465
|
+
if (!('accept-encoding' in headers)) return
|
|
466
|
+
|
|
467
|
+
const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip')
|
|
468
|
+
return encodingNegotiator.negotiate(
|
|
469
|
+
header,
|
|
470
|
+
supportedEncodings.filter((enc) => !checked.has(enc))
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function getEncodingExtension (encoding) {
|
|
475
|
+
switch (encoding) {
|
|
476
|
+
case 'br':
|
|
477
|
+
return 'br'
|
|
478
|
+
|
|
479
|
+
case 'gzip':
|
|
480
|
+
return 'gz'
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function getRedirectUrl (url) {
|
|
485
|
+
let i = 0
|
|
486
|
+
// we detech how many slash before a valid path
|
|
487
|
+
for (i; i < url.length; i++) {
|
|
488
|
+
if (url[i] !== '/' && url[i] !== '\\') break
|
|
489
|
+
}
|
|
490
|
+
// turns all leading / or \ into a single /
|
|
491
|
+
url = '/' + url.substr(i)
|
|
492
|
+
try {
|
|
493
|
+
const parsed = new URL(url, 'http://localhost.com/')
|
|
494
|
+
return parsed.pathname + (parsed.pathname[parsed.pathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
|
|
495
|
+
} catch (error) {
|
|
496
|
+
// the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
|
|
497
|
+
/* istanbul ignore next */
|
|
498
|
+
const err = new Error(`Invalid redirect URL: ${url}`)
|
|
499
|
+
/* istanbul ignore next */
|
|
500
|
+
err.statusCode = 400
|
|
501
|
+
/* istanbul ignore next */
|
|
502
|
+
throw err
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
module.exports = fp(fastifyStatic, {
|
|
507
|
+
fastify: '3.x',
|
|
508
|
+
name: 'fastify-static'
|
|
509
|
+
})
|