Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ Default: `json`
Options: `html`, `json`

Directory list can be in `html` format; in that case, `list.render` function is required.
Directory list in `json` format can also use `list.render` to customize the JSON response.
When `list.render` is omitted, the JSON response is controlled by `list.jsonFormat`.
The `list.render` function receives `(dirs, files, format)`, where `format` is `html` or `json`.

This option can be overridden by the URL parameter `format`. Options are `html` and `json`.

Expand Down Expand Up @@ -340,6 +343,26 @@ fastify.register(require('@fastify/static'), {
})
```

JSON render example:

```js
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/public/',
list: {
format: 'json',
render: (dirs, files) => {
return {
dirs: dirs.map(dir => dir.name),
images: files
.filter(file => file.name.endsWith('.png'))
.map(file => ({ name: file.name, href: file.href }))
}
}
}
})
```

Request

```bash
Expand Down
26 changes: 23 additions & 3 deletions lib/dirList.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ const dirList = {
}

const format = reply.request.query.format || options.format
const renderFormat = format === 'html' ? 'html' : 'json'
if (format !== 'html') {
if (typeof options.render === 'function' && options.format !== 'html') {
await reply.send(dirList.render(entries, route, prefix, options, renderFormat))
return
}

if (options.jsonFormat !== 'extended') {
const nameEntries = { dirs: [], files: [] }
entries.dirs.forEach(entry => nameEntries.dirs.push(entry.name))
Expand All @@ -142,12 +148,26 @@ const dirList = {
return
}

const html = options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route, prefix, options)),
entries.files.map(entry => dirList.htmlInfo(entry, route, prefix, options)))
const html = dirList.render(entries, route, prefix, options, renderFormat)
await reply.type('text/html').send(html)
},

/**
* render directory entries
* @param {ReturnType<typeof dirList.list>} entries directory list entries
* @param {string} route request route
* @param {string} prefix static prefix
* @param {(ListOptionsJsonFormat | ListOptionsHtmlFormat)} options
* @param {'html' | 'json'} format response format
* @return {unknown}
*/
render: function (entries, route, prefix, options, format) {
return options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route, prefix, options)),
entries.files.map(entry => dirList.htmlInfo(entry, route, prefix, options)),
format)
},

/**
* provide the html information about entry and route, to get name and full route
* @param entry file or dir name and stats
Expand Down
55 changes: 53 additions & 2 deletions test/dir-list.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,50 @@ test('dir list json format - extended info', async t => {
})
})

test('dir list json format - render', async t => {
t.plan(1)

const options = {
root: path.join(__dirname, '/static'),
prefix: '/public',
prefixAvoidTrailingSlash: true,
list: {
format: 'json',
names: ['index', 'index.json', '/'],
render (dirs, files) {
return {
dirs: dirs.map(dir => dir.name),
images: files
.filter(file => file.name.endsWith('.jpg'))
.map(file => ({ name: file.name, href: file.href }))
}
}
}
}
const route = '/public/shallow/'
const content = {
dirs: ['empty'],
images: [
{
name: 'sample.jpg',
href: '/public/shallow/sample.jpg'
}
]
}

await helper.arrange(t, options, async (url) => {
await t.test(route, async t => {
t.plan(4)

const response = await fetch(url + route)
t.assert.ok(response.ok)
t.assert.deepStrictEqual(response.status, 200)
t.assert.deepStrictEqual(await response.json(), content)
t.assert.ok(response.headers.get('content-type').includes('application/json'))
})
})
})

test('json format with url parameter format', async t => {
t.plan(12)

Expand All @@ -506,8 +550,15 @@ test('json format with url parameter format', async t => {
index: false,
list: {
format: 'json',
render () {
return 'html'
render (dirs, files, format) {
if (format === 'html') {
return 'html'
}

return {
dirs: dirs.map(dir => dir.name),
files: files.map(file => file.name)
}
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,14 @@ declare namespace fastifyStatic {
stats: Stats;
}

export type ListFormat = 'html' | 'json'

export interface ListRender {
(dirs: ListDir[], files: ListFile[]): string;
(dirs: ListDir[], files: ListFile[], format?: ListFormat): string;
}

export interface ListJsonRender {
(dirs: ListDir[], files: ListFile[], format?: ListFormat): unknown;
}

export interface ListOptions {
Expand All @@ -57,7 +63,7 @@ declare namespace fastifyStatic {
export interface ListOptionsJsonFormat extends ListOptions {
format: 'json';
// Required when the URL parameter `format=html` exists
render?: ListRender;
render?: ListJsonRender;
}

export interface ListOptionsHtmlFormat extends ListOptions {
Expand Down
9 changes: 9 additions & 0 deletions types/index.tst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ expect<FastifyStaticOptions>()
}
})

expect<FastifyStaticOptions>()
.type.toBeAssignableFrom({
root: '',
list: {
format: 'json' as const,
render: () => ({ files: [] })
}
})

expect<FastifyStaticOptions>()
.type.toBeAssignableFrom({
root: '',
Expand Down
Loading