Started to implement server
This commit is contained in:
1 parent
1f2ea8b7e5
commit
17a1b4bed3
177 files changed
+29894
-74
No files matched your search
+169
@@ -0,0 +1,169 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user