ipfs storage for images and other nontext items. for use with etica - runs on etica network and currencys
https://collect.etica-stats.org
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
7.3 KiB
250 lines
7.3 KiB
/*! fetch-blob. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */ |
|
|
|
// TODO (jimmywarting): in the feature use conditional loading with top level await (requires 14.x) |
|
// Node has recently added whatwg stream into core |
|
|
|
import './streams.cjs' |
|
|
|
// 64 KiB (same size chrome slice theirs blob into Uint8array's) |
|
const POOL_SIZE = 65536 |
|
|
|
/** @param {(Blob | Uint8Array)[]} parts */ |
|
async function * toIterator (parts, clone = true) { |
|
for (const part of parts) { |
|
if ('stream' in part) { |
|
yield * (/** @type {AsyncIterableIterator<Uint8Array>} */ (part.stream())) |
|
} else if (ArrayBuffer.isView(part)) { |
|
if (clone) { |
|
let position = part.byteOffset |
|
const end = part.byteOffset + part.byteLength |
|
while (position !== end) { |
|
const size = Math.min(end - position, POOL_SIZE) |
|
const chunk = part.buffer.slice(position, position + size) |
|
position += chunk.byteLength |
|
yield new Uint8Array(chunk) |
|
} |
|
} else { |
|
yield part |
|
} |
|
/* c8 ignore next 10 */ |
|
} else { |
|
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob) |
|
let position = 0, b = (/** @type {Blob} */ (part)) |
|
while (position !== b.size) { |
|
const chunk = b.slice(position, Math.min(b.size, position + POOL_SIZE)) |
|
const buffer = await chunk.arrayBuffer() |
|
position += buffer.byteLength |
|
yield new Uint8Array(buffer) |
|
} |
|
} |
|
} |
|
} |
|
|
|
const _Blob = class Blob { |
|
/** @type {Array.<(Blob|Uint8Array)>} */ |
|
#parts = [] |
|
#type = '' |
|
#size = 0 |
|
#endings = 'transparent' |
|
|
|
/** |
|
* 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 {*} blobParts |
|
* @param {{ type?: string, endings?: string }} [options] |
|
*/ |
|
constructor (blobParts = [], options = {}) { |
|
if (typeof blobParts !== 'object' || blobParts === null) { |
|
throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.') |
|
} |
|
|
|
if (typeof blobParts[Symbol.iterator] !== 'function') { |
|
throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.') |
|
} |
|
|
|
if (typeof options !== 'object' && typeof options !== 'function') { |
|
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.') |
|
} |
|
|
|
if (options === null) options = {} |
|
|
|
const encoder = new TextEncoder() |
|
for (const element of blobParts) { |
|
let part |
|
if (ArrayBuffer.isView(element)) { |
|
part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength)) |
|
} else if (element instanceof ArrayBuffer) { |
|
part = new Uint8Array(element.slice(0)) |
|
} else if (element instanceof Blob) { |
|
part = element |
|
} else { |
|
part = encoder.encode(`${element}`) |
|
} |
|
|
|
this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size |
|
this.#parts.push(part) |
|
} |
|
|
|
this.#endings = `${options.endings === undefined ? 'transparent' : options.endings}` |
|
const type = options.type === undefined ? '' : String(options.type) |
|
this.#type = /^[\x20-\x7E]*$/.test(type) ? type : '' |
|
} |
|
|
|
/** |
|
* The Blob interface's size property returns the |
|
* size of the Blob in bytes. |
|
*/ |
|
get size () { |
|
return this.#size |
|
} |
|
|
|
/** |
|
* The type property of a Blob object returns the MIME type of the file. |
|
*/ |
|
get type () { |
|
return 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 () { |
|
// More optimized than using this.arrayBuffer() |
|
// that requires twice as much ram |
|
const decoder = new TextDecoder() |
|
let str = '' |
|
for await (const part of toIterator(this.#parts, false)) { |
|
str += decoder.decode(part, { stream: true }) |
|
} |
|
// Remaining |
|
str += decoder.decode() |
|
return str |
|
} |
|
|
|
/** |
|
* 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 () { |
|
// Easier way... Just a unnecessary overhead |
|
// const view = new Uint8Array(this.size); |
|
// await this.stream().getReader({mode: 'byob'}).read(view); |
|
// return view.buffer; |
|
|
|
const data = new Uint8Array(this.size) |
|
let offset = 0 |
|
for await (const chunk of toIterator(this.#parts, false)) { |
|
data.set(chunk, offset) |
|
offset += chunk.length |
|
} |
|
|
|
return data.buffer |
|
} |
|
|
|
stream () { |
|
const it = toIterator(this.#parts, true) |
|
|
|
return new globalThis.ReadableStream({ |
|
// @ts-ignore |
|
type: 'bytes', |
|
async pull (ctrl) { |
|
const chunk = await it.next() |
|
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value) |
|
}, |
|
|
|
async cancel () { |
|
await it.return() |
|
} |
|
}) |
|
} |
|
|
|
/** |
|
* 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 = this.#parts |
|
const blobParts = [] |
|
let added = 0 |
|
|
|
for (const part of parts) { |
|
// don't add the overflow to new blobParts |
|
if (added >= span) { |
|
break |
|
} |
|
|
|
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 { |
|
let chunk |
|
if (ArrayBuffer.isView(part)) { |
|
chunk = part.subarray(relativeStart, Math.min(size, relativeEnd)) |
|
added += chunk.byteLength |
|
} else { |
|
chunk = part.slice(relativeStart, Math.min(size, relativeEnd)) |
|
added += chunk.size |
|
} |
|
relativeEnd -= size |
|
blobParts.push(chunk) |
|
relativeStart = 0 // All next sequential parts should start at 0 |
|
} |
|
} |
|
|
|
const blob = new Blob([], { type: String(type).toLowerCase() }) |
|
blob.#size = span |
|
blob.#parts = blobParts |
|
|
|
return blob |
|
} |
|
|
|
get [Symbol.toStringTag] () { |
|
return 'Blob' |
|
} |
|
|
|
static [Symbol.hasInstance] (object) { |
|
return ( |
|
object && |
|
typeof object === 'object' && |
|
typeof object.constructor === 'function' && |
|
( |
|
typeof object.stream === 'function' || |
|
typeof object.arrayBuffer === 'function' |
|
) && |
|
/^(Blob|File)$/.test(object[Symbol.toStringTag]) |
|
) |
|
} |
|
} |
|
|
|
Object.defineProperties(_Blob.prototype, { |
|
size: { enumerable: true }, |
|
type: { enumerable: true }, |
|
slice: { enumerable: true } |
|
}) |
|
|
|
/** @type {typeof globalThis.Blob} */ |
|
export const Blob = _Blob |
|
export default Blob
|
|
|