Skip to content

Commit

Permalink
[NO-ISSUE] Add serveStatic middleware (#1)
Browse files Browse the repository at this point in the history
- feat: initial serveStatic implementation
- refactor: clean up examples.
- refactor: remove superfluous verification code given we have static type checking.
- refactor: remove unnecessary file path parsing.
- chore: remove old typings folder.
- chore: update docs, lockfile etc.
- feat: add benchmarks github action
- feat: update API docs with `serveStatic` middleware section
  • Loading branch information
asos-craigmorten authored May 25, 2020
1 parent 54ceed5 commit dc60ae7
Show file tree
Hide file tree
Showing 86 changed files with 1,694 additions and 841 deletions.
2 changes: 1 addition & 1 deletion .github/API/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Adapted from the [ExpressJS API Docs](https://expressjs.com/en/4x/api.html).
- [Request](./request.md)
- [Response](./response.md)
- [Router](./router.md)
- [Body Parsers](./bodyparser.md)
- [Middlewares](./middlewares.md)
130 changes: 98 additions & 32 deletions .github/API/bodyparser.md → .github/API/middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,104 @@ The following table describes the properties of the optional `options` object.
| `type` | This is used to determine what media type the middleware will parse. This option can be a string, array of strings, or a function. If not a function, `type` option is passed directly to the Deno [media_types](https://deno.land/x/media_types) library and this can be an extension name (like `json`), a mime type (like `application/json`), or a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type` option is called as `fn(req)` and the request is parsed if it returns a truthy value. | Mixed | `"application/json"` |
| `verify` | This option, if supplied, is called as `verify(req, res, buf, encoding)`, where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. | Function | `undefined` |

## raw([options])

This is a built-in middleware function in Opine. It parses incoming request payloads into a `Buffer` and is based on
[body-parser](http://expressjs.com/en/resources/middleware/body-parser.html).

Returns middleware that parses all bodies as a `Buffer` and only looks at requests where the `Content-Type` header matches the `type` option. This parser accepts any Unicode encoding of the body. It does not yet support automatic inflation of `gzip` nor `deflate` encodings.

A new `body` `Buffer` containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`), or an empty string (`""`) if there was no body to parse, the `Content-Type` was not matched, or an error
occurred.

> As `req.body`'s shape is based on user-controlled input, all properties and values in this object are untrusted and should be validated before trusting. For example, `req.body.toString()` may fail in multiple ways, for example stacking multiple parsers `req.body` may be from a different parser. Testing that `req.body` is a `Buffer` before calling buffer methods is recommended.
```ts
import { opine, raw } from "https://deno.land/x/opine@master/mod.ts";

const app = opine();

app.use(raw()); // for parsing application/octet-stream

app.post("/upload", function (req, res, next) {
console.log(req.body);
res.json(req.body);
});
```

The following table describes the properties of the optional `options` object.

| Property | Description | Type | Default |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
| `type` | This is used to determine what media type the middleware will parse. This option can be a string, array of strings, or a function. If not a function, `type` option is passed directly to the Deno [media_types](https://deno.land/x/media_types) library and this can be an extension name (like `bin`), a mime type (like `application/octet-stream`), or a mime type with a wildcard (like `*/*` or `application/*`). If a function, the `type` option is called as `fn(req)` and the request is parsed if it returns a truthy value. | Mixed | `"application/octet-stream"` |
| `verify` | This option, if supplied, is called as `verify(req, res, buf, encoding)`, where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. | Function | `undefined` |

## serveStatic(root, [options])

This is a built-in middleware function in Opine. It serves static files and is based on [serve-static](https://github.com/expressjs/serve-static/).

> NOTE: For best results, use a reverse proxy cache to improve performance of serving static assets.
The `root` argument specifies the root directory from which to serve static assets. The function determines the file to serve by combining `req.url` with the provided `root` directory. When a file is not found, instead of sending a 404 response, it instead calls `next()` to move on to the next middleware, allowing for stacking and fallbacks.

The following table describes the properties of the `options` object.

| Property | Description | Type | Default |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
| `fallthrough` | Let client errors fall-through as unhandled requests, otherwise forward a client error. See [fallthrough](#fallthrough) below. | Boolean | `true` |
| `redirect` | Redirect to trailing "/" when the pathname is a directory. | Boolean | `true` |
| `before` | Function for setting HTTP headers to serve with the file. See [before](#before) below. | Function | |

### fallthrough

When this option is `true`, client errors such as a bad request or a request to a non-existent file will cause this middleware to simply call `next()` to invoke the next middleware in the stack. When false, these errors (even 404s), will invoke `next(err)`.

Set this option to `true` so you can map multiple physical directories to the same web address or for routes to fill in non-existent files.

Use `false` if you have mounted this middleware at a path designed to be strictly a single file system directory, which allows for short-circuiting 404s for less overhead. This middleware will also reply to all methods.

### before

For this option, specify a function (async is supported) to make modifications to the response prior to the file being served via a `res.sendFile()`. The general use-case for this function hook is to set custom response headers. The signature of the function is:

```ts
fn(res, path, stat);
```

Arguments:

- `res`, the [response object](./response.md#response).
- `path`, the file path that is being sent.
- `stat`, the `stat` object of the file that is being sent.

For example:

```ts
const before = (res: Response, path: string, stat: Deno.FileInfo) => {
res.set("X-Timestamp", Date.now());
res.set("X-Resource-Path", path);
res.set("X-Resource-Size", stat.size);
};
```

### Example of serveStatic

Here is an example of using the `serveStatic` middleware function with an elaborate options object:

```ts
const options = {
fallthrough: false
redirect: false,
before(res: Response, path: string, stat: Deno.FileInfo) {
res.set("X-Timestamp", Date.now());
res.set("X-Resource-Path", path);
res.set("X-Resource-Size", stat.size);
},
};

app.use(serveStatic("public", options));
```

## text([options])

This is a built-in middleware function in Opine. It parses incoming request payloads into a string and is based on [body-parser](http://expressjs.com/en/resources/middleware/body-parser.html).
Expand Down Expand Up @@ -94,35 +192,3 @@ The following table describes the properties of the optional `options` object.
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------- |
| `type` | This is used to determine what media type the middleware will parse. This option can be a string, array of strings, or a function. If not a function, `type` option is passed directly to the Deno [media_types](https://deno.land/x/media_types) library and this can be an extension name (like `urlencoded`), a mime type (like `application/x-www-form-urlencoded`), or a mime type with a wildcard (like `*/x-www-form-urlencoded`). If a function, the `type` option is called as `fn(req)` and the request is parsed if it returns a truthy value. | Mixed | `"application/x-www-form-urlencoded"` |
| `verify` | This option, if supplied, is called as `verify(req, res, buf, encoding)`, where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. | Function | `undefined` |

## raw([options])

This is a built-in middleware function in Opine. It parses incoming request payloads into a `Buffer` and is based on
[body-parser](http://expressjs.com/en/resources/middleware/body-parser.html).

Returns middleware that parses all bodies as a `Buffer` and only looks at requests where the `Content-Type` header matches the `type` option. This parser accepts any Unicode encoding of the body. It does not yet support automatic inflation of `gzip` nor `deflate` encodings.

A new `body` `Buffer` containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`), or an empty string (`""`) if there was no body to parse, the `Content-Type` was not matched, or an error
occurred.

> As `req.body`'s shape is based on user-controlled input, all properties and values in this object are untrusted and should be validated before trusting. For example, `req.body.toString()` may fail in multiple ways, for example stacking multiple parsers `req.body` may be from a different parser. Testing that `req.body` is a `Buffer` before calling buffer methods is recommended.
```ts
import { opine, raw } from "https://deno.land/x/opine@master/mod.ts";

const app = opine();

app.use(raw()); // for parsing application/octet-stream

app.post("/upload", function (req, res, next) {
console.log(req.body);
res.json(req.body);
});
```

The following table describes the properties of the optional `options` object.

| Property | Description | Type | Default |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
| `type` | This is used to determine what media type the middleware will parse. This option can be a string, array of strings, or a function. If not a function, `type` option is passed directly to the Deno [media_types](https://deno.land/x/media_types) library and this can be an extension name (like `bin`), a mime type (like `application/octet-stream`), or a mime type with a wildcard (like `*/*` or `application/*`). If a function, the `type` option is called as `fn(req)` and the request is parsed if it returns a truthy value. | Mixed | `"application/octet-stream"` |
| `verify` | This option, if supplied, is called as `verify(req, res, buf, encoding)`, where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. | Function | `undefined` |
11 changes: 11 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# ChangeLog

## [0.4.0] - 25-05-2020

- feat: initial serveStatic implementation
- refactor: clean up examples.
- refactor: remove superfluous verification code given we have static type checking.
- refactor: remove unnecessary file path parsing.
- chore: remove old typings folder.
- chore: update docs, lockfile etc.
- feat: add benchmarks github action
- feat: update API docs with `serveStatic` middleware section

## [0.3.0] - 25-05-2020

### Updated
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Test

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
benchmark:
runs-on: ubuntu-latest

strategy:
matrix:
deno-version: [1.0.2]

steps:
- name: Install wrk package
run: |
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get install build-essential libssl-dev git -y
git clone https://github.com/wg/wrk.git wrk
cd wrk
make
# move the executable to somewhere in your PATH, ex:
sudo cp wrk /usr/local/bin
- uses: actions/checkout@v2
- name: Use Deno ${{ matrix.deno-version }}
uses: denolib/setup-deno@master
with:
deno-version: ${{ matrix.deno-version }}
- name: Run Benchmark Tests
run: |
results=$(echo "**Benchmark Results**:$(make benchmark)")
results="${results//'%'/'%25'}"
results="${results//$'\n'/'%0A'}"
results="${results//$'\r'/'%0D'}"
echo "::set-output name=results::$results"
id: run_benchmark_tests
- name: Post Results To PR
uses: mshick/add-pr-comment@v1
with:
message: ${{ steps.run_benchmark_tests.outputs.results }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
allow-repeats: false
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: benchmark build ci doc fmt fmt-check precommit test typedoc update-lock
.PHONY: benchmark build ci doc fmt fmt-check lock precommit test typedoc

benchmark:
@./benchmarks/run.sh 1 ./benchmarks/middleware.ts
Expand Down Expand Up @@ -28,18 +28,19 @@ fmt:
fmt-check:
@deno fmt --check

lock:
@deno run --lock=lock.json --lock-write --reload mod.ts

precommit:
@make typedoc
@make fmt
@make fmt
@make fmt
@make update-lock
@make lock

test:
@deno test --allow-net ./test/units/

typedoc:
@typedoc --ignoreCompilerErrors --out ./docs --mode modules --includeDeclarations --excludeExternals --includes ./typings/index.d.ts ./src
@typedoc --ignoreCompilerErrors --out ./docs --mode modules --includeDeclarations --excludeExternals ./src

update-lock:
@deno run --lock=lock.json --lock-write --reload mod.ts
4 changes: 2 additions & 2 deletions benchmarks/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ let middlewareCount: number = parseInt(Deno.env.get("MW") || "1");
console.log("%s middleware", middlewareCount);

while (middlewareCount--) {
app.use((_req: Request, _res: Response, next: NextFunction): void => {
app.use((req: Request, res: Response, next: NextFunction): void => {
next();
});
}

app.use((_req: Request, res: Response, _next: NextFunction): void => {
app.use((req: Request, res: Response, next: NextFunction): void => {
res.send("Hello World");
});

Expand Down
2 changes: 1 addition & 1 deletion benchmarks/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ wrk 'http://localhost:3333/?foo[bar]=baz' \
-c 50 \
-t 8 \
| grep 'Requests/sec' \
| awk '{ print " " $2 }'
| awk '{ print "\t" $2 " RPS" }'

kill $pid
1 change: 1 addition & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
extname,
fromFileUrl,
basename,
join,
} from "https://deno.land/std@0.52.0/path/mod.ts";
export { setImmediate } from "https://deno.land/std@0.52.0/node/timers.ts";
export { Sha1 } from "https://deno.land/std@0.52.0/hash/sha1.ts";
Expand Down
2 changes: 1 addition & 1 deletion docs/_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ future: true
encoding: "UTF-8"
include:
- "_*_.html"
- "_*_.*.html"
- "_*_.*.html"
2 changes: 1 addition & 1 deletion docs/assets/js/search.json

Large diffs are not rendered by default.

Loading

0 comments on commit dc60ae7

Please sign in to comment.