@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.
Files changed (46) hide show
  1. package/.gitattributes +5 -0
  2. package/.github/dependabot.yml +13 -0
  3. package/.github/stale.yml +21 -0
  4. package/.github/workflows/ci.yml +46 -0
  5. package/LICENSE +21 -0
  6. package/README.md +440 -0
  7. package/example/public/images/sample.jpg +0 -0
  8. package/example/public/index.css +9 -0
  9. package/example/public/index.html +11 -0
  10. package/example/public/index.js +8 -0
  11. package/example/public2/test.css +4 -0
  12. package/example/public2/test.html +8 -0
  13. package/example/server-compress.js +15 -0
  14. package/example/server-dir-list.js +49 -0
  15. package/example/server.js +13 -0
  16. package/index.d.ts +98 -0
  17. package/index.js +509 -0
  18. package/lib/dirList.js +199 -0
  19. package/package.json +71 -0
  20. package/test/dir-list.test.js +675 -0
  21. package/test/static/.example +1 -0
  22. package/test/static/a .md +1 -0
  23. package/test/static/deep/path/for/test/index.html +5 -0
  24. package/test/static/deep/path/for/test/purpose/foo.html +5 -0
  25. package/test/static/foo.html +3 -0
  26. package/test/static/foobar.html +3 -0
  27. package/test/static/index.css +0 -0
  28. package/test/static/index.html +5 -0
  29. package/test/static/shallow/sample.jpg +0 -0
  30. package/test/static-pre-compressed/all-three.html +5 -0
  31. package/test/static-pre-compressed/all-three.html.br +0 -0
  32. package/test/static-pre-compressed/all-three.html.gz +0 -0
  33. package/test/static-pre-compressed/dir/index.html +5 -0
  34. package/test/static-pre-compressed/dir/index.html.br +0 -0
  35. package/test/static-pre-compressed/empty/.gitkeep +0 -0
  36. package/test/static-pre-compressed/gzip-only.html +3 -0
  37. package/test/static-pre-compressed/gzip-only.html.gz +0 -0
  38. package/test/static-pre-compressed/index.html +5 -0
  39. package/test/static-pre-compressed/index.html.br +0 -0
  40. package/test/static-pre-compressed/sample.jpg +0 -0
  41. package/test/static-pre-compressed/sample.jpg.br +0 -0
  42. package/test/static-pre-compressed/uncompressed.html +3 -0
  43. package/test/static.test.js +3482 -0
  44. package/test/static2/bar.html +3 -0
  45. package/test/static2/index.html +3 -0
  46. package/test/types/index.ts +105 -0
package/lib/dirList.js ADDED
@@ -0,0 +1,199 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const fs = require('fs').promises
5
+ const pLimit = require('p-limit')
6
+
7
+ const dirList = {
8
+ /**
9
+ * get files and dirs from dir, or error
10
+ * @param {string} dir full path fs dir
11
+ * @param {function(error, entries)} callback
12
+ * note: can't use glob because don't get error on non existing dir
13
+ */
14
+ list: async function (dir, options) {
15
+ const entries = { dirs: [], files: [] }
16
+ const files = await fs.readdir(dir)
17
+ if (files.length < 1) {
18
+ return entries
19
+ }
20
+
21
+ const limit = pLimit(4)
22
+ await Promise.all(files.map(filename => limit(async () => {
23
+ let stats
24
+ try {
25
+ stats = await fs.stat(path.join(dir, filename))
26
+ } catch (error) {
27
+ return
28
+ }
29
+ const entry = { name: filename, stats }
30
+ if (stats.isDirectory()) {
31
+ if (options.extendedFolderInfo) {
32
+ entry.extendedInfo = await getExtendedInfo(path.join(dir, filename))
33
+ }
34
+ entries.dirs.push(entry)
35
+ } else {
36
+ entries.files.push(entry)
37
+ }
38
+ })))
39
+
40
+ async function getExtendedInfo (folderPath) {
41
+ const depth = folderPath.split(path.sep).length
42
+ let totalSize = 0
43
+ let fileCount = 0
44
+ let totalFileCount = 0
45
+ let folderCount = 0
46
+ let totalFolderCount = 0
47
+ let lastModified = 0
48
+
49
+ async function walk (dir) {
50
+ const files = await fs.readdir(dir)
51
+ const limit = pLimit(4)
52
+ await Promise.all(files.map(filename => limit(async () => {
53
+ const filePath = path.join(dir, filename)
54
+ let stats
55
+ try {
56
+ stats = await fs.stat(filePath)
57
+ } catch (error) {
58
+ return
59
+ }
60
+
61
+ if (stats.isDirectory()) {
62
+ totalFolderCount++
63
+ if (filePath.split(path.sep).length === depth + 1) {
64
+ folderCount++
65
+ }
66
+ await walk(filePath)
67
+ } else {
68
+ totalSize += stats.size
69
+ totalFileCount++
70
+ if (filePath.split(path.sep).length === depth + 1) {
71
+ fileCount++
72
+ }
73
+ lastModified = Math.max(lastModified, stats.mtimeMs)
74
+ }
75
+ })))
76
+ }
77
+
78
+ await walk(folderPath)
79
+ return {
80
+ totalSize,
81
+ fileCount,
82
+ totalFileCount,
83
+ folderCount,
84
+ totalFolderCount,
85
+ lastModified
86
+ }
87
+ }
88
+
89
+ entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
90
+ entries.files.sort((a, b) => a.name.localeCompare(b.name))
91
+ return entries
92
+ },
93
+
94
+ /**
95
+ * send dir list content, or 404 on error
96
+ * @param {Fastify.Reply} reply
97
+ * @param {string} dir full path fs dir
98
+ * @param {ListOptions} options
99
+ * @param {string} route request route
100
+ */
101
+ send: async function ({ reply, dir, options, route, prefix }) {
102
+ let entries
103
+ try {
104
+ entries = await dirList.list(dir, options)
105
+ } catch (error) {
106
+ return reply.callNotFound()
107
+ }
108
+ const format = reply.request.query.format || options.format
109
+ if (format !== 'html') {
110
+ if (options.jsonFormat !== 'extended') {
111
+ const nameEntries = { dirs: [], files: [] }
112
+ entries.dirs.forEach(entry => nameEntries.dirs.push(entry.name))
113
+ entries.files.forEach(entry => nameEntries.files.push(entry.name))
114
+
115
+ reply.send(nameEntries)
116
+ } else {
117
+ reply.send(entries)
118
+ }
119
+ return
120
+ }
121
+
122
+ const html = options.render(
123
+ entries.dirs.map(entry => dirList.htmlInfo(entry, route, prefix, options)),
124
+ entries.files.map(entry => dirList.htmlInfo(entry, route, prefix, options)))
125
+ reply.type('text/html').send(html)
126
+ },
127
+
128
+ /**
129
+ * provide the html information about entry and route, to get name and full route
130
+ * @param entry file or dir name and stats
131
+ * @param {string} route request route
132
+ * @return {ListFile}
133
+ */
134
+ htmlInfo: function (entry, route, prefix, options) {
135
+ if (options.names && options.names.includes(path.basename(route))) {
136
+ route = path.normalize(path.join(route, '..'))
137
+ }
138
+ return {
139
+ href: path.join(prefix, route, entry.name).replace(/\\/g, '/'),
140
+ name: entry.name,
141
+ stats: entry.stats,
142
+ extendedInfo: entry.extendedInfo
143
+ }
144
+ },
145
+
146
+ /**
147
+ * say if the route can be handled by dir list or not
148
+ * @param {string} route request route
149
+ * @param {ListOptions} options
150
+ * @return {boolean}
151
+ */
152
+ handle: function (route, options) {
153
+ if (!options.names) {
154
+ return false
155
+ }
156
+ return options.names.includes(path.basename(route)) ||
157
+ // match trailing slash
158
+ (options.names.includes('/') && route[route.length - 1] === '/')
159
+ },
160
+
161
+ /**
162
+ * get path from route and fs root paths, considering trailing slash
163
+ * @param {string} root fs root path
164
+ * @param {string} route request route
165
+ */
166
+ path: function (root, route) {
167
+ const _route = route[route.length - 1] === '/'
168
+ ? route + 'none'
169
+ : route
170
+ return path.dirname(path.join(root, _route))
171
+ },
172
+
173
+ /**
174
+ * validate options
175
+ * @return {Error}
176
+ */
177
+ validateOptions: function (options) {
178
+ if (!options.list) {
179
+ return
180
+ }
181
+
182
+ if (Array.isArray(options.root)) {
183
+ return new TypeError('multi-root with list option is not supported')
184
+ }
185
+
186
+ if (options.list.format && options.list.format !== 'json' && options.list.format !== 'html') {
187
+ return new TypeError('The `list.format` option must be json or html')
188
+ }
189
+ if (options.list.names && !Array.isArray(options.list.names)) {
190
+ return new TypeError('The `list.names` option must be an array')
191
+ }
192
+ if (options.list.format === 'html' && typeof options.list.render !== 'function') {
193
+ return new TypeError('The `list.render` option must be a function and is required with html format')
194
+ }
195
+ }
196
+
197
+ }
198
+
199
+ module.exports = dirList
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@fastify/static",
3
+ "version": "5.0.0",
4
+ "description": "Plugin for serving static files as fast as possible.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "lint": "standard | snazzy",
9
+ "lint:fix": "standard --fix",
10
+ "unit": "tap --no-check-coverage test/*.test.js",
11
+ "typescript": "tsd",
12
+ "test": "npm run lint && npm run unit && npm run typescript",
13
+ "example": "node example/server.js",
14
+ "coverage": "tap --cov --coverage-report=html --no-check-coverage test",
15
+ "coveralls": "tap test/*test.js test/*/*.test.js --cov"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/fastify/fastify-static.git"
20
+ },
21
+ "keywords": [
22
+ "fastify",
23
+ "static"
24
+ ],
25
+ "author": "Tommaso Allevi - @allevo",
26
+ "license": "MIT",
27
+ "bugs": {
28
+ "url": "https://github.com/fastify/fastify-static/issues"
29
+ },
30
+ "homepage": "https://github.com/fastify/fastify-static",
31
+ "dependencies": {
32
+ "content-disposition": "^0.5.3",
33
+ "encoding-negotiator": "^2.0.1",
34
+ "fastify-plugin": "^3.0.0",
35
+ "glob": "^7.1.4",
36
+ "p-limit": "^3.1.0",
37
+ "readable-stream": "^3.4.0",
38
+ "send": "^0.17.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^17.0.0",
42
+ "@typescript-eslint/eslint-plugin": "^2.29.0",
43
+ "@typescript-eslint/parser": "^2.29.0",
44
+ "concat-stream": "^2.0.0",
45
+ "coveralls": "^3.0.4",
46
+ "eslint-plugin-typescript": "^0.14.0",
47
+ "fastify": "^3.7.0",
48
+ "fastify-compress": "^4.0.0",
49
+ "handlebars": "^4.7.6",
50
+ "pre-commit": "^1.2.2",
51
+ "proxyquire": "^2.1.0",
52
+ "simple-get": "^4.0.0",
53
+ "snazzy": "^9.0.0",
54
+ "standard": "^17.0.0",
55
+ "tap": "^16.0.0",
56
+ "tsd": "^0.20.0",
57
+ "typescript": "^4.0.2"
58
+ },
59
+ "tsd": {
60
+ "directory": "test/types"
61
+ },
62
+ "eslintConfig": {
63
+ "rules": {
64
+ "no-unused-vars": "off",
65
+ "@typescript-eslint/no-unused-vars": "error"
66
+ }
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ }
71
+ }