-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ranges): add ByteRange implementation
- Loading branch information
1 parent
9d905ee
commit 1741e1d
Showing
2 changed files
with
183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { | ||
type IntRange, | ||
isIntRange, | ||
isNumber, | ||
RangeHeader, | ||
RepresentationHeader, | ||
type SuffixRange, | ||
toHashString, | ||
} from "../deps.ts"; | ||
import type { | ||
IsSatisfiableContext, | ||
PartialContent, | ||
PartialContext, | ||
Range, | ||
} from "../types.ts"; | ||
import { RangeUnit, Specifier } from "../util.ts"; | ||
import { type InclRange, multipartByteranges } from "./utils.ts"; | ||
|
||
interface Options { | ||
readonly boundary?: BoundaryCallback; | ||
} | ||
|
||
export class BytesRange | ||
implements Range<Specifier.SuffixRange | Specifier.IntRange> { | ||
constructor(options?: Options) { | ||
this.#boundary = options?.boundary ?? getBoundary; | ||
} | ||
|
||
unit = RangeUnit.Bytes; | ||
specifiers = [Specifier.SuffixRange, Specifier.IntRange] as const; | ||
#boundary: BoundaryCallback; | ||
|
||
getSatisfiable( | ||
context: IsSatisfiableContext<IntRange | SuffixRange>, | ||
): (IntRange | SuffixRange)[] { | ||
return context.rangeSet.filter((rangeSpec) => | ||
isSatisfiable(rangeSpec, context.content.byteLength) | ||
); | ||
} | ||
|
||
async getPartial( | ||
context: PartialContext<IntRange | SuffixRange>, | ||
): Promise<PartialContent> { | ||
const { rangeSet, content, contentType } = context; | ||
const size = content.byteLength; | ||
const ranges = rangeSet.map((rangeSpec) => | ||
rangeSpec2InclRange(rangeSpec, size) | ||
) as [InclRange, ...InclRange[]]; | ||
|
||
if (ranges.length === 1) { | ||
const byteRange = ranges[0]; | ||
const partialBody = content.slice( | ||
byteRange.firstPos, | ||
byteRange.lastPos + 1, | ||
); | ||
const contentRange = | ||
`bytes ${byteRange.firstPos}-${byteRange.lastPos}/${size}`; | ||
const headers = new Headers({ [RangeHeader.ContentRange]: contentRange }); | ||
|
||
return { content: partialBody, headers }; | ||
} | ||
|
||
const boundary = await this.#boundary(content); | ||
const newContentType = `multipart/byteranges; boundary=${boundary}`; | ||
const multipart = multipartByteranges({ | ||
content, | ||
contentType, | ||
ranges, | ||
boundary, | ||
}); | ||
const headers = new Headers({ | ||
[RepresentationHeader.ContentType]: newContentType, | ||
}); | ||
|
||
return { content: multipart, headers }; | ||
} | ||
} | ||
|
||
export function isSatisfiable( | ||
rangeSpec: IntRange | SuffixRange, | ||
contentLength: number, | ||
): boolean { | ||
if (isIntRange(rangeSpec)) { | ||
if (!contentLength) return false; | ||
|
||
return rangeSpec.firstPos < contentLength; | ||
} | ||
|
||
return !!rangeSpec.suffixLength; | ||
} | ||
|
||
export function rangeSpec2InclRange( | ||
rangeSpec: IntRange | SuffixRange, | ||
completeLength: number, | ||
): InclRange { | ||
if (isIntRange(rangeSpec)) { | ||
const lastPos = isNumber(rangeSpec.lastPos) | ||
? completeLength < rangeSpec.lastPos | ||
? completeLength - 1 | ||
: rangeSpec.lastPos | ||
: completeLength - 1; | ||
|
||
return { firstPos: rangeSpec.firstPos, lastPos }; | ||
} | ||
|
||
if (completeLength < rangeSpec.suffixLength) { | ||
return { firstPos: 0, lastPos: completeLength - 1 }; | ||
} | ||
|
||
return { | ||
firstPos: completeLength - rangeSpec.suffixLength, | ||
lastPos: completeLength - 1, | ||
}; | ||
} | ||
|
||
export interface BoundaryCallback { | ||
(content: ArrayBuffer): string | Promise<string>; | ||
} | ||
|
||
export async function getBoundary(content: ArrayBuffer): Promise<string> { | ||
const buffer = await crypto.subtle.digest("sha-1", content); | ||
|
||
return toHashString(buffer); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { concat, isString } from "../deps.ts"; | ||
|
||
export interface InclRange { | ||
readonly firstPos: number; | ||
readonly lastPos: number; | ||
} | ||
|
||
interface Args { | ||
readonly content: ArrayBuffer; | ||
readonly contentType: string; | ||
readonly ranges: readonly InclRange[]; | ||
readonly boundary: string; | ||
} | ||
|
||
export function multipartByteranges(args: Args): Uint8Array { | ||
const { content, contentType, ranges, boundary } = args; | ||
const size = content.byteLength; | ||
const boundaryDelimiter = "--" + boundary; | ||
const endDelimiter = boundaryDelimiter + "--"; | ||
const encoder = new TextEncoder(); | ||
const contents = ranges | ||
.map(bodyParts) | ||
.flat() | ||
.map((buffer) => new Uint8Array(buffer)) | ||
.concat(encoder.encode(endDelimiter)); | ||
|
||
return join(contents, encoder.encode("\n")); | ||
|
||
function bodyParts(range: InclRange): ArrayBuffer[] { | ||
return [ | ||
boundaryDelimiter, | ||
`Content-Type: ${contentType}`, | ||
`Content-Range: ${range.firstPos}-${range.lastPos}/${size}`, | ||
"", | ||
content.slice(range.firstPos, range.lastPos + 1), | ||
].map(toBuffer); | ||
} | ||
} | ||
|
||
export function join( | ||
list: readonly Uint8Array[], | ||
separator: Uint8Array, | ||
): Uint8Array { | ||
return list.reduce((acc, cur, i) => { | ||
if (!i) return concat(acc, cur); | ||
|
||
return concat(acc, separator, cur); | ||
}, new Uint8Array()); | ||
} | ||
|
||
export function toBuffer(input: string | ArrayBuffer): ArrayBuffer { | ||
return isString(input) ? new TextEncoder().encode(input) : input; | ||
} |