@fastify/static 7.0.0 → 7.0.2
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/index.js
CHANGED
|
@@ -13,7 +13,6 @@ const contentDisposition = require('content-disposition')
|
|
|
13
13
|
const dirList = require('./lib/dirList')
|
|
14
14
|
|
|
15
15
|
const endForwardSlashRegex = /\/$/u
|
|
16
|
-
const doubleForwardSlashRegex = /\/\//gu
|
|
17
16
|
const asteriskRegex = /\*/gu
|
|
18
17
|
|
|
19
18
|
const supportedEncodings = ['br', 'gzip', 'deflate']
|
|
@@ -112,14 +111,11 @@ async function fastifyStatic (fastify, opts) {
|
|
|
112
111
|
throw new Error('"wildcard" option must be a boolean')
|
|
113
112
|
}
|
|
114
113
|
if (opts.wildcard === undefined || opts.wildcard === true) {
|
|
115
|
-
fastify.
|
|
116
|
-
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
|
|
117
|
-
})
|
|
118
|
-
fastify.get(prefix + '*', routeOpts, function (req, reply) {
|
|
114
|
+
fastify.get(prefix + '*', { ...routeOpts, exposeHeadRoute: true }, (req, reply) => {
|
|
119
115
|
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
|
|
120
116
|
})
|
|
121
117
|
if (opts.redirect === true && prefix !== opts.prefix) {
|
|
122
|
-
fastify.get(opts.prefix, routeOpts,
|
|
118
|
+
fastify.get(opts.prefix, routeOpts, (req, reply) => {
|
|
123
119
|
reply.redirect(301, getRedirectUrl(req.raw.url))
|
|
124
120
|
})
|
|
125
121
|
}
|
|
@@ -127,18 +123,18 @@ async function fastifyStatic (fastify, opts) {
|
|
|
127
123
|
const indexes = opts.index === undefined ? ['index.html'] : [].concat(opts.index)
|
|
128
124
|
const indexDirs = new Map()
|
|
129
125
|
const routes = new Set()
|
|
130
|
-
const globPattern = '**/**'
|
|
131
126
|
|
|
132
127
|
const roots = Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]
|
|
133
|
-
for (let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const files = await glob(
|
|
128
|
+
for (let rootPath of roots) {
|
|
129
|
+
rootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
|
|
130
|
+
!rootPath.endsWith('/') && (rootPath += '/')
|
|
131
|
+
const files = await glob('**/**', {
|
|
132
|
+
cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles
|
|
133
|
+
})
|
|
137
134
|
|
|
138
|
-
for (let
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const route = (prefix + file).replace(doubleForwardSlashRegex, '/')
|
|
135
|
+
for (let file of files) {
|
|
136
|
+
file = file.split(path.win32.sep).join(path.posix.sep)
|
|
137
|
+
const route = prefix + file
|
|
142
138
|
|
|
143
139
|
if (routes.has(route)) {
|
|
144
140
|
continue
|
|
@@ -175,9 +171,10 @@ async function fastifyStatic (fastify, opts) {
|
|
|
175
171
|
pathname,
|
|
176
172
|
rootPath,
|
|
177
173
|
rootPathOffset = 0,
|
|
178
|
-
pumpOptions
|
|
174
|
+
pumpOptions,
|
|
179
175
|
checkedEncodings
|
|
180
176
|
) {
|
|
177
|
+
const pathnameOrig = pathname
|
|
181
178
|
const options = Object.assign({}, sendOptions, pumpOptions)
|
|
182
179
|
|
|
183
180
|
if (rootPath) {
|
|
@@ -350,7 +347,7 @@ async function fastifyStatic (fastify, opts) {
|
|
|
350
347
|
return pumpSendToReply(
|
|
351
348
|
request,
|
|
352
349
|
reply,
|
|
353
|
-
|
|
350
|
+
pathnameOrig,
|
|
354
351
|
rootPath,
|
|
355
352
|
rootPathOffset,
|
|
356
353
|
undefined,
|
package/lib/dirList.js
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const os = require('node:os')
|
|
3
4
|
const path = require('node:path')
|
|
4
5
|
const fs = require('node:fs/promises')
|
|
5
|
-
const
|
|
6
|
+
const fastq = require('fastq')
|
|
7
|
+
const fastqConcurrency = Math.max(1, os.cpus().length - 1)
|
|
6
8
|
|
|
7
9
|
const dirList = {
|
|
10
|
+
_getExtendedInfo: async function (dir, info) {
|
|
11
|
+
const depth = dir.split(path.sep).length
|
|
12
|
+
const files = await fs.readdir(dir)
|
|
13
|
+
|
|
14
|
+
const worker = async (filename) => {
|
|
15
|
+
const filePath = path.join(dir, filename)
|
|
16
|
+
let stats
|
|
17
|
+
try {
|
|
18
|
+
stats = await fs.stat(filePath)
|
|
19
|
+
} catch {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (stats.isDirectory()) {
|
|
24
|
+
info.totalFolderCount++
|
|
25
|
+
filePath.split(path.sep).length === depth + 1 && info.folderCount++
|
|
26
|
+
await dirList._getExtendedInfo(filePath, info)
|
|
27
|
+
} else {
|
|
28
|
+
info.totalSize += stats.size
|
|
29
|
+
info.totalFileCount++
|
|
30
|
+
filePath.split(path.sep).length === depth + 1 && info.fileCount++
|
|
31
|
+
info.lastModified = Math.max(info.lastModified, stats.mtimeMs)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const queue = fastq.promise(worker, fastqConcurrency)
|
|
35
|
+
await Promise.all(files.map(filename => queue.push(filename)))
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* get extended info about a folder
|
|
40
|
+
* @param {string} folderPath full path fs dir
|
|
41
|
+
* @return {Promise<ExtendedInfo>}
|
|
42
|
+
*/
|
|
43
|
+
getExtendedInfo: async function (folderPath) {
|
|
44
|
+
const info = {
|
|
45
|
+
totalSize: 0,
|
|
46
|
+
fileCount: 0,
|
|
47
|
+
totalFileCount: 0,
|
|
48
|
+
folderCount: 0,
|
|
49
|
+
totalFolderCount: 0,
|
|
50
|
+
lastModified: 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await dirList._getExtendedInfo(folderPath, info)
|
|
54
|
+
|
|
55
|
+
return info
|
|
56
|
+
},
|
|
57
|
+
|
|
8
58
|
/**
|
|
9
59
|
* get files and dirs from dir, or error
|
|
10
60
|
* @param {string} dir full path fs dir
|
|
@@ -22,8 +72,7 @@ const dirList = {
|
|
|
22
72
|
return entries
|
|
23
73
|
}
|
|
24
74
|
|
|
25
|
-
const
|
|
26
|
-
await Promise.all(files.map(filename => limit(async () => {
|
|
75
|
+
const worker = async (filename) => {
|
|
27
76
|
let stats
|
|
28
77
|
try {
|
|
29
78
|
stats = await fs.stat(path.join(dir, filename))
|
|
@@ -33,62 +82,15 @@ const dirList = {
|
|
|
33
82
|
const entry = { name: filename, stats }
|
|
34
83
|
if (stats.isDirectory()) {
|
|
35
84
|
if (options.extendedFolderInfo) {
|
|
36
|
-
entry.extendedInfo = await getExtendedInfo(path.join(dir, filename))
|
|
85
|
+
entry.extendedInfo = await dirList.getExtendedInfo(path.join(dir, filename))
|
|
37
86
|
}
|
|
38
87
|
entries.dirs.push(entry)
|
|
39
88
|
} else {
|
|
40
89
|
entries.files.push(entry)
|
|
41
90
|
}
|
|
42
|
-
})))
|
|
43
|
-
|
|
44
|
-
async function getExtendedInfo (folderPath) {
|
|
45
|
-
const depth = folderPath.split(path.sep).length
|
|
46
|
-
let totalSize = 0
|
|
47
|
-
let fileCount = 0
|
|
48
|
-
let totalFileCount = 0
|
|
49
|
-
let folderCount = 0
|
|
50
|
-
let totalFolderCount = 0
|
|
51
|
-
let lastModified = 0
|
|
52
|
-
|
|
53
|
-
async function walk (dir) {
|
|
54
|
-
const files = await fs.readdir(dir)
|
|
55
|
-
const limit = pLimit(4)
|
|
56
|
-
await Promise.all(files.map(filename => limit(async () => {
|
|
57
|
-
const filePath = path.join(dir, filename)
|
|
58
|
-
let stats
|
|
59
|
-
try {
|
|
60
|
-
stats = await fs.stat(filePath)
|
|
61
|
-
} catch {
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (stats.isDirectory()) {
|
|
66
|
-
totalFolderCount++
|
|
67
|
-
if (filePath.split(path.sep).length === depth + 1) {
|
|
68
|
-
folderCount++
|
|
69
|
-
}
|
|
70
|
-
await walk(filePath)
|
|
71
|
-
} else {
|
|
72
|
-
totalSize += stats.size
|
|
73
|
-
totalFileCount++
|
|
74
|
-
if (filePath.split(path.sep).length === depth + 1) {
|
|
75
|
-
fileCount++
|
|
76
|
-
}
|
|
77
|
-
lastModified = Math.max(lastModified, stats.mtimeMs)
|
|
78
|
-
}
|
|
79
|
-
})))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
await walk(folderPath)
|
|
83
|
-
return {
|
|
84
|
-
totalSize,
|
|
85
|
-
fileCount,
|
|
86
|
-
totalFileCount,
|
|
87
|
-
folderCount,
|
|
88
|
-
totalFolderCount,
|
|
89
|
-
lastModified
|
|
90
|
-
}
|
|
91
91
|
}
|
|
92
|
+
const queue = fastq.promise(worker, fastqConcurrency)
|
|
93
|
+
await Promise.all(files.map(filename => queue.push(filename)))
|
|
92
94
|
|
|
93
95
|
entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
|
|
94
96
|
entries.files.sort((a, b) => a.name.localeCompare(b.name))
|
|
@@ -115,6 +117,7 @@ const dirList = {
|
|
|
115
117
|
} catch {
|
|
116
118
|
return reply.callNotFound()
|
|
117
119
|
}
|
|
120
|
+
|
|
118
121
|
const format = reply.request.query.format || options.format
|
|
119
122
|
if (format !== 'html') {
|
|
120
123
|
if (options.jsonFormat !== 'extended') {
|
|
@@ -203,7 +206,6 @@ const dirList = {
|
|
|
203
206
|
return new TypeError('The `list.render` option must be a function and is required with html format')
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
|
-
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
module.exports = dirList
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/static",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.2",
|
|
4
4
|
"description": "Plugin for serving static files as fast as possible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"@fastify/send": "^2.0.0",
|
|
36
36
|
"content-disposition": "^0.5.3",
|
|
37
37
|
"fastify-plugin": "^4.0.0",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
38
|
+
"fastq": "^1.17.0",
|
|
39
|
+
"glob": "^10.3.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@fastify/compress": "^
|
|
42
|
+
"@fastify/compress": "^7.0.0",
|
|
43
43
|
"@fastify/pre-commit": "^2.0.2",
|
|
44
44
|
"@types/node": "^20.1.0",
|
|
45
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
46
|
-
"@typescript-eslint/parser": "^
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
|
46
|
+
"@typescript-eslint/parser": "^7.1.0",
|
|
47
47
|
"concat-stream": "^2.0.0",
|
|
48
48
|
"coveralls": "^3.0.4",
|
|
49
49
|
"eslint-plugin-typescript": "^0.14.0",
|
|
Binary file
|
package/test/static.test.js
CHANGED
|
@@ -46,6 +46,9 @@ const indexBr = fs.readFileSync(
|
|
|
46
46
|
const dirIndexBr = fs.readFileSync(
|
|
47
47
|
'./test/static-pre-compressed/dir/index.html.br'
|
|
48
48
|
)
|
|
49
|
+
const dirIndexGz = fs.readFileSync(
|
|
50
|
+
'./test/static-pre-compressed/dir-gz/index.html.gz'
|
|
51
|
+
)
|
|
49
52
|
const uncompressedStatic = fs
|
|
50
53
|
.readFileSync('./test/static-pre-compressed/uncompressed.html')
|
|
51
54
|
.toString('utf8')
|
|
@@ -2012,6 +2015,96 @@ t.test('register with wildcard false', t => {
|
|
|
2012
2015
|
})
|
|
2013
2016
|
})
|
|
2014
2017
|
|
|
2018
|
+
t.test('register with wildcard false (trailing slash in the root)', t => {
|
|
2019
|
+
t.plan(6)
|
|
2020
|
+
|
|
2021
|
+
const pluginOptions = {
|
|
2022
|
+
root: path.join(__dirname, '/static/'),
|
|
2023
|
+
prefix: '/assets/',
|
|
2024
|
+
index: false,
|
|
2025
|
+
wildcard: false
|
|
2026
|
+
}
|
|
2027
|
+
const fastify = Fastify({
|
|
2028
|
+
ignoreTrailingSlash: true
|
|
2029
|
+
})
|
|
2030
|
+
fastify.register(fastifyStatic, pluginOptions)
|
|
2031
|
+
|
|
2032
|
+
fastify.get('/*', (request, reply) => {
|
|
2033
|
+
reply.send({ hello: 'world' })
|
|
2034
|
+
})
|
|
2035
|
+
|
|
2036
|
+
t.teardown(fastify.close.bind(fastify))
|
|
2037
|
+
|
|
2038
|
+
fastify.listen({ port: 0 }, (err) => {
|
|
2039
|
+
t.error(err)
|
|
2040
|
+
|
|
2041
|
+
fastify.server.unref()
|
|
2042
|
+
|
|
2043
|
+
t.test('/index.css', (t) => {
|
|
2044
|
+
t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT)
|
|
2045
|
+
simple.concat({
|
|
2046
|
+
method: 'GET',
|
|
2047
|
+
url: 'http://localhost:' + fastify.server.address().port + '/assets/index.css'
|
|
2048
|
+
}, (err, response, body) => {
|
|
2049
|
+
t.error(err)
|
|
2050
|
+
t.equal(response.statusCode, 200)
|
|
2051
|
+
genericResponseChecks(t, response)
|
|
2052
|
+
})
|
|
2053
|
+
})
|
|
2054
|
+
|
|
2055
|
+
t.test('/not-defined', (t) => {
|
|
2056
|
+
t.plan(3)
|
|
2057
|
+
simple.concat({
|
|
2058
|
+
method: 'GET',
|
|
2059
|
+
url: 'http://localhost:' + fastify.server.address().port + '/assets/not-defined'
|
|
2060
|
+
}, (err, response, body) => {
|
|
2061
|
+
t.error(err)
|
|
2062
|
+
t.equal(response.statusCode, 200)
|
|
2063
|
+
t.same(JSON.parse(body), { hello: 'world' })
|
|
2064
|
+
})
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
t.test('/deep/path/for/test/purpose/foo.html', (t) => {
|
|
2068
|
+
t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
|
|
2069
|
+
simple.concat({
|
|
2070
|
+
method: 'GET',
|
|
2071
|
+
url: 'http://localhost:' + fastify.server.address().port + '/assets/deep/path/for/test/purpose/foo.html'
|
|
2072
|
+
}, (err, response, body) => {
|
|
2073
|
+
t.error(err)
|
|
2074
|
+
t.equal(response.statusCode, 200)
|
|
2075
|
+
t.equal(body.toString(), deepContent)
|
|
2076
|
+
genericResponseChecks(t, response)
|
|
2077
|
+
})
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
t.test('/../index.js', (t) => {
|
|
2081
|
+
t.plan(3)
|
|
2082
|
+
simple.concat({
|
|
2083
|
+
method: 'GET',
|
|
2084
|
+
url: 'http://localhost:' + fastify.server.address().port + '/assets/../index.js',
|
|
2085
|
+
followRedirect: false
|
|
2086
|
+
}, (err, response, body) => {
|
|
2087
|
+
t.error(err)
|
|
2088
|
+
t.equal(response.statusCode, 200)
|
|
2089
|
+
t.same(JSON.parse(body), { hello: 'world' })
|
|
2090
|
+
})
|
|
2091
|
+
})
|
|
2092
|
+
|
|
2093
|
+
t.test('/index.css', t => {
|
|
2094
|
+
t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
|
|
2095
|
+
simple.concat({
|
|
2096
|
+
method: 'HEAD',
|
|
2097
|
+
url: 'http://localhost:' + fastify.server.address().port + '/assets/index.css'
|
|
2098
|
+
}, (err, response, body) => {
|
|
2099
|
+
t.error(err)
|
|
2100
|
+
t.equal(response.statusCode, 200)
|
|
2101
|
+
t.equal(body.toString(), '')
|
|
2102
|
+
genericResponseChecks(t, response)
|
|
2103
|
+
})
|
|
2104
|
+
})
|
|
2105
|
+
})
|
|
2106
|
+
})
|
|
2107
|
+
|
|
2015
2108
|
t.test('register with wildcard string', (t) => {
|
|
2016
2109
|
t.plan(1)
|
|
2017
2110
|
|
|
@@ -3406,6 +3499,35 @@ t.test(
|
|
|
3406
3499
|
}
|
|
3407
3500
|
)
|
|
3408
3501
|
|
|
3502
|
+
t.test(
|
|
3503
|
+
'will serve precompressed gzip index in subdir',
|
|
3504
|
+
async (t) => {
|
|
3505
|
+
const pluginOptions = {
|
|
3506
|
+
root: path.join(__dirname, '/static-pre-compressed'),
|
|
3507
|
+
preCompressed: true
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
const fastify = Fastify()
|
|
3511
|
+
|
|
3512
|
+
fastify.register(fastifyStatic, pluginOptions)
|
|
3513
|
+
t.teardown(fastify.close.bind(fastify))
|
|
3514
|
+
|
|
3515
|
+
const response = await fastify.inject({
|
|
3516
|
+
method: 'GET',
|
|
3517
|
+
url: '/dir-gz',
|
|
3518
|
+
headers: {
|
|
3519
|
+
'accept-encoding': 'gzip, deflate, br'
|
|
3520
|
+
}
|
|
3521
|
+
})
|
|
3522
|
+
|
|
3523
|
+
genericResponseChecks(t, response)
|
|
3524
|
+
t.equal(response.headers['content-encoding'], 'gzip')
|
|
3525
|
+
t.equal(response.statusCode, 200)
|
|
3526
|
+
t.same(response.rawPayload, dirIndexGz)
|
|
3527
|
+
t.end()
|
|
3528
|
+
}
|
|
3529
|
+
)
|
|
3530
|
+
|
|
3409
3531
|
t.test(
|
|
3410
3532
|
'will serve precompressed index with alternative index option',
|
|
3411
3533
|
async (t) => {
|