@fastify/static 7.0.0 → 7.0.1

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.head(prefix + '*', routeOpts, function (req, reply) {
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, function (req, reply) {
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 i = 0; i < roots.length; ++i) {
134
- const rootPath = roots[i]
135
- const posixRootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
136
- const files = await glob(`${posixRootPath}/${globPattern}`, { follow: true, nodir: true, dot: opts.serveDotFiles })
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 i = 0; i < files.length; ++i) {
139
- const file = files[i].split(path.win32.sep).join(path.posix.sep)
140
- .replace(`${posixRootPath}/`, '')
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,7 +171,7 @@ async function fastifyStatic (fastify, opts) {
175
171
  pathname,
176
172
  rootPath,
177
173
  rootPathOffset = 0,
178
- pumpOptions = {},
174
+ pumpOptions,
179
175
  checkedEncodings
180
176
  ) {
181
177
  const options = Object.assign({}, sendOptions, pumpOptions)
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 pLimit = require('p-limit')
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 limit = pLimit(4)
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.0",
3
+ "version": "7.0.1",
4
4
  "description": "Plugin for serving static files as fast as possible.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -35,11 +35,11 @@
35
35
  "@fastify/send": "^2.0.0",
36
36
  "content-disposition": "^0.5.3",
37
37
  "fastify-plugin": "^4.0.0",
38
- "glob": "^10.3.4",
39
- "p-limit": "^3.1.0"
38
+ "fastq": "^1.17.0",
39
+ "glob": "^10.3.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@fastify/compress": "^6.0.0",
42
+ "@fastify/compress": "^7.0.0",
43
43
  "@fastify/pre-commit": "^2.0.2",
44
44
  "@types/node": "^20.1.0",
45
45
  "@typescript-eslint/eslint-plugin": "^6.3.0",
@@ -2012,6 +2012,96 @@ t.test('register with wildcard false', t => {
2012
2012
  })
2013
2013
  })
2014
2014
 
2015
+ t.test('register with wildcard false (trailing slash in the root)', t => {
2016
+ t.plan(6)
2017
+
2018
+ const pluginOptions = {
2019
+ root: path.join(__dirname, '/static/'),
2020
+ prefix: '/assets/',
2021
+ index: false,
2022
+ wildcard: false
2023
+ }
2024
+ const fastify = Fastify({
2025
+ ignoreTrailingSlash: true
2026
+ })
2027
+ fastify.register(fastifyStatic, pluginOptions)
2028
+
2029
+ fastify.get('/*', (request, reply) => {
2030
+ reply.send({ hello: 'world' })
2031
+ })
2032
+
2033
+ t.teardown(fastify.close.bind(fastify))
2034
+
2035
+ fastify.listen({ port: 0 }, (err) => {
2036
+ t.error(err)
2037
+
2038
+ fastify.server.unref()
2039
+
2040
+ t.test('/index.css', (t) => {
2041
+ t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT)
2042
+ simple.concat({
2043
+ method: 'GET',
2044
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/index.css'
2045
+ }, (err, response, body) => {
2046
+ t.error(err)
2047
+ t.equal(response.statusCode, 200)
2048
+ genericResponseChecks(t, response)
2049
+ })
2050
+ })
2051
+
2052
+ t.test('/not-defined', (t) => {
2053
+ t.plan(3)
2054
+ simple.concat({
2055
+ method: 'GET',
2056
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/not-defined'
2057
+ }, (err, response, body) => {
2058
+ t.error(err)
2059
+ t.equal(response.statusCode, 200)
2060
+ t.same(JSON.parse(body), { hello: 'world' })
2061
+ })
2062
+ })
2063
+
2064
+ t.test('/deep/path/for/test/purpose/foo.html', (t) => {
2065
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
2066
+ simple.concat({
2067
+ method: 'GET',
2068
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/deep/path/for/test/purpose/foo.html'
2069
+ }, (err, response, body) => {
2070
+ t.error(err)
2071
+ t.equal(response.statusCode, 200)
2072
+ t.equal(body.toString(), deepContent)
2073
+ genericResponseChecks(t, response)
2074
+ })
2075
+ })
2076
+
2077
+ t.test('/../index.js', (t) => {
2078
+ t.plan(3)
2079
+ simple.concat({
2080
+ method: 'GET',
2081
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/../index.js',
2082
+ followRedirect: false
2083
+ }, (err, response, body) => {
2084
+ t.error(err)
2085
+ t.equal(response.statusCode, 200)
2086
+ t.same(JSON.parse(body), { hello: 'world' })
2087
+ })
2088
+ })
2089
+
2090
+ t.test('/index.css', t => {
2091
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
2092
+ simple.concat({
2093
+ method: 'HEAD',
2094
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/index.css'
2095
+ }, (err, response, body) => {
2096
+ t.error(err)
2097
+ t.equal(response.statusCode, 200)
2098
+ t.equal(body.toString(), '')
2099
+ genericResponseChecks(t, response)
2100
+ })
2101
+ })
2102
+ })
2103
+ })
2104
+
2015
2105
  t.test('register with wildcard string', (t) => {
2016
2106
  t.plan(1)
2017
2107