Skip to content

Commit

Permalink
Introduce @http/fs and fileBody()
Browse files Browse the repository at this point in the history
Restructure for cross-runtime file server
  • Loading branch information
jollytoad committed Jul 11, 2024
1 parent 2300b91 commit 8d4c5c1
Show file tree
Hide file tree
Showing 36 changed files with 481 additions and 256 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ This changelog will need to be split between individual packages

### Changed

- [@http/route-deno] use own `serveDir()`/`serveFile()` rather than
`@std/http/file-server`, preparing for cross-runtime support in a later
release
- [@http/route] introduced a cross-runtime `staticRoute()`
- [@http/route-deno] is now deprecated

### Added

- [@http/fs] new package for filesystem based functions
- [@http/fs] added `fileBody()` as a cross-runtime way to turn a file into a
Response body
- [@http/fs] added `serveDir()`/`serveFile()`, adapted from
`@std/http/file-server`
- [@http/response] add responses required by `serveDir`/`serveFile`

## [0.19.0]
Expand Down
7 changes: 6 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
]
},
"imports": {
"$test/generate/": "./packages/generate/_test/",
"@http/assert": "jsr:@http/assert@^0.19.0",
"@http/discovery": "jsr:@http/discovery@^0.19.0",
"@http/examples": "jsr:@http/examples@^0.19.0",
"@http/fs": "jsr:@http/fs@^0.19.0",
"@http/generate": "jsr:@http/generate@^0.19.0",
"@http/host-deno-deploy": "jsr:@http/host-deno-deploy@^0.19.0",
"@http/host-deno-local": "jsr:@http/host-deno-local@^0.19.0",
Expand All @@ -48,12 +48,17 @@
"@std/streams": "jsr:@std/streams@^1.0.0-rc.2",
"@std/testing": "jsr:@std/testing@^1.0.0-rc.3",
"@std/url": "jsr:@std/url@^1.0.0-rc.2",
"@types/bun": "npm:@types/bun@^1.1.6",
"@types/node": "npm:@types/node@^20.14.10",
"$test/generate/": "./packages/generate/_test/",
"bun-types": "npm:bun-types@^1.1.18",
"ts-poet": "npm:ts-poet@^6.9.0"
},
"workspaces": [
"./packages/assert",
"./packages/discovery",
"./packages/examples",
"./packages/fs",
"./packages/generate",
"./packages/host-deno-deploy",
"./packages/host-deno-local",
Expand Down
19 changes: 17 additions & 2 deletions import_map_local.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
"@http/examples/static-route": "./packages/examples/static_route.ts",
"@http/examples/verify-header": "./packages/examples/verify_header.ts",
"@http/examples/when-pattern": "./packages/examples/when_pattern.ts",
"@http/fs/file-body": "./packages/fs/file_body.ts",
"@http/fs/file-body-bun": "./packages/fs/file_body_bun.ts",
"@http/fs/file-body-deno": "./packages/fs/file_body_deno.ts",
"@http/fs/file-desc": "./packages/fs/file_desc.ts",
"@http/fs/file-not-found": "./packages/fs/file_not_found.ts",
"@http/fs/serve-dir": "./packages/fs/serve_dir.ts",
"@http/fs/serve-file": "./packages/fs/serve_file.ts",
"@http/fs/stat": "./packages/fs/stat.ts",
"@http/fs/types": "./packages/fs/types.ts",
"@http/generate/code-builder": "./packages/generate/code_builder.ts",
"@http/generate/default-handler-generator": "./packages/generate/default_handler_generator.ts",
"@http/generate/deno/write-module": "./packages/generate/deno/write_module.ts",
Expand All @@ -42,6 +51,7 @@
"@http/generate/node/write-module": "./packages/generate/node/write_module.ts",
"@http/generate/types": "./packages/generate/types.ts",
"@http/generate/write-module": "./packages/generate/write_module.ts",
"@http/host-deno-deploy/deno-deploy-etag": "./packages/host-deno-deploy/deno_deploy_etag.ts",
"@http/host-deno-deploy/init": "./packages/host-deno-deploy/init.ts",
"@http/host-deno-deploy/types": "./packages/host-deno-deploy/types.ts",
"@http/host-deno-local/init": "./packages/host-deno-local/init.ts",
Expand Down Expand Up @@ -91,9 +101,7 @@
"@http/response/set-headers": "./packages/response/set_headers.ts",
"@http/response/temporary-redirect": "./packages/response/temporary_redirect.ts",
"@http/response/unauthorized": "./packages/response/unauthorized.ts",
"@http/route-deno/deno-deploy-etag": "./packages/route-deno/deno_deploy_etag.ts",
"@http/route-deno/static-route": "./packages/route-deno/static_route.ts",
"@http/route-deno/types": "./packages/route-deno/types.ts",
"@http/route/as-url-pattern": "./packages/route/as_url_pattern.ts",
"@http/route/by-media-type": "./packages/route/by_media_type.ts",
"@http/route/by-method": "./packages/route/by_method.ts",
Expand All @@ -102,6 +110,7 @@
"@http/route/cascade": "./packages/route/cascade.ts",
"@http/route/handle": "./packages/route/handle.ts",
"@http/route/lazy": "./packages/route/lazy.ts",
"@http/route/static-route": "./packages/route/static_route.ts",
"@http/route/types": "./packages/route/types.ts",
"@http/route/with-fallback": "./packages/route/with_fallback.ts",
"@std/assert": "jsr:@std/assert@^1.0.0",
Expand All @@ -128,7 +137,13 @@
"@std/testing/": "jsr:/@std/testing@^1.0.0-rc.3/",
"@std/url": "jsr:@std/url@^1.0.0-rc.2",
"@std/url/": "jsr:/@std/url@^1.0.0-rc.2/",
"@types/bun": "npm:@types/bun@^1.1.6",
"@types/bun/": "npm:/@types/bun@^1.1.6/",
"@types/node": "npm:@types/node@^20.14.10",
"@types/node/": "npm:/@types/node@^20.14.10/",
"$test/generate/": "./packages/generate/_test/",
"bun-types": "npm:bun-types@^1.1.18",
"bun-types/": "npm:/bun-types@^1.1.18/",
"ts-poet": "npm:ts-poet@^6.9.0",
"ts-poet/": "npm:/ts-poet@^6.9.0/"
}
Expand Down
5 changes: 1 addition & 4 deletions packages/examples/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,14 @@ And in this example, we'll add the ability to serve up static files.

```sh
mkdir app/static
deno add @http/route-deno
```

(`@http/route-deno` contains the Deno specific router function `staticRoute`)

Create `app/handler.ts`:

```ts
import routes from "./routes.ts";
import { handle } from "@http/route/handle";
import { staticRoute } from "@http/route-deno/static-route";
import { staticRoute } from "@http/route/static-route";

export default handle([
routes,
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/intercept_response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* @module
*/

import { staticRoute } from "@http/route-deno/static-route";
import { staticRoute } from "@http/route/static-route";
import { withFallback } from "@http/route/with-fallback";
import { interceptResponse } from "@http/interceptor/intercept-response";
import { skip } from "@http/interceptor/skip";
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/static_route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @module
*/

import { staticRoute } from "@http/route-deno/static-route";
import { staticRoute } from "@http/route/static-route";
import { withFallback } from "@http/route/with-fallback";
import { port } from "@http/host-deno-local/port";

Expand Down
28 changes: 28 additions & 0 deletions packages/fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Filesystem functions for HTTP servers

Cross-runtime functions for interacting with the Filesystem from a HTTP server.

This package doesn't aim to cover all possible filesystem operations or file
data, just enough to aid in sending files to/from a HTTP server.

## `fileBody()`

Is a cross-runtime function to construct the body for a Response from a file
given its path and optionally start/end offsets. The actual return value of this
is a `BodyInit` type, ie. something suitable for passing into a `Response`. The
actual object may vary depending on the runtime.

## `serveDir()` & `serveFile()`

These functions have been copied from `@std/http/file-server` and adapted to fit
better with other `@http` functions, and to provide the basis for `staticRoute`.

They have some features stripped out that were present in
`@std/http/file-server`:

- No directory listing renderer
- No built-in support for Deno Deploy (see `denoDeployEtag()`)
- No CORS support (can use `cors()` interceptor instead)
- No custom headers added to response (can use an interceptor and
`appendHeaders()` instead)
- No standalone server
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
22 changes: 22 additions & 0 deletions packages/fs/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@http/fs",
"version": "0.19.0",
"exports": {
"./file-body": "./file_body.ts",
"./file-body-bun": "./file_body_bun.ts",
"./file-body-deno": "./file_body_deno.ts",
"./file-desc": "./file_desc.ts",
"./file-not-found": "./file_not_found.ts",
"./serve-dir": "./serve_dir.ts",
"./serve-file": "./serve_file.ts",
"./stat": "./stat.ts",
"./types": "./types.ts"
},
"publish": {
"exclude": [
"_test/**",
"_testdata/**",
"*.test.ts"
]
}
}
12 changes: 12 additions & 0 deletions packages/fs/file_body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { FileBodyFn } from "./types.ts";

/**
* Create a Response body for a given file
*/
export const fileBody: FileBodyFn = "Deno" in globalThis
? (await import("./file_body_deno.ts")).default
: "Bun" in globalThis
? (await import("./file_body_bun.ts")).default
: () => {
throw new Error("fileBody not supported on this runtime");
};
23 changes: 23 additions & 0 deletions packages/fs/file_body_bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference types="npm:bun-types@^1.1.6" />

import type { FileBodyOptions } from "./types.ts";

/**
* Create a Response body for a given file
*/
export function fileBodyBun(
filePath: string,
opts?: FileBodyOptions,
): Promise<BodyInit> {
const { start = 0, end } = opts ?? {};

let file = Bun.file(filePath);

if (start > 0 || end !== undefined) {
file = file.slice(start, end);
}

return Promise.resolve(file.stream() as ReadableStream<Uint8Array>);
}

export default fileBodyBun;
25 changes: 25 additions & 0 deletions packages/fs/file_body_deno.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ByteSliceStream } from "@std/streams/byte-slice-stream";
import type { FileBodyOptions } from "./types.ts";

/**
* Create a Response body for a given file
*/
export async function fileBodyDeno(
filePath: string,
opts?: FileBodyOptions,
): Promise<BodyInit> {
const { start = 0, end } = opts ?? {};
const file = await Deno.open(filePath);

if (start > 0) {
await file.seek(start, Deno.SeekMode.Start);
}

if (end !== undefined) {
return file.readable.pipeThrough(new ByteSliceStream(0, end - start));
}

return file.readable;
}

export default fileBodyDeno;
47 changes: 47 additions & 0 deletions packages/fs/file_desc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { FileDesc } from "./types.ts";

export type { FileDesc };

/**
* Does the given file descriptor represent a directory?
*
* @example
* ```ts
* import { stat } from "jsr:@http/fs/stat";
* import { isDirectory } from "jsr:@http/fs/file-desc";
*
* const fileInfo = await stat("./foo");
*
* if (isDirectory(fileInfo)) {
* ...
* }
* ```
*
* @param entry may be a Deno `FileInfo`, or Node `Stats` object
*/
export function isDirectory(entry: FileDesc): boolean {
return typeof entry.isDirectory === "function"
? entry.isDirectory()
: !!entry.isDirectory;
}

/**
* Does the given file descriptor represent a regular file?
*
* @example
* ```ts
* import { stat } from "jsr:@http/fs/stat";
* import { isFile } from "jsr:@http/fs/file-desc";
*
* const fileInfo = await stat("./foo");
*
* if (isFile(fileInfo)) {
* ...
* }
* ```
*
* @param entry may be a Deno `FileInfo`, or Node `Stats` object
*/
export function isFile(entry: FileDesc): boolean {
return typeof entry.isFile === "function" ? entry.isFile() : !!entry.isFile;
}
6 changes: 6 additions & 0 deletions packages/fs/file_not_found.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Check whether an error is a file not found
*/
export function fileNotFound(error: unknown): boolean {
return error instanceof Error && "code" in error && error.code === "ENOENT";
}
Loading

0 comments on commit 8d4c5c1

Please sign in to comment.