Skip to main content
Module

x/oak/range.ts

A middleware framework for handling HTTP with Deno 🐿️ 🦕
Extremely Popular
Go to Latest
File
// Copyright 2018-2022 the oak authors. All rights reserved. MIT license.
import { concat, contentType, copyBytes, createHttpError, Status,} from "./deps.ts";import { calculate } from "./etag.ts";import type { FileInfo } from "./etag.ts";import { assert, DEFAULT_CHUNK_SIZE } from "./util.ts";
const ETAG_RE = /(?:W\/)?"[ !#-\x7E\x80-\xFF]+"/;
export interface ByteRange { start: number; end: number;}
/** Determine, by the value of an `If-Range` header, if a `Range` header should * be applied to a request, returning `true` if it should or otherwise * `false`. */export async function ifRange( value: string, mtime: number, entity: Uint8Array | FileInfo,): Promise<boolean> { if (value) { const matches = value.match(ETAG_RE); if (matches) { const [match] = matches; if (await calculate(entity) === match) { return true; } } else { return new Date(value).getTime() >= mtime; } } return false;}
export function parseRange(value: string, size: number): ByteRange[] { const ranges: ByteRange[] = []; const [unit, rangesStr] = value.split("="); if (unit !== "bytes") { throw createHttpError(Status.RequestedRangeNotSatisfiable); } for (const range of rangesStr.split(/\s*,\s+/)) { const item = range.split("-"); if (item.length !== 2) { throw createHttpError(Status.RequestedRangeNotSatisfiable); } const [startStr, endStr] = item; let start: number; let end: number; try { if (startStr === "") { start = size - parseInt(endStr, 10) - 1; end = size - 1; } else if (endStr === "") { start = parseInt(startStr, 10); end = size - 1; } else { start = parseInt(startStr, 10); end = parseInt(endStr, 10); } } catch { throw createHttpError(); } if (start < 0 || start >= size || end < 0 || end >= size || start > end) { throw createHttpError(Status.RequestedRangeNotSatisfiable); } ranges.push({ start, end }); } return ranges;}
/** A reader */async function readRange( file: Deno.Reader & Deno.Seeker, range: ByteRange,): Promise<Uint8Array> { let length = range.end - range.start + 1; assert(length); await file.seek(range.start, Deno.SeekMode.Start); const result = new Uint8Array(length); let off = 0; while (length) { const p = new Uint8Array(Math.min(length, DEFAULT_CHUNK_SIZE)); const nread = await file.read(p); assert(nread !== null, "Unexpected EOF encountered when reading a range."); assert(nread > 0, "Unexpected read of 0 bytes while reading a range."); copyBytes(p, result, off); off += nread; length -= nread; assert(length >= 0, "Unexpected length remaining."); } return result;}
const encoder = new TextEncoder();
/** A class that takes a file (either a Deno.FsFile or Uint8Array) and bytes * and streams the ranges as a multi-part encoded HTTP body. * * This is specifically used by the `.send()` functionality to fulfill range * requests it receives, and could be used by others when trying to deal with * range requests, but is generally a low level API that most users of oak * would not need to worry about. */export class MultiPartStream extends ReadableStream<Uint8Array> { #contentLength: number; #postscript: Uint8Array; #preamble: Uint8Array;
constructor( file: (Deno.Reader & Deno.Seeker & Deno.Closer) | Uint8Array, type: string, ranges: ByteRange[], size: number, boundary: string, ) { super({ pull: async (controller) => { const range = ranges.shift(); if (!range) { controller.enqueue(this.#postscript); controller.close(); if (!(file instanceof Uint8Array)) { file.close(); } return; } let bytes: Uint8Array; if (file instanceof Uint8Array) { bytes = file.subarray(range.start, range.end + 1); } else { bytes = await readRange(file, range); } const rangeHeader = encoder.encode( `Content-Range: ${range.start}-${range.end}/${size}\n\n`, ); controller.enqueue(concat(this.#preamble, rangeHeader, bytes)); }, });
const resolvedType = contentType(type); if (!resolvedType) { throw new TypeError(`Could not resolve media type for "${type}"`); } this.#preamble = encoder.encode( `\n--${boundary}\nContent-Type: ${resolvedType}\n`, );
this.#postscript = encoder.encode(`\n--${boundary}--\n`); this.#contentLength = ranges.reduce( (prev, { start, end }): number => { return prev + this.#preamble.length + String(start).length + String(end).length + String(size).length + 20 + (end - start); }, this.#postscript.length, ); }
/** The content length of the entire streamed body. */ contentLength() { return this.#contentLength; }}