UNPKG

@httpland/range-request-middleware

Version:
154 lines (153 loc) 6.38 kB
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. // This module is browser compatible. var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _BytesRange_boundary; import { isIntRange, isNotEmpty, isNull, isNumber, isOtherRange, RangeHeader, RepresentationHeader, Status, stringifyContentRange, toHashString, } from "../deps.js"; import { RangeUnit, RequestedRangeNotSatisfiableResponse, shallowMergeHeaders, } from "../utils.js"; import { multipartByteranges } from "./utils.js"; /** {@link Range} implementation for `bytes` range unit. * It support single and multiple range request. * @see https://www.rfc-editor.org/rfc/rfc9110#section-14.1.2 * * @example * ```ts * import { * BytesRange, * type IntRange, * type SuffixRange, * } from "https://deno.land/x/range_request_middleware@$VERSION/mod.ts"; * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; * * const bytesRange = new BytesRange(); * const rangeUnit = "bytes"; * declare const initResponse: Response; * declare const rangeSet: [IntRange, SuffixRange]; * * const response = await bytesRange.respond(initResponse, { * rangeUnit, * rangeSet, * }); * * assertEquals(bytesRange.rangeUnit, rangeUnit); * assertEquals(response.status, 206); * assertEquals( * response.headers.get("content-type"), * "multipart/byteranges; boundary=<BOUNDARY>", * ); * ``` */ export class BytesRange { constructor(options) { _BytesRange_boundary.set(this, void 0); Object.defineProperty(this, "rangeUnit", { enumerable: true, configurable: true, writable: true, value: RangeUnit.Bytes }); __classPrivateFieldSet(this, _BytesRange_boundary, options?.computeBoundary ?? digestSha1, "f"); } respond(response, context) { if (context.rangeUnit !== this.rangeUnit) return response; return createPartialResponse(response, { ...context, computeBoundary: __classPrivateFieldGet(this, _BytesRange_boundary, "f"), }); } } _BytesRange_boundary = new WeakMap(); /** Create partial response from response. */ export async function createPartialResponse(response, context) { if (response.bodyUsed) return response; const content = await response .clone() .arrayBuffer(); const { rangeUnit, rangeSet } = context; const size = content.byteLength; const inclRanges = rangeSet .filter(isSupportedRanceSpec) .filter((rangeSpec) => isSatisfiable(rangeSpec, size)).map((rangeSpec) => rangeSpec2InclRange(rangeSpec, size)); if (!isNotEmpty(inclRanges)) { return new RequestedRangeNotSatisfiableResponse({ rangeUnit, completeLength: size, }, { headers: response.headers }); } if (inclRanges.length === 1) { const inclRange = inclRanges[0]; const partialBody = content.slice(inclRange.firstPos, inclRange.lastPos + 1); const contentRange = stringifyContentRange({ rangeUnit: RangeUnit.Bytes, ...inclRange, completeLength: size, }); const right = new Headers({ [RangeHeader.ContentRange]: contentRange }); const headers = shallowMergeHeaders(response.headers, right); return new Response(partialBody, { status: Status.PartialContent, headers, }); } const contentType = response.headers.get(RepresentationHeader.ContentType); if (isNull(contentType)) return response; const boundary = await context.computeBoundary(content); const newContentType = `multipart/byteranges; boundary=${boundary}`; const multipart = multipartByteranges({ content, contentType, ranges: inclRanges, boundary, rangeUnit: RangeUnit.Bytes, }); const right = new Headers({ [RepresentationHeader.ContentType]: newContentType, }); const headers = shallowMergeHeaders(response.headers, right); return new Response(multipart, { status: Status.PartialContent, headers }); } /** Whether the range spec is satisfiable or not. */ export function isSatisfiable(rangeSpec, contentLength) { if (isIntRange(rangeSpec)) { if (!contentLength) return false; return rangeSpec.firstPos < contentLength; } return !!rangeSpec.suffixLength; } export function isSupportedRanceSpec(rangeSpec) { return !isOtherRange(rangeSpec); } /** Convert {@link RangeSpec} into {@link InclRange}. */ export function rangeSpec2InclRange(rangeSpec, completeLength) { if (isIntRange(rangeSpec)) { const lastPos = !isNumber(rangeSpec.lastPos) || (isNumber(rangeSpec.lastPos) && completeLength <= rangeSpec.lastPos) ? completeLength ? completeLength - 1 : 0 : rangeSpec.lastPos; return { firstPos: rangeSpec.firstPos, lastPos }; } const firstPos = completeLength < rangeSpec.suffixLength ? 0 : completeLength - rangeSpec.suffixLength; const lastPos = completeLength ? completeLength - 1 : 0; return { firstPos, lastPos }; } export async function digestSha1(content) { const hash = await crypto .subtle .digest("SHA-1", content); return toHashString(hash); }