@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 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: Multi-root is not supported within the `list` option.
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
- format: 'json' | 'html';
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 | ListOptions;
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 directoy without a trailing shash
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 detech how many slash before a valid path
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 {function(error, entries)} callback
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
- const files = await fs.readdir(dir)
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 {ListOptions} options
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 {ListOptions} options
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "6.1.0",
3
+ "version": "6.4.0",
4
4
  "description": "Plugin for serving static files as fast as possible.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -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` is invalid', t => {
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('dir list - url parameter format', t => {
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.ok(body.toString())
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
@@ -2840,7 +2840,7 @@ t.test('routes should fallback to default errorHandler', t => {
2840
2840
  })
2841
2841
  })
2842
2842
 
2843
- t.test('precent encoded URLs in glob mode', (t) => {
2843
+ t.test('percent encoded URLs in glob mode', (t) => {
2844
2844
  t.plan(4)
2845
2845
 
2846
2846
  const fastify = Fastify({})
@@ -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(() => {