From 1926b027502c1666661bc3e012eaf8b0e4faf13f Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 10 Mar 2023 11:11:29 +0900 Subject: [PATCH] feat(middleware): add range middleware factory --- _dev_deps.ts | 18 +++++++++++ deps.ts | 57 ++++++++++++++++++++++++++++++++ middleware.ts | 20 ++++++++++++ middleware_test.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 _dev_deps.ts create mode 100644 deps.ts create mode 100644 middleware.ts create mode 100644 middleware_test.ts diff --git a/_dev_deps.ts b/_dev_deps.ts new file mode 100644 index 0000000..f547664 --- /dev/null +++ b/_dev_deps.ts @@ -0,0 +1,18 @@ +export { + assert, + assertEquals, + assertThrows, +} from "https://deno.land/std@0.178.0/testing/asserts.ts"; +export { describe, it } from "https://deno.land/std@0.178.0/testing/bdd.ts"; +export { + assertSpyCall, + assertSpyCalls, + spy, +} from "https://deno.land/std@0.178.0/testing/mock.ts"; +export { Status } from "https://deno.land/std@0.178.0/http/http_status.ts"; +export { equalsResponse } from "https://deno.land/x/http_utils@1.0.0-beta.13/response.ts"; +export { + ConditionalHeader, + RangeHeader, + RepresentationHeader, +} from "https://deno.land/x/http_utils@1.0.0-beta.13/header.ts"; diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..0051cd5 --- /dev/null +++ b/deps.ts @@ -0,0 +1,57 @@ +// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. +// This module is browser compatible. + +export { Status } from "https://deno.land/std@0.178.0/http/http_status.ts"; +export { + isNull, + isNumber, + isString, +} from "https://deno.land/x/isx@1.0.0-beta.24/mod.ts"; +export { type Middleware } from "https://deno.land/x/http_middleware@1.0.0-beta.1/mod.ts"; +export { + ConditionalHeader, + RangeHeader, + RepresentationHeader, +} from "https://deno.land/x/http_utils@1.0.0-beta.12/header.ts"; +export { Method } from "https://deno.land/x/http_utils@1.0.0-beta.12/method.ts"; +export { isErr, unsafe } from "https://deno.land/x/result_js@1.0.0/mod.ts"; +import { + RepresentationHeader, +} from "https://deno.land/x/http_utils@1.0.0-beta.12/header.ts"; +export { + type IntRange, + isIntRange, + isOtherRange, + isSuffixRange, + type OtherRange, + parse, + type Range, + type RangeSpec, + type RangesSpecifier, + type SuffixRange, +} from "https://deno.land/x/range_parser@1.0.0/mod.ts"; +export { concat } from "https://deno.land/std@0.178.0/bytes/concat.ts"; +export { toHashString } from "https://deno.land/std@0.178.0/crypto/to_hash_string.ts"; + +/** Create headers with no representation header fields. */ +export class NoContentHeaders extends Headers { + constructor(init?: HeadersInit | undefined) { + super(init); + + const allHeaders = [ + RepresentationHeader.ContentEncoding, + RepresentationHeader.ContentLanguage, + RepresentationHeader.ContentLength, + RepresentationHeader.ContentLocation, + RepresentationHeader.ContentType, + RepresentationHeader.ETag, + RepresentationHeader.LastModified, + ]; + + allHeaders.forEach(this.delete.bind(this)); + } +} + +export function isNotEmpty(input: readonly T[]): input is [T, ...T[]] { + return !!input.length; +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..2f7bbf8 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,20 @@ +// deno-lint-ignore-file no-explicit-any + +import { type Middleware } from "./deps.ts"; +import { withContentRange } from "./transform.ts"; +import { BytesRange } from "./ranges/bytes.ts"; +import type { Range } from "./types.ts"; + +interface Options { + readonly ranges?: readonly Range[]; +} + +export function range(options?: Options): Middleware { + const ranges = options?.ranges ?? [new BytesRange() as Range]; + + return async (request, next) => { + const response = await next(request); + + return withContentRange(request, response, { ranges }); + }; +} diff --git a/middleware_test.ts b/middleware_test.ts new file mode 100644 index 0000000..6a206f3 --- /dev/null +++ b/middleware_test.ts @@ -0,0 +1,81 @@ +import { range } from "./middleware.ts"; +import { + assert, + describe, + equalsResponse, + it, + RangeHeader, + RepresentationHeader, + Status, +} from "./_dev_deps.ts"; + +describe("range", () => { + it("should", async () => { + const middleware = range(); + const rangeRequest = new Request("test:", { + headers: { range: "bytes=5-9" }, + }); + const response = await middleware( + rangeRequest, + () => new Response("abcdefghijklmnopqrstuvwxyz"), + ); + + assert( + await equalsResponse( + response, + new Response(`fghij`, { + status: Status.PartialContent, + headers: { + [RangeHeader.ContentRange]: `bytes 5-9/26`, + }, + }), + true, + ), + ); + }); + + it("should", async () => { + const middleware = range(); + const rangeRequest = new Request("test:", { + headers: { range: "bytes=5-9, 20-, -5" }, + }); + const response = await middleware( + rangeRequest, + () => new Response("abcdefghijklmnopqrstuvwxyz"), + ); + + const boundary = `32d10c7b8cf96570ca04ce37f2a19d84240d3a89`; + + assert( + await equalsResponse( + response, + new Response( + `--${boundary} +Content-Type: text/plain;charset=UTF-8 +Content-Range: 5-9/26 + +fghij +--${boundary} +Content-Type: text/plain;charset=UTF-8 +Content-Range: 20-25/26 + +uvwxyz +--${boundary} +Content-Type: text/plain;charset=UTF-8 +Content-Range: 21-25/26 + +vwxyz +--${boundary}--`, + { + status: Status.PartialContent, + headers: { + [RepresentationHeader.ContentType]: + `multipart/byteranges; boundary=${boundary}`, + }, + }, + ), + true, + ), + ); + }); +});