@httpland/range-request-middleware
Version:
HTTP range request middleware
163 lines (162 loc) • 6.93 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;
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;
;