@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/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
|
+
}
|