/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-use-before-define */ import { Buffer } from "buffer" export interface File { path: string content?: AsyncIterable [key: string]: any } /** * Transform types * * @remarks * This function comes from {@link https://github.com/ipfs/js-ipfs-utils/blob/master/src/files/normalise-input.js} * @example * Supported types * ```yaml * // INPUT TYPES * Bytes (Buffer|ArrayBuffer|TypedArray) [single file] * Bloby (Blob|File) [single file] * String [single file] * { path, content: Bytes } [single file] * { path, content: Bloby } [single file] * { path, content: String } [single file] * { path, content: Iterable } [single file] * { path, content: Iterable } [single file] * { path, content: AsyncIterable } [single file] * Iterable [single file] * Iterable [single file] * Iterable [multiple files] * Iterable [multiple files] * Iterable<{ path, content: Bytes }> [multiple files] * Iterable<{ path, content: Bloby }> [multiple files] * Iterable<{ path, content: String }> [multiple files] * Iterable<{ path, content: Iterable }> [multiple files] * Iterable<{ path, content: Iterable }> [multiple files] * Iterable<{ path, content: AsyncIterable }> [multiple files] * AsyncIterable [single file] * AsyncIterable [multiple files] * AsyncIterable [multiple files] * AsyncIterable<{ path, content: Bytes }> [multiple files] * AsyncIterable<{ path, content: Bloby }> [multiple files] * AsyncIterable<{ path, content: String }> [multiple files] * AsyncIterable<{ path, content: Iterable }> [multiple files] * AsyncIterable<{ path, content: Iterable }> [multiple files] * AsyncIterable<{ path, content: AsyncIterable }> [multiple files] * * // OUTPUT * AsyncIterable<{ path, content: AsyncIterable }> * ``` * * @public * * @param {Object} input * @return AsyncInterable<{ path, content: AsyncIterable }> */ export function normaliseInput(input: any) { // must give us something if (input === null || input === undefined) { throw new Error(`Unexpected input: ${input}`) } // String if (typeof input === "string" || input instanceof String) { return (async function* () { // eslint-disable-line require-await yield toFileObject(input) })() } // Buffer|ArrayBuffer|TypedArray // Blob|File if (isBytes(input) || isBloby(input)) { return (async function* () { // eslint-disable-line require-await yield toFileObject(input) })() } // Iterable if (input[Symbol.iterator]) { return (async function* () { // eslint-disable-line require-await const iterator = input[Symbol.iterator]() const first = iterator.next() if (first.done) return iterator // Iterable // Iterable if (Number.isInteger(first.value) || isBytes(first.value)) { yield toFileObject( (function* () { yield first.value yield* iterator })(), ) return } // Iterable // Iterable // Iterable<{ path, content }> if (isFileObject(first.value) || isBloby(first.value) || typeof first.value === "string") { yield toFileObject(first.value) for (const obj of iterator) { yield toFileObject(obj) } return } throw new Error("Unexpected input: " + typeof input) })() } // window.ReadableStream if (typeof input.getReader === "function") { return (async function* () { for await (const obj of browserStreamToIt(input)) { yield toFileObject(obj) } })() } // AsyncIterable if (input[Symbol.asyncIterator]) { return (async function* () { const iterator = input[Symbol.asyncIterator]() const first = await iterator.next() if (first.done) return iterator // AsyncIterable if (isBytes(first.value)) { yield toFileObject( (async function* () { // eslint-disable-line require-await yield first.value yield* iterator })(), ) return } // AsyncIterable // AsyncIterable // AsyncIterable<{ path, content }> if (isFileObject(first.value) || isBloby(first.value) || typeof first.value === "string") { yield toFileObject(first.value) for await (const obj of iterator) { yield toFileObject(obj) } return } throw new Error("Unexpected input: " + typeof input) })() } // { path, content: ? } // Note: Detected _after_ AsyncIterable because Node.js streams have a // `path` property that passes this check. if (isFileObject(input)) { return (async function* () { // eslint-disable-line require-await yield toFileObject(input) })() } throw new Error("Unexpected input: " + typeof input) } function toFileObject(input: any) { const obj: File = { path: input.path || "", mode: input.mode, mtime: input.mtime, } if (input.content) { obj.content = toAsyncIterable(input.content) } else if (!input.path) { // Not already a file object with path or content prop obj.content = toAsyncIterable(input) } return obj } function toAsyncIterable(input: any) { // Bytes | String if (isBytes(input) || typeof input === "string") { return (async function* () { // eslint-disable-line require-await yield toBuffer(input) })() } // Bloby if (isBloby(input)) { return blobToAsyncGenerator(input) } // Browser stream if (typeof input.getReader === "function") { return browserStreamToIt(input) } // Iterator if (input[Symbol.iterator]) { return (async function* () { // eslint-disable-line require-await const iterator: IterableIterator = input[Symbol.iterator]() const first = iterator.next() if (first.done) return iterator // Iterable if (Number.isInteger(first.value as number)) { yield toBuffer( Array.from( (function* () { yield first.value yield* iterator })(), ) as any, ) return } // Iterable if (isBytes(first.value)) { yield toBuffer(first.value) for (const chunk of iterator) { yield toBuffer(chunk) } return } throw new Error("Unexpected input: " + typeof input) })() } // AsyncIterable if (input[Symbol.asyncIterator]) { return (async function* () { for await (const chunk of input) { yield toBuffer(chunk) } })() } throw new Error(`Unexpected input: ${input}`) } function toBuffer(chunk: Buffer | ArrayBuffer): Buffer | ArrayBuffer { return isBytes(chunk) ? chunk : Buffer.from(chunk) } function isBytes(obj: Buffer | ArrayBuffer) { return Buffer.isBuffer(obj) || ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer } function isBloby(obj: any) { return typeof globalThis.Blob !== "undefined" && obj instanceof globalThis.Blob } // An object with a path or content property function isFileObject(obj: any) { return typeof obj === "object" && (obj.path || obj.content) } function blobToAsyncGenerator(blob: Blob) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (typeof blob.stream === "function") { // firefox < 69 does not support blob.stream() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return browserStreamToIt(blob.stream()) } return readBlob(blob) } async function* browserStreamToIt(stream: ReadableStream) { const reader = stream.getReader() while (true) { const result = await reader.read() if (result.done) { return } yield result.value } } async function* readBlob(blob: Blob, options?: any) { options = options || {} const reader = new globalThis.FileReader() const chunkSize = options.chunkSize || 1024 * 1024 let offset = options.offset || 0 const getNextChunk = () => new Promise((resolve, reject) => { reader.onloadend = (e) => { const data = e.target?.result as ArrayBuffer resolve(data.byteLength === 0 ? null : data) } reader.onerror = reject const end = offset + chunkSize const slice = blob.slice(offset, end) reader.readAsArrayBuffer(slice) offset = end }) while (true) { const data = await getNextChunk() if (data == null) { return } yield Buffer.from(data) } }