@fastify/static 6.1.0 → 6.4.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/README.md +3 -1
- package/index.d.ts +13 -4
- package/index.js +6 -4
- package/lib/dirList.js +19 -7
- package/package.json +1 -1
- package/test/dir-list.test.js +192 -3
- package/test/static-dotfiles/.aaa +0 -0
- package/test/static-dotfiles/dir/index.html +0 -0
- package/test/static-dotfiles/test.txt +0 -0
- package/test/static.test.js +1 -1
- package/test/types/index.ts +31 -1
package/README.md
CHANGED
|
@@ -190,7 +190,9 @@ If set, it provide the directory list calling the directory path.
|
|
|
190
190
|
|
|
191
191
|
Default response is json.
|
|
192
192
|
|
|
193
|
-
Note:
|
|
193
|
+
Note:
|
|
194
|
+
- Multi-root is not supported within the `list` option.
|
|
195
|
+
- If `dotfiles` option value is `deny` or `ignore`, dotfiles will be excluded.
|
|
194
196
|
|
|
195
197
|
**Example:**
|
|
196
198
|
|
package/index.d.ts
CHANGED
|
@@ -43,13 +43,22 @@ interface ListRender {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface ListOptions {
|
|
46
|
-
|
|
47
|
-
names: string[];
|
|
48
|
-
render: ListRender;
|
|
46
|
+
names?: string[];
|
|
49
47
|
extendedFolderInfo?: boolean;
|
|
50
48
|
jsonFormat?: 'names' | 'extended';
|
|
51
49
|
}
|
|
52
50
|
|
|
51
|
+
interface ListOptionsJsonFormat extends ListOptions {
|
|
52
|
+
format: 'json';
|
|
53
|
+
// Required when the URL parameter `format=html` exists
|
|
54
|
+
render?: ListRender;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ListOptionsHtmlFormat extends ListOptions {
|
|
58
|
+
format: 'html';
|
|
59
|
+
render: ListRender;
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
// Passed on to `send`
|
|
54
63
|
interface SendOptions {
|
|
55
64
|
acceptRanges?: boolean;
|
|
@@ -73,7 +82,7 @@ export interface FastifyStaticOptions extends SendOptions {
|
|
|
73
82
|
setHeaders?: (...args: any[]) => void;
|
|
74
83
|
redirect?: boolean;
|
|
75
84
|
wildcard?: boolean;
|
|
76
|
-
list?: boolean |
|
|
85
|
+
list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
|
|
77
86
|
allowedPath?: (pathName: string, root?: string) => boolean;
|
|
78
87
|
/**
|
|
79
88
|
* @description
|
package/index.js
CHANGED
|
@@ -161,7 +161,8 @@ async function fastifyStatic (fastify, opts) {
|
|
|
161
161
|
dir: path,
|
|
162
162
|
options: opts.list,
|
|
163
163
|
route: pathname,
|
|
164
|
-
prefix
|
|
164
|
+
prefix,
|
|
165
|
+
dotfiles: opts.dotfiles
|
|
165
166
|
}).catch((err) => reply.send(err))
|
|
166
167
|
return
|
|
167
168
|
}
|
|
@@ -194,7 +195,7 @@ async function fastifyStatic (fastify, opts) {
|
|
|
194
195
|
|
|
195
196
|
stream.on('error', function (err) {
|
|
196
197
|
if (err.code === 'ENOENT') {
|
|
197
|
-
// when preCompress is enabled and the path is a
|
|
198
|
+
// when preCompress is enabled and the path is a directory without a trailing slash
|
|
198
199
|
if (opts.preCompressed && encoding) {
|
|
199
200
|
const indexPathname = findIndexFile(pathname, options.root, options.index)
|
|
200
201
|
if (indexPathname) {
|
|
@@ -217,7 +218,8 @@ async function fastifyStatic (fastify, opts) {
|
|
|
217
218
|
dir: dirList.path(opts.root, pathname),
|
|
218
219
|
options: opts.list,
|
|
219
220
|
route: pathname,
|
|
220
|
-
prefix
|
|
221
|
+
prefix,
|
|
222
|
+
dotfiles: opts.dotfiles
|
|
221
223
|
}).catch((err) => reply.send(err))
|
|
222
224
|
return
|
|
223
225
|
}
|
|
@@ -492,7 +494,7 @@ function getEncodingExtension (encoding) {
|
|
|
492
494
|
|
|
493
495
|
function getRedirectUrl (url) {
|
|
494
496
|
let i = 0
|
|
495
|
-
// we
|
|
497
|
+
// we detect how many slash before a valid path
|
|
496
498
|
for (i; i < url.length; i++) {
|
|
497
499
|
if (url[i] !== '/' && url[i] !== '\\') break
|
|
498
500
|
}
|
package/lib/dirList.js
CHANGED
|
@@ -8,12 +8,16 @@ const dirList = {
|
|
|
8
8
|
/**
|
|
9
9
|
* get files and dirs from dir, or error
|
|
10
10
|
* @param {string} dir full path fs dir
|
|
11
|
-
* @param {
|
|
11
|
+
* @param {(boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
|
|
12
|
+
* @param {string} dotfiles
|
|
12
13
|
* note: can't use glob because don't get error on non existing dir
|
|
13
14
|
*/
|
|
14
|
-
list: async function (dir, options) {
|
|
15
|
+
list: async function (dir, options, dotfiles) {
|
|
15
16
|
const entries = { dirs: [], files: [] }
|
|
16
|
-
|
|
17
|
+
let files = await fs.readdir(dir)
|
|
18
|
+
if (dotfiles === 'deny' || dotfiles === 'ignore') {
|
|
19
|
+
files = files.filter(f => f.charAt(0) !== '.')
|
|
20
|
+
}
|
|
17
21
|
if (files.length < 1) {
|
|
18
22
|
return entries
|
|
19
23
|
}
|
|
@@ -95,13 +99,18 @@ const dirList = {
|
|
|
95
99
|
* send dir list content, or 404 on error
|
|
96
100
|
* @param {Fastify.Reply} reply
|
|
97
101
|
* @param {string} dir full path fs dir
|
|
98
|
-
* @param {
|
|
102
|
+
* @param {(boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
|
|
99
103
|
* @param {string} route request route
|
|
104
|
+
* @param {string} dotfiles
|
|
100
105
|
*/
|
|
101
|
-
send: async function ({ reply, dir, options, route, prefix }) {
|
|
106
|
+
send: async function ({ reply, dir, options, route, prefix, dotfiles }) {
|
|
107
|
+
if (reply.request.query.format === 'html' && typeof options.render !== 'function') {
|
|
108
|
+
throw new Error('The `list.render` option must be a function and is required with the URL parameter `format=html`')
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
let entries
|
|
103
112
|
try {
|
|
104
|
-
entries = await dirList.list(dir, options)
|
|
113
|
+
entries = await dirList.list(dir, options, dotfiles)
|
|
105
114
|
} catch (error) {
|
|
106
115
|
return reply.callNotFound()
|
|
107
116
|
}
|
|
@@ -146,7 +155,7 @@ const dirList = {
|
|
|
146
155
|
/**
|
|
147
156
|
* say if the route can be handled by dir list or not
|
|
148
157
|
* @param {string} route request route
|
|
149
|
-
* @param {
|
|
158
|
+
* @param {(boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
|
|
150
159
|
* @return {boolean}
|
|
151
160
|
*/
|
|
152
161
|
handle: function (route, options) {
|
|
@@ -189,6 +198,9 @@ const dirList = {
|
|
|
189
198
|
if (options.list.names && !Array.isArray(options.list.names)) {
|
|
190
199
|
return new TypeError('The `list.names` option must be an array')
|
|
191
200
|
}
|
|
201
|
+
if (options.list.jsonFormat != null && options.list.jsonFormat !== 'names' && options.list.jsonFormat !== 'extended') {
|
|
202
|
+
return new TypeError('The `list.jsonFormat` option must be name or extended')
|
|
203
|
+
}
|
|
192
204
|
if (options.list.format === 'html' && typeof options.list.render !== 'function') {
|
|
193
205
|
return new TypeError('The `list.render` option must be a function and is required with html format')
|
|
194
206
|
}
|
package/package.json
CHANGED
package/test/dir-list.test.js
CHANGED
|
@@ -40,7 +40,7 @@ t.test('throws when `root` is an array', t => {
|
|
|
40
40
|
t.equal(err.message, 'multi-root with list option is not supported')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
t.test('throws when `list.format option
|
|
43
|
+
t.test('throws when `list.format` option is invalid', t => {
|
|
44
44
|
t.plan(2)
|
|
45
45
|
|
|
46
46
|
const err = dirList.validateOptions({ list: { format: 'hello' } })
|
|
@@ -56,6 +56,14 @@ t.test('throws when `list.names option` is not an array', t => {
|
|
|
56
56
|
t.equal(err.message, 'The `list.names` option must be an array')
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
t.test('throws when `list.jsonFormat` option is invalid', t => {
|
|
60
|
+
t.plan(2)
|
|
61
|
+
|
|
62
|
+
const err = dirList.validateOptions({ list: { jsonFormat: 'hello' } })
|
|
63
|
+
t.type(err, TypeError)
|
|
64
|
+
t.equal(err.message, 'The `list.jsonFormat` option must be name or extended')
|
|
65
|
+
})
|
|
66
|
+
|
|
59
67
|
t.test('throws when `list.format` is html and `list render` is not a function', t => {
|
|
60
68
|
t.plan(2)
|
|
61
69
|
|
|
@@ -469,7 +477,103 @@ t.test('dir list json format - extended info', t => {
|
|
|
469
477
|
})
|
|
470
478
|
})
|
|
471
479
|
|
|
472
|
-
t.test('
|
|
480
|
+
t.test('json format with url parameter format', t => {
|
|
481
|
+
t.plan(13)
|
|
482
|
+
|
|
483
|
+
const options = {
|
|
484
|
+
root: path.join(__dirname, '/static'),
|
|
485
|
+
prefix: '/public',
|
|
486
|
+
index: false,
|
|
487
|
+
list: {
|
|
488
|
+
format: 'json',
|
|
489
|
+
render (dirs, files) {
|
|
490
|
+
return 'html'
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const route = '/public/'
|
|
495
|
+
const jsonContent = { dirs: ['deep', 'shallow'], files: ['.example', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
|
|
496
|
+
|
|
497
|
+
helper.arrange(t, options, (url) => {
|
|
498
|
+
simple.concat({
|
|
499
|
+
method: 'GET',
|
|
500
|
+
url: url + route
|
|
501
|
+
}, (err, response, body) => {
|
|
502
|
+
t.error(err)
|
|
503
|
+
t.equal(response.statusCode, 200)
|
|
504
|
+
t.equal(body.toString(), JSON.stringify(jsonContent))
|
|
505
|
+
t.ok(response.headers['content-type'].includes('application/json'))
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
simple.concat({
|
|
509
|
+
method: 'GET',
|
|
510
|
+
url: url + route + '?format=html'
|
|
511
|
+
}, (err, response, body) => {
|
|
512
|
+
t.error(err)
|
|
513
|
+
t.equal(response.statusCode, 200)
|
|
514
|
+
t.equal(body.toString(), 'html')
|
|
515
|
+
t.ok(response.headers['content-type'].includes('text/html'))
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
simple.concat({
|
|
519
|
+
method: 'GET',
|
|
520
|
+
url: url + route + '?format=json'
|
|
521
|
+
}, (err, response, body) => {
|
|
522
|
+
t.error(err)
|
|
523
|
+
t.equal(response.statusCode, 200)
|
|
524
|
+
t.equal(body.toString(), JSON.stringify(jsonContent))
|
|
525
|
+
t.ok(response.headers['content-type'].includes('application/json'))
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
t.test('json format with url parameter format and without render option', t => {
|
|
531
|
+
t.plan(12)
|
|
532
|
+
|
|
533
|
+
const options = {
|
|
534
|
+
root: path.join(__dirname, '/static'),
|
|
535
|
+
prefix: '/public',
|
|
536
|
+
index: false,
|
|
537
|
+
list: {
|
|
538
|
+
format: 'json'
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const route = '/public/'
|
|
542
|
+
const jsonContent = { dirs: ['deep', 'shallow'], files: ['.example', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
|
|
543
|
+
|
|
544
|
+
helper.arrange(t, options, (url) => {
|
|
545
|
+
simple.concat({
|
|
546
|
+
method: 'GET',
|
|
547
|
+
url: url + route
|
|
548
|
+
}, (err, response, body) => {
|
|
549
|
+
t.error(err)
|
|
550
|
+
t.equal(response.statusCode, 200)
|
|
551
|
+
t.equal(body.toString(), JSON.stringify(jsonContent))
|
|
552
|
+
t.ok(response.headers['content-type'].includes('application/json'))
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
simple.concat({
|
|
556
|
+
method: 'GET',
|
|
557
|
+
url: url + route + '?format=html'
|
|
558
|
+
}, (err, response, body) => {
|
|
559
|
+
t.error(err)
|
|
560
|
+
t.equal(response.statusCode, 500)
|
|
561
|
+
t.equal(JSON.parse(body.toString()).message, 'The `list.render` option must be a function and is required with the URL parameter `format=html`')
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
simple.concat({
|
|
565
|
+
method: 'GET',
|
|
566
|
+
url: url + route + '?format=json'
|
|
567
|
+
}, (err, response, body) => {
|
|
568
|
+
t.error(err)
|
|
569
|
+
t.equal(response.statusCode, 200)
|
|
570
|
+
t.equal(body.toString(), JSON.stringify(jsonContent))
|
|
571
|
+
t.ok(response.headers['content-type'].includes('application/json'))
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
t.test('html format with url parameter format', t => {
|
|
473
577
|
t.plan(13)
|
|
474
578
|
|
|
475
579
|
const options = {
|
|
@@ -484,6 +588,7 @@ t.test('dir list - url parameter format', t => {
|
|
|
484
588
|
}
|
|
485
589
|
}
|
|
486
590
|
const route = '/public/'
|
|
591
|
+
const jsonContent = { dirs: ['deep', 'shallow'], files: ['.example', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
|
|
487
592
|
|
|
488
593
|
helper.arrange(t, options, (url) => {
|
|
489
594
|
simple.concat({
|
|
@@ -512,7 +617,7 @@ t.test('dir list - url parameter format', t => {
|
|
|
512
617
|
}, (err, response, body) => {
|
|
513
618
|
t.error(err)
|
|
514
619
|
t.equal(response.statusCode, 200)
|
|
515
|
-
t.
|
|
620
|
+
t.equal(body.toString(), JSON.stringify(jsonContent))
|
|
516
621
|
t.ok(response.headers['content-type'].includes('application/json'))
|
|
517
622
|
})
|
|
518
623
|
})
|
|
@@ -636,6 +741,90 @@ t.test('serve a non existent dir and get error', t => {
|
|
|
636
741
|
})
|
|
637
742
|
})
|
|
638
743
|
|
|
744
|
+
t.test('dir list with dotfiles allow option', t => {
|
|
745
|
+
t.plan(2)
|
|
746
|
+
|
|
747
|
+
const options = {
|
|
748
|
+
root: path.join(__dirname, '/static-dotfiles'),
|
|
749
|
+
prefix: '/public',
|
|
750
|
+
dotfiles: 'allow',
|
|
751
|
+
index: false,
|
|
752
|
+
list: true
|
|
753
|
+
}
|
|
754
|
+
const route = '/public/'
|
|
755
|
+
const content = { dirs: ['dir'], files: ['.aaa', 'test.txt'] }
|
|
756
|
+
|
|
757
|
+
helper.arrange(t, options, (url) => {
|
|
758
|
+
t.test(route, t => {
|
|
759
|
+
t.plan(3)
|
|
760
|
+
simple.concat({
|
|
761
|
+
method: 'GET',
|
|
762
|
+
url: url + route
|
|
763
|
+
}, (err, response, body) => {
|
|
764
|
+
t.error(err)
|
|
765
|
+
t.equal(response.statusCode, 200)
|
|
766
|
+
t.equal(body.toString(), JSON.stringify(content))
|
|
767
|
+
})
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
t.test('dir list with dotfiles deny option', t => {
|
|
773
|
+
t.plan(2)
|
|
774
|
+
|
|
775
|
+
const options = {
|
|
776
|
+
root: path.join(__dirname, '/static-dotfiles'),
|
|
777
|
+
prefix: '/public',
|
|
778
|
+
dotfiles: 'deny',
|
|
779
|
+
index: false,
|
|
780
|
+
list: true
|
|
781
|
+
}
|
|
782
|
+
const route = '/public/'
|
|
783
|
+
const content = { dirs: ['dir'], files: ['test.txt'] }
|
|
784
|
+
|
|
785
|
+
helper.arrange(t, options, (url) => {
|
|
786
|
+
t.test(route, t => {
|
|
787
|
+
t.plan(3)
|
|
788
|
+
simple.concat({
|
|
789
|
+
method: 'GET',
|
|
790
|
+
url: url + route
|
|
791
|
+
}, (err, response, body) => {
|
|
792
|
+
t.error(err)
|
|
793
|
+
t.equal(response.statusCode, 200)
|
|
794
|
+
t.equal(body.toString(), JSON.stringify(content))
|
|
795
|
+
})
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
t.test('dir list with dotfiles ignore option', t => {
|
|
801
|
+
t.plan(2)
|
|
802
|
+
|
|
803
|
+
const options = {
|
|
804
|
+
root: path.join(__dirname, '/static-dotfiles'),
|
|
805
|
+
prefix: '/public',
|
|
806
|
+
dotfiles: 'ignore',
|
|
807
|
+
index: false,
|
|
808
|
+
list: true
|
|
809
|
+
}
|
|
810
|
+
const route = '/public/'
|
|
811
|
+
const content = { dirs: ['dir'], files: ['test.txt'] }
|
|
812
|
+
|
|
813
|
+
helper.arrange(t, options, (url) => {
|
|
814
|
+
t.test(route, t => {
|
|
815
|
+
t.plan(3)
|
|
816
|
+
simple.concat({
|
|
817
|
+
method: 'GET',
|
|
818
|
+
url: url + route
|
|
819
|
+
}, (err, response, body) => {
|
|
820
|
+
t.error(err)
|
|
821
|
+
t.equal(response.statusCode, 200)
|
|
822
|
+
t.equal(body.toString(), JSON.stringify(content))
|
|
823
|
+
})
|
|
824
|
+
})
|
|
825
|
+
})
|
|
826
|
+
})
|
|
827
|
+
|
|
639
828
|
t.test('dir list error', t => {
|
|
640
829
|
t.plan(7)
|
|
641
830
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/static.test.js
CHANGED
|
@@ -2840,7 +2840,7 @@ t.test('routes should fallback to default errorHandler', t => {
|
|
|
2840
2840
|
})
|
|
2841
2841
|
})
|
|
2842
2842
|
|
|
2843
|
-
t.test('
|
|
2843
|
+
t.test('percent encoded URLs in glob mode', (t) => {
|
|
2844
2844
|
t.plan(4)
|
|
2845
2845
|
|
|
2846
2846
|
const fastify = Fastify({})
|
package/test/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fastify from 'fastify'
|
|
2
|
-
import { expectError } from 'tsd'
|
|
2
|
+
import { expectAssignable, expectError } from 'tsd'
|
|
3
3
|
import fastifyStatic, { FastifyStaticOptions } from '../..'
|
|
4
4
|
|
|
5
5
|
const appWithImplicitHttp = fastify()
|
|
@@ -32,6 +32,36 @@ expectError<FastifyStaticOptions>({
|
|
|
32
32
|
wildcard: '**/**'
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
expectAssignable<FastifyStaticOptions>({
|
|
36
|
+
root: '',
|
|
37
|
+
list: {
|
|
38
|
+
format: 'json',
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expectAssignable<FastifyStaticOptions>({
|
|
43
|
+
root: '',
|
|
44
|
+
list: {
|
|
45
|
+
format: 'json',
|
|
46
|
+
render: () => ''
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expectAssignable<FastifyStaticOptions>({
|
|
51
|
+
root: '',
|
|
52
|
+
list: {
|
|
53
|
+
format: 'html',
|
|
54
|
+
render: () => ''
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expectError<FastifyStaticOptions>({
|
|
59
|
+
root: '',
|
|
60
|
+
list: {
|
|
61
|
+
format: 'html',
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
35
65
|
appWithImplicitHttp
|
|
36
66
|
.register(fastifyStatic, options)
|
|
37
67
|
.after(() => {
|