diff --git a/README.md b/README.md
index acf1d51..f3d090d 100644
--- a/README.md
+++ b/README.md
@@ -216,6 +216,75 @@ var server = http.createServer(function onRequest (req, res) {
server.listen(3000)
```
+### Custom directory index view
+
+This is an example of serving up a structure of directories with a
+custom function to render a listing of a directory.
+
+```js
+var http = require('node:http')
+var fs = require('node:fs')
+var parseUrl = require('parseurl')
+var send = require('@fastify/send')
+
+// Transfer arbitrary files from within /www/example.com/public/*
+// with a custom handler for directory listing
+var server = http.createServer(async function onRequest (req, res) {
+ const { statusCode, headers, stream, type, metadata } = await send(req, parseUrl(req).pathname, { index: false, root: '/www/public' })
+ if(type === 'directory') {
+ // get directory list
+ const list = await readdir(metadata.path)
+ // render an index for the directory
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=UTF-8' })
+ res.end(list.join('\n') + '\n')
+ } else {
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ }
+})
+
+server.listen(3000)
+```
+
+### Serving from a root directory with custom error-handling
+
+```js
+var http = require('node:http')
+var parseUrl = require('parseurl')
+var send = require('@fastify/send')
+
+var server = http.createServer(async function onRequest (req, res) {
+ // transfer arbitrary files from within
+ // /www/example.com/public/*
+ const { statusCode, headers, stream, type, metadata } = await send(req, parseUrl(req).pathname, { root: '/www/public' })
+ switch (type) {
+ case 'directory': {
+ // your custom directory handling logic:
+ res.writeHead(301, {
+ 'Location': metadata.requestPath + '/'
+ })
+ res.end('Redirecting to ' + metadata.requestPath + '/')
+ break
+ }
+ case 'error': {
+ // your custom error-handling logic:
+ res.writeHead(metadata.error.status ?? 500, {})
+ res.end(metadata.error.message)
+ break
+ }
+ default: {
+ // your custom headers
+ // serve all files for download
+ res.setHeader('Content-Disposition', 'attachment')
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ }
+ }
+})
+
+server.listen(3000)
+```
+
## License
[MIT](LICENSE)
diff --git a/lib/createHttpError.js b/lib/createHttpError.js
new file mode 100644
index 0000000..ba7bcca
--- /dev/null
+++ b/lib/createHttpError.js
@@ -0,0 +1,23 @@
+'use strict'
+
+const createError = require('http-errors')
+
+/**
+ * Create a HttpError object from simple arguments.
+ *
+ * @param {number} status
+ * @param {Error|object} err
+ * @private
+ */
+
+function createHttpError (status, err) {
+ if (!err) {
+ return createError(status)
+ }
+
+ return err instanceof Error
+ ? createError(status, err, { expose: false })
+ : createError(status, err)
+}
+
+module.exports.createHttpError = createHttpError
diff --git a/lib/send.js b/lib/send.js
index f98d375..226dd6f 100644
--- a/lib/send.js
+++ b/lib/send.js
@@ -18,6 +18,7 @@ const { isUtf8MimeType } = require('../lib/isUtf8MimeType')
const { normalizeList } = require('../lib/normalizeList')
const { parseBytesRange } = require('../lib/parseBytesRange')
const { parseTokenList } = require('./parseTokenList')
+const { createHttpError } = require('./createHttpError')
/**
* Path function references.
@@ -403,7 +404,10 @@ function sendError (statusCode, err) {
return {
statusCode,
headers,
- stream: Readable.from(doc[0])
+ stream: Readable.from(doc[0]),
+ // metadata
+ type: 'error',
+ metadata: { error: createHttpError(statusCode, err) }
}
}
@@ -427,7 +431,7 @@ function sendStatError (err) {
* @api private
*/
-function sendNotModified (headers) {
+function sendNotModified (headers, path, stat) {
debug('not modified')
delete headers['Content-Encoding']
@@ -439,7 +443,10 @@ function sendNotModified (headers) {
return {
statusCode: 304,
headers,
- stream: Readable.from('')
+ stream: Readable.from(''),
+ // metadata
+ type: 'file',
+ metadata: { path, stat }
}
}
@@ -498,7 +505,7 @@ function sendFileDirectly (request, path, stat, options) {
}
if (isNotModifiedFailure(request, headers)) {
- return sendNotModified(headers)
+ return sendNotModified(headers, path, stat)
}
}
@@ -556,7 +563,14 @@ function sendFileDirectly (request, path, stat, options) {
// HEAD support
if (request.method === 'HEAD') {
- return { statusCode, headers, stream: Readable.from('') }
+ return {
+ statusCode,
+ headers,
+ stream: Readable.from(''),
+ // metadata
+ type: 'file',
+ metadata: { path, stat }
+ }
}
const stream = fs.createReadStream(path, {
@@ -564,15 +578,22 @@ function sendFileDirectly (request, path, stat, options) {
end: Math.max(offset, offset + len - 1)
})
- return { statusCode, headers, stream }
+ return {
+ statusCode,
+ headers,
+ stream,
+ // metadata
+ type: 'file',
+ metadata: { path, stat }
+ }
}
-function sendRedirect (path) {
- if (hasTrailingSlash(path)) {
+function sendRedirect (path, options) {
+ if (hasTrailingSlash(options.path)) {
return sendError(403)
}
- const loc = encodeURI(collapseLeadingSlashes(path + '/'))
+ const loc = encodeURI(collapseLeadingSlashes(options.path + '/'))
const doc = createHtmlDocument('Redirecting', 'Redirecting to ' +
escapeHtml(loc) + '')
@@ -586,7 +607,10 @@ function sendRedirect (path) {
return {
statusCode: 301,
headers,
- stream: Readable.from(doc[0])
+ stream: Readable.from(doc[0]),
+ // metadata
+ type: 'directory',
+ metadata: { requestPath: options.path, path }
}
}
@@ -636,7 +660,7 @@ async function sendFile (request, path, options) {
return sendError(404)
}
if (error) return sendStatError(error)
- if (stat.isDirectory()) return sendRedirect(options.path)
+ if (stat.isDirectory()) return sendRedirect(path, options)
return sendFileDirectly(request, path, stat, options)
}
diff --git a/package.json b/package.json
index ce9fb38..4e7e356 100644
--- a/package.json
+++ b/package.json
@@ -22,10 +22,11 @@
"server"
],
"dependencies": {
+ "@lukeed/ms": "^2.0.2",
"escape-html": "~1.0.3",
"fast-decode-uri-component": "^1.0.1",
- "mime": "^3",
- "@lukeed/ms": "^2.0.2"
+ "http-errors": "^2.0.0",
+ "mime": "^3"
},
"devDependencies": {
"@fastify/pre-commit": "^2.1.0",
diff --git a/test/send.3.test.js b/test/send.3.test.js
new file mode 100644
index 0000000..8fa0332
--- /dev/null
+++ b/test/send.3.test.js
@@ -0,0 +1,137 @@
+'use strict'
+
+const { test } = require('tap')
+const http = require('node:http')
+const path = require('node:path')
+const request = require('supertest')
+const { readdir } = require('node:fs/promises')
+const send = require('../lib/send').send
+
+const fixtures = path.join(__dirname, 'fixtures')
+
+test('send(file)', function (t) {
+ t.plan(5)
+
+ t.test('file type', function (t) {
+ t.plan(6)
+
+ const app = http.createServer(async function (req, res) {
+ const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
+ t.equal(type, 'file')
+ t.ok(metadata.path)
+ t.ok(metadata.stat)
+ t.notOk(metadata.error)
+ t.notOk(metadata.requestPath)
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ })
+
+ request(app)
+ .get('/name.txt')
+ .expect('Content-Length', '4')
+ .expect(200, 'tobi', err => t.error(err))
+ })
+
+ t.test('directory type', function (t) {
+ t.plan(6)
+
+ const app = http.createServer(async function (req, res) {
+ const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
+ t.equal(type, 'directory')
+ t.ok(metadata.path)
+ t.notOk(metadata.stat)
+ t.notOk(metadata.error)
+ t.ok(metadata.requestPath)
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ })
+
+ request(app)
+ .get('/pets')
+ .expect('Location', '/pets/')
+ .expect(301, err => t.error(err))
+ })
+
+ t.test('error type', function (t) {
+ t.plan(6)
+
+ const app = http.createServer(async function (req, res) {
+ const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
+ t.equal(type, 'error')
+ t.notOk(metadata.path)
+ t.notOk(metadata.stat)
+ t.ok(metadata.error)
+ t.notOk(metadata.requestPath)
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ })
+
+ const path = Array(100).join('foobar')
+ request(app)
+ .get('/' + path)
+ .expect(404, err => t.error(err))
+ })
+
+ t.test('custom directory index view', function (t) {
+ t.plan(1)
+
+ const app = http.createServer(async function (req, res) {
+ const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
+ if (type === 'directory') {
+ const list = await readdir(metadata.path)
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=UTF-8' })
+ res.end(list.join('\n') + '\n')
+ } else {
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ }
+ })
+
+ request(app)
+ .get('/pets')
+ .expect('Content-Type', 'text/plain; charset=UTF-8')
+ .expect(200, '.hidden\nindex.html\n', err => t.error(err))
+ })
+
+ t.test('serving from a root directory with custom error-handling', function (t) {
+ t.plan(3)
+
+ const app = http.createServer(async function (req, res) {
+ const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
+ switch (type) {
+ case 'directory': {
+ res.writeHead(301, {
+ Location: metadata.requestPath + '/'
+ })
+ res.end('Redirecting to ' + metadata.requestPath + '/')
+ break
+ }
+ case 'error': {
+ res.writeHead(metadata.error.status ?? 500, {})
+ res.end(metadata.error.message)
+ break
+ }
+ default: {
+ // serve all files for download
+ res.setHeader('Content-Disposition', 'attachment')
+ res.writeHead(statusCode, headers)
+ stream.pipe(res)
+ }
+ }
+ })
+
+ request(app)
+ .get('/pets')
+ .expect('Location', '/pets/')
+ .expect(301, err => t.error(err))
+
+ request(app)
+ .get('/not-exists')
+ .expect(404, err => t.error(err))
+
+ request(app)
+ .get('/pets/index.html')
+ .expect('Content-Disposition', 'attachment')
+ .expect(200, err => t.error(err))
+ })
+})
diff --git a/types/index.d.ts b/types/index.d.ts
index 7f93448..31cd91d 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -4,6 +4,7 @@
///
+import { Dirent } from "fs";
import * as stream from "stream";
/**
@@ -111,12 +112,37 @@ declare namespace send {
start?: number | undefined;
}
- export interface SendResult {
+ export interface BaseSendResult {
statusCode: number
headers: Record
stream: stream.Readable
}
+ export interface FileSendResult extends BaseSendResult {
+ type: 'file'
+ metadata: {
+ path: string
+ stat: Dirent
+ }
+ }
+
+ export interface DirectorySendResult extends BaseSendResult {
+ type: 'directory'
+ metadata: {
+ path: string
+ requestPath: string
+ }
+ }
+
+ export interface ErrorSendResult extends BaseSendResult {
+ type: 'error'
+ metadata: {
+ error: Error
+ }
+ }
+
+ export type SendResult = FileSendResult | DirectorySendResult | ErrorSendResult
+
export const send: Send
export { send as default }
diff --git a/types/index.test-d.ts b/types/index.test-d.ts
index 2fc1d4c..783ceba 100644
--- a/types/index.test-d.ts
+++ b/types/index.test-d.ts
@@ -1,6 +1,7 @@
+import { Dirent } from 'fs';
import { Readable } from 'stream';
import { expectType } from 'tsd';
-import send, { SendResult } from '..';
+import send, { DirectorySendResult, ErrorSendResult, FileSendResult, SendResult } from '..';
send.mime.define({
'application/x-my-type': ['x-mt', 'x-mtt']
@@ -33,3 +34,23 @@ const req: any = {}
expectType(result.stream)
}
+
+const result = await send(req, '/test.html')
+switch (result.type) {
+ case 'file': {
+ expectType(result)
+ expectType(result.metadata.path)
+ expectType(result.metadata.stat)
+ break
+ }
+ case 'directory': {
+ expectType(result)
+ expectType(result.metadata.path)
+ expectType(result.metadata.requestPath)
+ break
+ }
+ case 'error': {
+ expectType(result)
+ expectType(result.metadata.error)
+ }
+}
\ No newline at end of file