UNPKG

@httpland/range-request-middleware

Version:
163 lines (162 loc) 6.93 kB
"use strict"; // 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; Object.defineProperty(exports, "__esModule", { value: true }); exports.digestSha1 = exports.rangeSpec2InclRange = exports.isSupportedRanceSpec = exports.isSatisfiable = exports.createPartialResponse = exports.BytesRange = void 0; const deps_js_1 = require("../deps.js"); const utils_js_1 = require("../utils.js"); const utils_js_2 = require("./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>", * ); * ``` */ class BytesRange { constructor(options) { _BytesRange_boundary.set(this, void 0); Object.defineProperty(this, "rangeUnit", { enumerable: true, configurable: true, writable: true, value: utils_js_1.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"), }); } } exports.BytesRange = BytesRange; _BytesRange_boundary = new WeakMap(); /** Create partial response from response. */ 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 (!(0, deps_js_1.isNotEmpty)(inclRanges)) { return new utils_js_1.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 = (0, deps_js_1.stringifyContentRange)({ rangeUnit: utils_js_1.RangeUnit.Bytes, ...inclRange, completeLength: size, }); const right = new Headers({ [deps_js_1.RangeHeader.ContentRange]: contentRange }); const headers = (0, utils_js_1.shallowMergeHeaders)(response.headers, right); return new Response(partialBody, { status: deps_js_1.Status.PartialContent, headers, }); } const contentType = response.headers.get(deps_js_1.RepresentationHeader.ContentType); if ((0, deps_js_1.isNull)(contentType)) return response; const boundary = await context.computeBoundary(content); const newContentType = `multipart/byteranges; boundary=${boundary}`; const multipart = (0, utils_js_2.multipartByteranges)({ content, contentType, ranges: inclRanges, boundary, rangeUnit: utils_js_1.RangeUnit.Bytes, }); const right = new Headers({ [deps_js_1.RepresentationHeader.ContentType]: newContentType, }); const headers = (0, utils_js_1.shallowMergeHeaders)(response.headers, right); return new Response(multipart, { status: deps_js_1.Status.PartialContent, headers }); } exports.createPartialResponse = createPartialResponse; /** Whether the range spec is satisfiable or not. */ function isSatisfiable(rangeSpec, contentLength) { if ((0, deps_js_1.isIntRange)(rangeSpec)) { if (!contentLength) return false; return rangeSpec.firstPos < contentLength; } return !!rangeSpec.suffixLength; } exports.isSatisfiable = isSatisfiable; function isSupportedRanceSpec(rangeSpec) { return !(0, deps_js_1.isOtherRange)(rangeSpec); } exports.isSupportedRanceSpec = isSupportedRanceSpec; /** Convert {@link RangeSpec} into {@link InclRange}. */ function rangeSpec2InclRange(rangeSpec, completeLength) { if ((0, deps_js_1.isIntRange)(rangeSpec)) { const lastPos = !(0, deps_js_1.isNumber)(rangeSpec.lastPos) || ((0, deps_js_1.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 }; } exports.rangeSpec2InclRange = rangeSpec2InclRange; async function digestSha1(content) { const hash = await crypto .subtle .digest("SHA-1", content); return (0, deps_js_1.toHashString)(hash); } exports.digestSha1 = digestSha1;