181 lines
4.5 KiB
JavaScript
181 lines
4.5 KiB
JavaScript
const {Readable} = require('stream');
|
|
|
|
/**
|
|
* @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
|
|
*/
|
|
const wm = new WeakMap();
|
|
|
|
async function * read(parts) {
|
|
for (const part of parts) {
|
|
if ('stream' in part) {
|
|
yield * part.stream();
|
|
} else {
|
|
yield part;
|
|
}
|
|
}
|
|
}
|
|
|
|
class Blob {
|
|
/**
|
|
* The Blob() constructor returns a new Blob object. The content
|
|
* of the blob consists of the concatenation of the values given
|
|
* in the parameter array.
|
|
*
|
|
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
|
|
* @param {{ type?: string }} [options]
|
|
*/
|
|
constructor(blobParts = [], options = {}) {
|
|
let size = 0;
|
|
|
|
const parts = blobParts.map(element => {
|
|
let buffer;
|
|
if (element instanceof Buffer) {
|
|
buffer = element;
|
|
} else if (ArrayBuffer.isView(element)) {
|
|
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
|
|
} else if (element instanceof ArrayBuffer) {
|
|
buffer = Buffer.from(element);
|
|
} else if (element instanceof Blob) {
|
|
buffer = element;
|
|
} else {
|
|
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
|
|
}
|
|
|
|
// eslint-disable-next-line unicorn/explicit-length-check
|
|
size += buffer.length || buffer.size || 0;
|
|
return buffer;
|
|
});
|
|
|
|
const type = options.type === undefined ? '' : String(options.type).toLowerCase();
|
|
|
|
wm.set(this, {
|
|
type: /[^\u0020-\u007E]/.test(type) ? '' : type,
|
|
size,
|
|
parts
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The Blob interface's size property returns the
|
|
* size of the Blob in bytes.
|
|
*/
|
|
get size() {
|
|
return wm.get(this).size;
|
|
}
|
|
|
|
/**
|
|
* The type property of a Blob object returns the MIME type of the file.
|
|
*/
|
|
get type() {
|
|
return wm.get(this).type;
|
|
}
|
|
|
|
/**
|
|
* The text() method in the Blob interface returns a Promise
|
|
* that resolves with a string containing the contents of
|
|
* the blob, interpreted as UTF-8.
|
|
*
|
|
* @return {Promise<string>}
|
|
*/
|
|
async text() {
|
|
return Buffer.from(await this.arrayBuffer()).toString();
|
|
}
|
|
|
|
/**
|
|
* The arrayBuffer() method in the Blob interface returns a
|
|
* Promise that resolves with the contents of the blob as
|
|
* binary data contained in an ArrayBuffer.
|
|
*
|
|
* @return {Promise<ArrayBuffer>}
|
|
*/
|
|
async arrayBuffer() {
|
|
const data = new Uint8Array(this.size);
|
|
let offset = 0;
|
|
for await (const chunk of this.stream()) {
|
|
data.set(chunk, offset);
|
|
offset += chunk.length;
|
|
}
|
|
|
|
return data.buffer;
|
|
}
|
|
|
|
/**
|
|
* The Blob interface's stream() method is difference from native
|
|
* and uses node streams instead of whatwg streams.
|
|
*
|
|
* @returns {Readable} Node readable stream
|
|
*/
|
|
stream() {
|
|
return Readable.from(read(wm.get(this).parts));
|
|
}
|
|
|
|
/**
|
|
* The Blob interface's slice() method creates and returns a
|
|
* new Blob object which contains data from a subset of the
|
|
* blob on which it's called.
|
|
*
|
|
* @param {number} [start]
|
|
* @param {number} [end]
|
|
* @param {string} [type]
|
|
*/
|
|
slice(start = 0, end = this.size, type = '') {
|
|
const {size} = this;
|
|
|
|
let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
|
|
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
|
|
|
|
const span = Math.max(relativeEnd - relativeStart, 0);
|
|
const parts = wm.get(this).parts.values();
|
|
const blobParts = [];
|
|
let added = 0;
|
|
|
|
for (const part of parts) {
|
|
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size;
|
|
if (relativeStart && size <= relativeStart) {
|
|
// Skip the beginning and change the relative
|
|
// start & end position as we skip the unwanted parts
|
|
relativeStart -= size;
|
|
relativeEnd -= size;
|
|
} else {
|
|
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
|
|
blobParts.push(chunk);
|
|
added += ArrayBuffer.isView(chunk) ? chunk.byteLength : chunk.size;
|
|
relativeStart = 0; // All next sequental parts should start at 0
|
|
|
|
// don't add the overflow to new blobParts
|
|
if (added >= span) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const blob = new Blob([], {type: String(type).toLowerCase()});
|
|
Object.assign(wm.get(blob), {size: span, parts: blobParts});
|
|
|
|
return blob;
|
|
}
|
|
|
|
get [Symbol.toStringTag]() {
|
|
return 'Blob';
|
|
}
|
|
|
|
static [Symbol.hasInstance](object) {
|
|
return (
|
|
object &&
|
|
typeof object === 'object' &&
|
|
typeof object.stream === 'function' &&
|
|
object.stream.length === 0 &&
|
|
typeof object.constructor === 'function' &&
|
|
/^(Blob|File)$/.test(object[Symbol.toStringTag])
|
|
);
|
|
}
|
|
}
|
|
|
|
Object.defineProperties(Blob.prototype, {
|
|
size: {enumerable: true},
|
|
type: {enumerable: true},
|
|
slice: {enumerable: true}
|
|
});
|
|
|
|
module.exports = Blob;
|