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 1 commit
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
Next Next commit
feat: extends send result to provide ability of custom handling
  • Loading branch information
climba03003 committed Jul 12, 2024
commit c9fb418bb02ed4b8ad82c5c8b879c76fa8bb6571
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)
45 changes: 34 additions & 11 deletions lib/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,10 @@ function sendError (statusCode, err) {
return {
statusCode,
headers,
stream: Readable.from(doc[0])
stream: Readable.from(doc[0]),
// metadata
type: 'error',
metadata: { error: err }
}
}

Expand All @@ -427,7 +430,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 +442,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 +504,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 +562,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 +606,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 +659,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
160 changes: 160 additions & 0 deletions test/send.3.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'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(2)

t.test('with metadata.error', 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('without metadata.error', 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.notOk(metadata.error)
t.notOk(metadata.requestPath)
res.writeHead(statusCode, headers)
stream.pipe(res)
})

request(app)
.get('/some%00thing.txt')
.expect(400, /Bad Request/, 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(500, err => t.error(err))

request(app)
.get('/pets/index.html')
.expect('Content-Disposition', 'attachment')
.expect(200, err => t.error(err))
})
})
28 changes: 27 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/// <reference types="node" />

import { Dirent } from "fs";
import * as stream from "stream";

/**
Expand Down Expand Up @@ -111,12 +112,37 @@ declare namespace send {
start?: number | undefined;
}

export interface SendResult {
export interface BaseSendResult {
statusCode: number
headers: Record<string, string>
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 }
Expand Down
Loading