Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extends send result to provide ability of custom handling #80

Merged
merged 3 commits into from
Jul 12, 2024
Merged
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
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