@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
@@ -0,0 +1,675 @@
1
+ 'use strict'
2
+
3
+ /* eslint n/no-deprecated-api: "off" */
4
+
5
+ const fs = require('fs')
6
+ const path = require('path')
7
+ const t = require('tap')
8
+ const simple = require('simple-get')
9
+ const Fastify = require('fastify')
10
+
11
+ const fastifyStatic = require('..')
12
+ const dirList = require('../lib/dirList')
13
+
14
+ const helper = {
15
+ arrange: function (t, options, f) {
16
+ return helper.arrangeModule(t, options, fastifyStatic, f)
17
+ },
18
+ arrangeModule: function (t, options, mock, f) {
19
+ const fastify = Fastify()
20
+ fastify.register(mock, options)
21
+ t.teardown(fastify.close.bind(fastify))
22
+ fastify.listen(0, err => {
23
+ t.error(err)
24
+ fastify.server.unref()
25
+ f('http://localhost:' + fastify.server.address().port)
26
+ })
27
+ return f
28
+ }
29
+ }
30
+
31
+ try {
32
+ fs.mkdirSync(path.join(__dirname, 'static/shallow/empty'))
33
+ } catch (error) {}
34
+
35
+ t.test('throws when `root` is an array', t => {
36
+ t.plan(2)
37
+
38
+ const err = dirList.validateOptions({ root: ['hello', 'world'], list: true })
39
+ t.type(err, TypeError)
40
+ t.equal(err.message, 'multi-root with list option is not supported')
41
+ })
42
+
43
+ t.test('throws when `list.format option` is invalid', t => {
44
+ t.plan(2)
45
+
46
+ const err = dirList.validateOptions({ list: { format: 'hello' } })
47
+ t.type(err, TypeError)
48
+ t.equal(err.message, 'The `list.format` option must be json or html')
49
+ })
50
+
51
+ t.test('throws when `list.names option` is not an array', t => {
52
+ t.plan(2)
53
+
54
+ const err = dirList.validateOptions({ list: { names: 'hello' } })
55
+ t.type(err, TypeError)
56
+ t.equal(err.message, 'The `list.names` option must be an array')
57
+ })
58
+
59
+ t.test('throws when `list.format` is html and `list render` is not a function', t => {
60
+ t.plan(2)
61
+
62
+ const err = dirList.validateOptions({ list: { format: 'html', render: 'hello' } })
63
+ t.type(err, TypeError)
64
+ t.equal(err.message, 'The `list.render` option must be a function and is required with html format')
65
+ })
66
+
67
+ t.test('dir list wrong options', t => {
68
+ t.plan(3)
69
+
70
+ const cases = [
71
+ {
72
+ options: {
73
+ root: path.join(__dirname, '/static'),
74
+ prefix: '/public',
75
+ list: {
76
+ format: 'no-json,no-html'
77
+ }
78
+ },
79
+ error: new TypeError('The `list.format` option must be json or html')
80
+ },
81
+ {
82
+ options: {
83
+ root: path.join(__dirname, '/static'),
84
+ list: {
85
+ format: 'html'
86
+ // no render function
87
+ }
88
+ },
89
+ error: new TypeError('The `list.render` option must be a function and is required with html format')
90
+ },
91
+ {
92
+ options: {
93
+ root: path.join(__dirname, '/static'),
94
+ list: {
95
+ names: 'not-an-array'
96
+ }
97
+ },
98
+ error: new TypeError('The `list.names` option must be an array')
99
+ }
100
+ ]
101
+
102
+ for (const case_ of cases) {
103
+ const fastify = Fastify()
104
+ fastify.register(fastifyStatic, case_.options)
105
+ fastify.listen(0, err => {
106
+ t.equal(err.message, case_.error.message)
107
+ fastify.server.unref()
108
+ })
109
+ }
110
+ })
111
+
112
+ t.test('dir list default options', t => {
113
+ t.plan(2)
114
+
115
+ const options = {
116
+ root: path.join(__dirname, '/static'),
117
+ prefix: '/public',
118
+ list: true
119
+ }
120
+ const route = '/public/shallow'
121
+ const content = { dirs: ['empty'], files: ['sample.jpg'] }
122
+
123
+ helper.arrange(t, options, (url) => {
124
+ t.test(route, t => {
125
+ t.plan(3)
126
+ simple.concat({
127
+ method: 'GET',
128
+ url: url + route
129
+ }, (err, response, body) => {
130
+ t.error(err)
131
+ t.equal(response.statusCode, 200)
132
+ t.equal(body.toString(), JSON.stringify(content))
133
+ })
134
+ })
135
+ })
136
+ })
137
+
138
+ t.test('dir list, custom options', t => {
139
+ t.plan(2)
140
+
141
+ const options = {
142
+ root: path.join(__dirname, '/static'),
143
+ prefix: '/public',
144
+ index: false,
145
+ list: true
146
+ }
147
+
148
+ const route = '/public/'
149
+ const content = { dirs: ['deep', 'shallow'], files: ['.example', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] }
150
+
151
+ helper.arrange(t, options, (url) => {
152
+ t.test(route, t => {
153
+ t.plan(3)
154
+ simple.concat({
155
+ method: 'GET',
156
+ url: url + route
157
+ }, (err, response, body) => {
158
+ t.error(err)
159
+ t.equal(response.statusCode, 200)
160
+ t.equal(body.toString(), JSON.stringify(content))
161
+ })
162
+ })
163
+ })
164
+ })
165
+
166
+ t.test('dir list html format', t => {
167
+ t.plan(6)
168
+
169
+ // render html in 2 ways: one with handlebars and one with template string
170
+
171
+ const Handlebars = require('handlebars')
172
+ const source = `
173
+ <html><body>
174
+ <ul>
175
+ {{#dirs}}
176
+ <li><a href="{{href}}">{{name}}</a></li>
177
+ {{/dirs}}
178
+ </ul>
179
+ <ul>
180
+ {{#files}}
181
+ <li><a href="{{href}}" target="_blank">{{name}}</a></li>
182
+ {{/files}}
183
+ </ul>
184
+ </body></html>
185
+ `
186
+ const handlebarTemplate = Handlebars.compile(source)
187
+ const templates = [
188
+ {
189
+ render: (dirs, files) => {
190
+ return handlebarTemplate({ dirs, files })
191
+ },
192
+ output: `
193
+ <html><body>
194
+ <ul>
195
+ <li><a href="/public/deep">deep</a></li>
196
+ <li><a href="/public/shallow">shallow</a></li>
197
+ </ul>
198
+ <ul>
199
+ <li><a href="/public/.example" target="_blank">.example</a></li>
200
+ <li><a href="/public/a .md" target="_blank">a .md</a></li>
201
+ <li><a href="/public/foo.html" target="_blank">foo.html</a></li>
202
+ <li><a href="/public/foobar.html" target="_blank">foobar.html</a></li>
203
+ <li><a href="/public/index.css" target="_blank">index.css</a></li>
204
+ <li><a href="/public/index.html" target="_blank">index.html</a></li>
205
+ </ul>
206
+ </body></html>
207
+ `
208
+ },
209
+
210
+ {
211
+ render: (dirs, files) => {
212
+ return `
213
+ <html><body>
214
+ <ul>
215
+ ${dirs.map(dir => `<li><a href="${dir.href}">${dir.name}</a></li>`).join('\n ')}
216
+ </ul>
217
+ <ul>
218
+ ${files.map(file => `<li><a href="${file.href}" target="_blank">${file.name}</a></li>`).join('\n ')}
219
+ </ul>
220
+ </body></html>
221
+ `
222
+ },
223
+ output: `
224
+ <html><body>
225
+ <ul>
226
+ <li><a href="/public/deep">deep</a></li>
227
+ <li><a href="/public/shallow">shallow</a></li>
228
+ </ul>
229
+ <ul>
230
+ <li><a href="/public/.example" target="_blank">.example</a></li>
231
+ <li><a href="/public/a .md" target="_blank">a .md</a></li>
232
+ <li><a href="/public/foo.html" target="_blank">foo.html</a></li>
233
+ <li><a href="/public/foobar.html" target="_blank">foobar.html</a></li>
234
+ <li><a href="/public/index.css" target="_blank">index.css</a></li>
235
+ <li><a href="/public/index.html" target="_blank">index.html</a></li>
236
+ </ul>
237
+ </body></html>
238
+ `
239
+ }
240
+
241
+ ]
242
+
243
+ for (const template of templates) {
244
+ const options = {
245
+ root: path.join(__dirname, '/static'),
246
+ prefix: '/public',
247
+ index: false,
248
+ list: {
249
+ format: 'html',
250
+ names: ['index', 'index.htm'],
251
+ render: template.render
252
+ }
253
+ }
254
+ const routes = ['/public/index.htm', '/public/index']
255
+
256
+ // check all routes by names
257
+
258
+ helper.arrange(t, options, (url) => {
259
+ for (const route of routes) {
260
+ t.test(route, t => {
261
+ t.plan(3)
262
+ simple.concat({
263
+ method: 'GET',
264
+ url: url + route
265
+ }, (err, response, body) => {
266
+ t.error(err)
267
+ t.equal(response.statusCode, 200)
268
+ t.equal(body.toString(), template.output)
269
+ })
270
+ })
271
+ }
272
+ })
273
+ }
274
+ })
275
+
276
+ t.test('dir list href nested structure', t => {
277
+ t.plan(6)
278
+
279
+ const options = {
280
+ root: path.join(__dirname, '/static'),
281
+ prefix: '/public',
282
+ index: false,
283
+ list: {
284
+ format: 'html',
285
+ names: ['index', 'index.htm'],
286
+ render (dirs, files) {
287
+ return dirs[0].href
288
+ }
289
+ }
290
+ }
291
+
292
+ const routes = [
293
+ { path: '/public/', response: '/public/deep' },
294
+ { path: '/public/index', response: '/public/deep' },
295
+ { path: '/public/deep/', response: '/public/deep/path' },
296
+ { path: '/public/deep/index.htm', response: '/public/deep/path' },
297
+ { path: '/public/deep/path/', response: '/public/deep/path/for' }
298
+ ]
299
+ helper.arrange(t, options, (url) => {
300
+ for (const route of routes) {
301
+ t.test(route.path, t => {
302
+ t.plan(5)
303
+ simple.concat({
304
+ method: 'GET',
305
+ url: url + route.path
306
+ }, (err, response, body) => {
307
+ t.error(err)
308
+ t.equal(response.statusCode, 200)
309
+ t.equal(body.toString(), route.response)
310
+ simple.concat({
311
+ method: 'GET',
312
+ url: url + body.toString()
313
+ }, (err, response, body) => {
314
+ t.error(err)
315
+ t.equal(response.statusCode, 200)
316
+ })
317
+ })
318
+ })
319
+ }
320
+ })
321
+ })
322
+
323
+ t.test('dir list html format - stats', t => {
324
+ t.plan(7)
325
+
326
+ const options1 = {
327
+ root: path.join(__dirname, '/static'),
328
+ prefix: '/public',
329
+ index: false,
330
+ list: {
331
+ format: 'html',
332
+ render (dirs, files) {
333
+ t.ok(dirs.length > 0)
334
+ t.ok(files.length > 0)
335
+
336
+ t.ok(dirs.every(every))
337
+ t.ok(files.every(every))
338
+
339
+ function every (value) {
340
+ return value.stats &&
341
+ value.stats.atime &&
342
+ !value.extendedInfo
343
+ }
344
+ }
345
+ }
346
+ }
347
+
348
+ const route = '/public/'
349
+
350
+ helper.arrange(t, options1, (url) => {
351
+ simple.concat({
352
+ method: 'GET',
353
+ url: url + route
354
+ }, (err, response, body) => {
355
+ t.error(err)
356
+ t.equal(response.statusCode, 200)
357
+ })
358
+ })
359
+ })
360
+
361
+ t.test('dir list html format - extended info', t => {
362
+ t.plan(4)
363
+
364
+ const route = '/public/'
365
+
366
+ const options = {
367
+ root: path.join(__dirname, '/static'),
368
+ prefix: '/public',
369
+ index: false,
370
+ list: {
371
+ format: 'html',
372
+ extendedFolderInfo: true,
373
+ render (dirs, files) {
374
+ t.test('dirs', t => {
375
+ t.plan(dirs.length * 7)
376
+
377
+ for (const value of dirs) {
378
+ t.ok(value.extendedInfo)
379
+
380
+ t.equal(typeof value.extendedInfo.fileCount, 'number')
381
+ t.equal(typeof value.extendedInfo.totalFileCount, 'number')
382
+ t.equal(typeof value.extendedInfo.folderCount, 'number')
383
+ t.equal(typeof value.extendedInfo.totalFolderCount, 'number')
384
+ t.equal(typeof value.extendedInfo.totalSize, 'number')
385
+ t.equal(typeof value.extendedInfo.lastModified, 'number')
386
+ }
387
+ })
388
+ }
389
+ }
390
+ }
391
+
392
+ helper.arrange(t, options, (url) => {
393
+ simple.concat({
394
+ method: 'GET',
395
+ url: url + route
396
+ }, (err, response, body) => {
397
+ t.error(err)
398
+ t.equal(response.statusCode, 200)
399
+ })
400
+ })
401
+ })
402
+
403
+ t.test('dir list json format', t => {
404
+ t.plan(2)
405
+
406
+ const options = {
407
+ root: path.join(__dirname, '/static'),
408
+ prefix: '/public',
409
+ prefixAvoidTrailingSlash: true,
410
+ list: {
411
+ format: 'json',
412
+ names: ['index', 'index.json', '/']
413
+ }
414
+ }
415
+ const routes = ['/public/shallow/']
416
+ const content = { dirs: ['empty'], files: ['sample.jpg'] }
417
+
418
+ helper.arrange(t, options, (url) => {
419
+ for (const route of routes) {
420
+ t.test(route, t => {
421
+ t.plan(3)
422
+ simple.concat({
423
+ method: 'GET',
424
+ url: url + route
425
+ }, (err, response, body) => {
426
+ t.error(err)
427
+ t.equal(response.statusCode, 200)
428
+ t.equal(body.toString(), JSON.stringify(content))
429
+ })
430
+ })
431
+ }
432
+ })
433
+ })
434
+
435
+ t.test('dir list json format - extended info', t => {
436
+ t.plan(2)
437
+
438
+ const options = {
439
+ root: path.join(__dirname, '/static'),
440
+ prefix: '/public',
441
+ prefixAvoidTrailingSlash: true,
442
+ list: {
443
+ format: 'json',
444
+ names: ['index', 'index.json', '/'],
445
+ extendedFolderInfo: true,
446
+ jsonFormat: 'extended'
447
+
448
+ }
449
+ }
450
+ const routes = ['/public/shallow/']
451
+
452
+ helper.arrange(t, options, (url) => {
453
+ for (const route of routes) {
454
+ t.test(route, t => {
455
+ t.plan(5)
456
+ simple.concat({
457
+ method: 'GET',
458
+ url: url + route
459
+ }, (err, response, body) => {
460
+ t.error(err)
461
+ t.equal(response.statusCode, 200)
462
+ const bodyObject = JSON.parse(body.toString())
463
+ t.equal(bodyObject.dirs[0].name, 'empty')
464
+ t.equal(typeof bodyObject.dirs[0].stats.atime, 'string')
465
+ t.equal(typeof bodyObject.dirs[0].extendedInfo.totalSize, 'number')
466
+ })
467
+ })
468
+ }
469
+ })
470
+ })
471
+
472
+ t.test('dir list - url parameter format', t => {
473
+ t.plan(13)
474
+
475
+ const options = {
476
+ root: path.join(__dirname, '/static'),
477
+ prefix: '/public',
478
+ index: false,
479
+ list: {
480
+ format: 'html',
481
+ render (dirs, files) {
482
+ return 'html'
483
+ }
484
+ }
485
+ }
486
+ const route = '/public/'
487
+
488
+ helper.arrange(t, options, (url) => {
489
+ simple.concat({
490
+ method: 'GET',
491
+ url: url + route
492
+ }, (err, response, body) => {
493
+ t.error(err)
494
+ t.equal(response.statusCode, 200)
495
+ t.equal(body.toString(), 'html')
496
+ t.ok(response.headers['content-type'].includes('text/html'))
497
+ })
498
+
499
+ simple.concat({
500
+ method: 'GET',
501
+ url: url + route + '?format=html'
502
+ }, (err, response, body) => {
503
+ t.error(err)
504
+ t.equal(response.statusCode, 200)
505
+ t.equal(body.toString(), 'html')
506
+ t.ok(response.headers['content-type'].includes('text/html'))
507
+ })
508
+
509
+ simple.concat({
510
+ method: 'GET',
511
+ url: url + route + '?format=json'
512
+ }, (err, response, body) => {
513
+ t.error(err)
514
+ t.equal(response.statusCode, 200)
515
+ t.ok(body.toString())
516
+ t.ok(response.headers['content-type'].includes('application/json'))
517
+ })
518
+ })
519
+ })
520
+
521
+ t.test('dir list on empty dir', t => {
522
+ t.plan(2)
523
+
524
+ const options = {
525
+ root: path.join(__dirname, '/static'),
526
+ prefix: '/public',
527
+ list: true
528
+ }
529
+ const route = '/public/shallow/empty'
530
+ const content = { dirs: [], files: [] }
531
+
532
+ helper.arrange(t, options, (url) => {
533
+ t.test(route, t => {
534
+ t.plan(3)
535
+ simple.concat({
536
+ method: 'GET',
537
+ url: url + route
538
+ }, (err, response, body) => {
539
+ t.error(err)
540
+ t.equal(response.statusCode, 200)
541
+ t.equal(body.toString(), JSON.stringify(content))
542
+ })
543
+ })
544
+ })
545
+ })
546
+
547
+ t.test('dir list serve index.html on index option', t => {
548
+ t.plan(2)
549
+
550
+ const options = {
551
+ root: path.join(__dirname, '/static'),
552
+ prefix: '/public',
553
+ index: false,
554
+ list: {
555
+ format: 'html',
556
+ names: ['index', 'index.html'],
557
+ render: () => 'dir list index'
558
+ }
559
+ }
560
+
561
+ helper.arrange(t, options, (url) => {
562
+ t.test('serve index.html from fs', t => {
563
+ t.plan(6)
564
+
565
+ let route = '/public/index.html'
566
+
567
+ simple.concat({
568
+ method: 'GET',
569
+ url: url + route
570
+ }, (err, response, body) => {
571
+ t.error(err)
572
+ t.equal(response.statusCode, 200)
573
+ t.equal(body.toString(), '<html>\n <body>\n the body\n </body>\n</html>\n')
574
+ })
575
+
576
+ route = '/public/index'
577
+ simple.concat({
578
+ method: 'GET',
579
+ url: url + route
580
+ }, (err, response, body) => {
581
+ t.error(err)
582
+ t.equal(response.statusCode, 200)
583
+ t.equal(body.toString(), 'dir list index')
584
+ })
585
+ })
586
+ })
587
+ })
588
+
589
+ t.test('serve a non existent dir and get error', t => {
590
+ t.plan(2)
591
+
592
+ const options = {
593
+ root: '/none',
594
+ prefix: '/public',
595
+ list: true
596
+ }
597
+ const route = '/public/'
598
+
599
+ helper.arrange(t, options, (url) => {
600
+ t.test(route, t => {
601
+ t.plan(2)
602
+ simple.concat({
603
+ method: 'GET',
604
+ url: url + route
605
+ }, (err, response, body) => {
606
+ t.error(err)
607
+ t.equal(response.statusCode, 404)
608
+ })
609
+ })
610
+ })
611
+ })
612
+
613
+ t.test('serve a non existent dir and get error', t => {
614
+ t.plan(2)
615
+
616
+ const options = {
617
+ root: path.join(__dirname, '/static'),
618
+ prefix: '/public',
619
+ list: {
620
+ names: ['index']
621
+ }
622
+ }
623
+ const route = '/public/none/index'
624
+
625
+ helper.arrange(t, options, (url) => {
626
+ t.test(route, t => {
627
+ t.plan(2)
628
+ simple.concat({
629
+ method: 'GET',
630
+ url: url + route
631
+ }, (err, response, body) => {
632
+ t.error(err)
633
+ t.equal(response.statusCode, 404)
634
+ })
635
+ })
636
+ })
637
+ })
638
+
639
+ t.test('dir list error', t => {
640
+ t.plan(7)
641
+
642
+ const options = {
643
+ root: path.join(__dirname, '/static'),
644
+ prefix: '/public',
645
+ prefixAvoidTrailingSlash: true,
646
+ index: false,
647
+ list: {
648
+ format: 'html',
649
+ names: ['index', 'index.htm'],
650
+ render: () => ''
651
+ }
652
+ }
653
+
654
+ const errorMessage = 'mocking send'
655
+ dirList.send = async () => { throw new Error(errorMessage) }
656
+
657
+ const mock = t.mock('..', {
658
+ '../lib/dirList.js': dirList
659
+ })
660
+
661
+ const routes = ['/public/', '/public/index.htm']
662
+
663
+ helper.arrangeModule(t, options, mock, (url) => {
664
+ for (const route of routes) {
665
+ simple.concat({
666
+ method: 'GET',
667
+ url: url + route
668
+ }, (err, response, body) => {
669
+ t.error(err)
670
+ t.equal(JSON.parse(body.toString()).message, errorMessage)
671
+ t.equal(response.statusCode, 500)
672
+ })
673
+ }
674
+ })
675
+ })
@@ -0,0 +1 @@
1
+ contents of a dotfile
@@ -0,0 +1 @@
1
+ example
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ inner index.html
4
+ </body>
5
+ </html>
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ the deep path for test purpose body
4
+ </body>
5
+ </html>
@@ -0,0 +1,3 @@
1
+ <html>
2
+ <body>foo</body>
3
+ </html>
@@ -0,0 +1,3 @@
1
+ <html>
2
+ <body>foobar</body>
3
+ </html>
File without changes
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ the body
4
+ </body>
5
+ </html>
Binary file