Skip to content

Commit

Permalink
feat: extends send result to provide ability of custom handling (#80)
Browse files Browse the repository at this point in the history
* feat: extends send result to provide ability of custom handling

* feat: ensure error exists

* fixup
  • Loading branch information
climba03003 authored Jul 12, 2024
1 parent ab277e4 commit d28f7d3
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 15 deletions.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
23 changes: 23 additions & 0 deletions lib/createHttpError.js
Original file line number Diff line number Diff line change
@@ -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
46 changes: 35 additions & 11 deletions lib/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) }
}
}

Expand All @@ -427,7 +431,7 @@ function sendStatError (err) {
* @api private
*/

function sendNotModified (headers) {
function sendNotModified (headers, path, stat) {
debug('not modified')

delete headers['Content-Encoding']
Expand All @@ -439,7 +443,10 @@ function sendNotModified (headers) {
return {
statusCode: 304,
headers,
stream: Readable.from('')
stream: Readable.from(''),
// metadata
type: 'file',
metadata: { path, stat }
}
}

Expand Down Expand Up @@ -498,7 +505,7 @@ function sendFileDirectly (request, path, stat, options) {
}

if (isNotModifiedFailure(request, headers)) {
return sendNotModified(headers)
return sendNotModified(headers, path, stat)
}
}

Expand Down Expand Up @@ -556,23 +563,37 @@ 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, {
start: offset,
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 <a href="' + escapeHtml(loc) + '">' +
escapeHtml(loc) + '</a>')

Expand All @@ -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 }
}
}

Expand Down Expand Up @@ -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)
}

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
137 changes: 137 additions & 0 deletions test/send.3.test.js
Original file line number Diff line number Diff line change
@@ -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))
})
})
Loading

0 comments on commit d28f7d3

Please sign in to comment.