@httpland/range-request-middleware
Version:
HTTP range request middleware
154 lines (153 loc) • 6.38 kB
JavaScript
// 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);
}