import { Buffer } from 'buffer'; import { ReadableStream } from 'stream/web'; import * as PropertySymbol from '../PropertySymbol.js'; /** * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/Blob. * * Based on: * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/file-api/Blob-impl.js (MIT licensed). */ export default class Blob { public readonly type: string = ''; public [PropertySymbol.buffer]: Buffer = null; /** * Constructor. * * @param [bits] Bits. * @param [options] Options. * @param [options.type] MIME type. */ constructor( bits?: (ArrayBuffer | ArrayBufferView | Blob | Buffer | string)[], options?: { type?: string } ) { const buffers = []; if (bits) { for (const bit of bits) { let buffer: Buffer; if (bit instanceof ArrayBuffer) { buffer = Buffer.from(new Uint8Array(bit)); } else if (bit instanceof Blob) { buffer = bit[PropertySymbol.buffer]; } else if (bit instanceof Buffer) { buffer = bit; } else if (ArrayBuffer.isView(bit)) { buffer = Buffer.from(new Uint8Array(bit.buffer, bit.byteOffset, bit.byteLength)); } else { buffer = Buffer.from(typeof bit === 'string' ? bit : String(bit)); } buffers.push(buffer); } } this[PropertySymbol.buffer] = Buffer.concat(buffers); if (options && options.type && options.type.match(/^[\u0020-\u007E]*$/)) { this.type = String(options.type).toLowerCase(); } } /** * Returns size. * * @returns Size. */ public get size(): number { return this[PropertySymbol.buffer].length; } /** * Slices the blob. * * @param start Start. * @param end End. * @param contentType Content type. * @returns New Blob. */ public slice(start = 0, end: number = null, contentType = ''): Blob { const size = this.size; let relativeStart; let relativeEnd; let relativeContentType; if (start === undefined) { relativeStart = 0; } else if (start < 0) { relativeStart = Math.max(size + start, 0); } else { relativeStart = Math.min(start, size); } if (end === null) { relativeEnd = size; } else if (end < 0) { relativeEnd = Math.max(size + end, 0); } else { relativeEnd = Math.min(end, size); } if (contentType === undefined) { relativeContentType = ''; } else { // Sanitization (lower case and invalid char check) is done in the // Constructor relativeContentType = contentType; } const span = Math.max(relativeEnd - relativeStart, 0); const buffer = this[PropertySymbol.buffer]; const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); const blob = new Blob([], { type: relativeContentType }); (blob[PropertySymbol.buffer]) = slicedBuffer; return blob; } /** * Returns a Promise that resolves to a ArrayBuffer. * * @returns ArrayBuffer. */ // Reference: // https://github.com/web-std/io/blob/c88170bf24f064adfbb3586a21fb76650ca5a9ab/packages/blob/src/blob.js#L139-L148 // https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer /** * */ public async arrayBuffer(): Promise { return new Uint8Array(this[PropertySymbol.buffer]).buffer; } /** * Returns a Promise that resolves to a text. * * @returns Text. */ public async text(): Promise { return this[PropertySymbol.buffer].toString(); } /** * Returns returns a ReadableStream which upon reading returns the data contained within the Blob. * * @returns ReadableStream */ public stream(): ReadableStream { const buffer = this[PropertySymbol.buffer]; return new ReadableStream({ start(controller) { controller.enqueue(buffer); controller.close(); } }); } /** * Returns the object converted to string. * * @returns String. */ public toString(): string { return '[object Blob]'; } }