Skip to content

Commit b22b8de

Browse files
achingbrainhugomrdias
authored andcommitted
feat: add normalise input function (#5)
* feat: consolidate ipfs.add input normalisation Allows input normalisation function to be shared between ipfs and the http client. * feat: support pull streams (#8) * feat: support pull streams This PR updates the `normaliseInput` function to accept pull streams. I've also made the following changes: 1. Update the docs for supported inputs * `Buffer|ArrayBuffer|TypedArray` is aliased as `Bytes` * `Blob|File` is aliased as `Bloby` * Added info for what a input "means" i.e. causes single/multiple files to be added 1. Peek the first item of an (async) iterator properly 1. Move file object check below `input[Symbol.asyncIterator]` check because Node.js streams have a path property that will false positive the `isFileObject` check 1. Fix `toFileObject` to allow objects with no `content` property 1. Simplify `toBuffer` to remove checks that `Buffer.from` already does
1 parent b30d7a3 commit b22b8de

File tree

3 files changed

+490
-1
lines changed

3 files changed

+490
-1
lines changed

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,21 @@
2727
"license": "MIT",
2828
"dependencies": {
2929
"buffer": "^5.2.1",
30+
"err-code": "^2.0.0",
3031
"is-buffer": "^2.0.3",
3132
"is-electron": "^2.2.0",
3233
"is-pull-stream": "0.0.0",
3334
"is-stream": "^2.0.0",
3435
"kind-of": "^6.0.2",
36+
"pull-stream-to-async-iterator": "^1.0.2",
3537
"readable-stream": "^3.4.0"
3638
},
3739
"devDependencies": {
3840
"aegir": "^20.0.0",
41+
"async-iterator-all": "^1.0.0",
3942
"chai": "^4.2.0",
4043
"dirty-chai": "^2.0.1",
41-
"electron": "^5.0.7",
44+
"electron": "^6.0.6",
4245
"electron-mocha": "^8.0.3",
4346
"pull-stream": "^3.6.13"
4447
},

src/files/normalise-input.js

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
'use strict'
2+
3+
const errCode = require('err-code')
4+
const { Buffer } = require('buffer')
5+
const pullStreamToIterable = require('pull-stream-to-async-iterator')
6+
const { isSource } = require('is-pull-stream')
7+
const globalThis = require('../globalthis')
8+
9+
/*
10+
* Transform one of:
11+
*
12+
* ```
13+
* Bytes (Buffer|ArrayBuffer|TypedArray) [single file]
14+
* Bloby (Blob|File) [single file]
15+
* String [single file]
16+
* { path, content: Bytes } [single file]
17+
* { path, content: Bloby } [single file]
18+
* { path, content: String } [single file]
19+
* { path, content: Iterable<Number> } [single file]
20+
* { path, content: Iterable<Bytes> } [single file]
21+
* { path, content: AsyncIterable<Bytes> } [single file]
22+
* { path, content: PullStream<Bytes> } [single file]
23+
* Iterable<Number> [single file]
24+
* Iterable<Bytes> [single file]
25+
* Iterable<Bloby> [multiple files]
26+
* Iterable<String> [multiple files]
27+
* Iterable<{ path, content: Bytes }> [multiple files]
28+
* Iterable<{ path, content: Bloby }> [multiple files]
29+
* Iterable<{ path, content: String }> [multiple files]
30+
* Iterable<{ path, content: Iterable<Number> }> [multiple files]
31+
* Iterable<{ path, content: Iterable<Bytes> }> [multiple files]
32+
* Iterable<{ path, content: AsyncIterable<Bytes> }> [multiple files]
33+
* Iterable<{ path, content: PullStream<Bytes> }> [multiple files]
34+
* AsyncIterable<Bytes> [single file]
35+
* AsyncIterable<Bloby> [multiple files]
36+
* AsyncIterable<String> [multiple files]
37+
* AsyncIterable<{ path, content: Bytes }> [multiple files]
38+
* AsyncIterable<{ path, content: Bloby }> [multiple files]
39+
* AsyncIterable<{ path, content: String }> [multiple files]
40+
* AsyncIterable<{ path, content: Iterable<Number> }> [multiple files]
41+
* AsyncIterable<{ path, content: Iterable<Bytes> }> [multiple files]
42+
* AsyncIterable<{ path, content: AsyncIterable<Bytes> }> [multiple files]
43+
* AsyncIterable<{ path, content: PullStream<Bytes> }> [multiple files]
44+
* PullStream<Bytes> [single file]
45+
* PullStream<Bloby> [multiple files]
46+
* PullStream<String> [multiple files]
47+
* PullStream<{ path, content: Bytes }> [multiple files]
48+
* PullStream<{ path, content: Bloby }> [multiple files]
49+
* PullStream<{ path, content: String }> [multiple files]
50+
* PullStream<{ path, content: Iterable<Number> }> [multiple files]
51+
* PullStream<{ path, content: Iterable<Bytes> }> [multiple files]
52+
* PullStream<{ path, content: AsyncIterable<Bytes> }> [multiple files]
53+
* PullStream<{ path, content: PullStream<Bytes> }> [multiple files]
54+
* ```
55+
* Into:
56+
*
57+
* ```
58+
* AsyncIterable<{ path, content: AsyncIterable<Buffer> }>
59+
* ```
60+
*
61+
* @param input Object
62+
* @return AsyncInterable<{ path, content: AsyncIterable<Buffer> }>
63+
*/
64+
module.exports = function normaliseInput (input) {
65+
// must give us something
66+
if (input === null || input === undefined) {
67+
throw errCode(new Error(`Unexpected input: ${input}`, 'ERR_UNEXPECTED_INPUT'))
68+
}
69+
70+
// String
71+
if (typeof input === 'string' || input instanceof String) {
72+
return (async function * () { // eslint-disable-line require-await
73+
yield toFileObject(input)
74+
})()
75+
}
76+
77+
// Buffer|ArrayBuffer|TypedArray
78+
// Blob|File
79+
if (isBytes(input) || isBloby(input)) {
80+
return (async function * () { // eslint-disable-line require-await
81+
yield toFileObject(input)
82+
})()
83+
}
84+
85+
// Iterable<?>
86+
if (input[Symbol.iterator]) {
87+
return (async function * () { // eslint-disable-line require-await
88+
const iterator = input[Symbol.iterator]()
89+
const first = iterator.next()
90+
if (first.done) return iterator
91+
92+
// Iterable<Number>
93+
// Iterable<Bytes>
94+
if (Number.isInteger(first.value) || isBytes(first.value)) {
95+
yield toFileObject((function * () {
96+
yield first.value
97+
yield * iterator
98+
})())
99+
return
100+
}
101+
102+
// Iterable<Bloby>
103+
// Iterable<String>
104+
// Iterable<{ path, content }>
105+
if (isFileObject(first.value) || isBloby(first.value) || typeof first.value === 'string') {
106+
yield toFileObject(first.value)
107+
for (const obj of iterator) {
108+
yield toFileObject(obj)
109+
}
110+
return
111+
}
112+
113+
throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
114+
})()
115+
}
116+
117+
// AsyncIterable<?>
118+
if (input[Symbol.asyncIterator]) {
119+
return (async function * () {
120+
const iterator = input[Symbol.asyncIterator]()
121+
const first = await iterator.next()
122+
if (first.done) return iterator
123+
124+
// AsyncIterable<Bytes>
125+
if (isBytes(first.value)) {
126+
yield toFileObject((async function * () { // eslint-disable-line require-await
127+
yield first.value
128+
yield * iterator
129+
})())
130+
return
131+
}
132+
133+
// AsyncIterable<Bloby>
134+
// AsyncIterable<String>
135+
// AsyncIterable<{ path, content }>
136+
if (isFileObject(first.value) || isBloby(first.value) || typeof first.value === 'string') {
137+
yield toFileObject(first.value)
138+
for await (const obj of iterator) {
139+
yield toFileObject(obj)
140+
}
141+
return
142+
}
143+
144+
throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
145+
})()
146+
}
147+
148+
// { path, content: ? }
149+
// Note: Detected _after_ AsyncIterable<?> because Node.js streams have a
150+
// `path` property that passes this check.
151+
if (isFileObject(input)) {
152+
return (async function * () { // eslint-disable-line require-await
153+
yield toFileObject(input)
154+
})()
155+
}
156+
157+
// PullStream<?>
158+
if (isSource(input)) {
159+
return (async function * () {
160+
const iterator = pullStreamToIterable(input)[Symbol.asyncIterator]()
161+
const first = await iterator.next()
162+
if (first.done) return iterator
163+
164+
// PullStream<Bytes>
165+
if (isBytes(first.value)) {
166+
yield toFileObject((async function * () { // eslint-disable-line require-await
167+
yield first.value
168+
yield * iterator
169+
})())
170+
return
171+
}
172+
173+
// PullStream<Bloby>
174+
// PullStream<String>
175+
// PullStream<{ path, content }>
176+
if (isFileObject(first.value) || isBloby(first.value) || typeof first.value === 'string') {
177+
yield toFileObject(first.value)
178+
for await (const obj of iterator) {
179+
yield toFileObject(obj)
180+
}
181+
return
182+
}
183+
184+
throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
185+
})()
186+
}
187+
188+
throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
189+
}
190+
191+
function toFileObject (input) {
192+
const obj = { path: input.path || '' }
193+
194+
if (input.content) {
195+
obj.content = toAsyncIterable(input.content)
196+
} else if (!input.path) { // Not already a file object with path or content prop
197+
obj.content = toAsyncIterable(input)
198+
}
199+
200+
return obj
201+
}
202+
203+
function toAsyncIterable (input) {
204+
// Bytes | String
205+
if (isBytes(input) || typeof input === 'string') {
206+
return (async function * () { // eslint-disable-line require-await
207+
yield toBuffer(input)
208+
})()
209+
}
210+
211+
// Bloby
212+
if (isBloby(input)) {
213+
return blobToAsyncGenerator(input)
214+
}
215+
216+
// Iterator<?>
217+
if (input[Symbol.iterator]) {
218+
return (async function * () { // eslint-disable-line require-await
219+
const iterator = input[Symbol.iterator]()
220+
const first = iterator.next()
221+
if (first.done) return iterator
222+
223+
// Iterable<Number>
224+
if (Number.isInteger(first.value)) {
225+
yield toBuffer(Array.from((function * () {
226+
yield first.value
227+
yield * iterator
228+
})()))
229+
return
230+
}
231+
232+
// Iterable<Bytes>
233+
if (isBytes(first.value)) {
234+
yield toBuffer(first.value)
235+
for (const chunk of iterator) {
236+
yield toBuffer(chunk)
237+
}
238+
return
239+
}
240+
241+
throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
242+
})()
243+
}
244+
245+
// AsyncIterable<Bytes>
246+
if (input[Symbol.asyncIterator]) {
247+
return (async function * () {
248+
for await (const chunk of input) {
249+
yield toBuffer(chunk)
250+
}
251+
})()
252+
}
253+
254+
// PullStream<Bytes>
255+
if (isSource(input)) {
256+
return pullStreamToIterable(input)
257+
}
258+
259+
throw errCode(new Error(`Unexpected input: ${input}`, 'ERR_UNEXPECTED_INPUT'))
260+
}
261+
262+
function toBuffer (chunk) {
263+
return isBytes(chunk) ? chunk : Buffer.from(chunk)
264+
}
265+
266+
function isBytes (obj) {
267+
return Buffer.isBuffer(obj) || ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer
268+
}
269+
270+
function isBloby (obj) {
271+
return typeof globalThis.Blob !== 'undefined' && obj instanceof globalThis.Blob
272+
}
273+
274+
// An object with a path or content property
275+
function isFileObject (obj) {
276+
return typeof obj === 'object' && (obj.path || obj.content)
277+
}
278+
279+
function blobToAsyncGenerator (blob) {
280+
if (typeof blob.stream === 'function') {
281+
// firefox < 69 does not support blob.stream()
282+
return streamBlob(blob)
283+
}
284+
285+
return readBlob(blob)
286+
}
287+
288+
async function * streamBlob (blob) {
289+
const reader = blob.stream().getReader()
290+
291+
while (true) {
292+
const result = await reader.read()
293+
294+
if (result.done) {
295+
return
296+
}
297+
298+
yield result.value
299+
}
300+
}
301+
302+
async function * readBlob (blob, options) {
303+
options = options || {}
304+
305+
const reader = new globalThis.FileReader()
306+
const chunkSize = options.chunkSize || 1024 * 1024
307+
let offset = options.offset || 0
308+
309+
const getNextChunk = () => new Promise((resolve, reject) => {
310+
reader.onloadend = e => {
311+
const data = e.target.result
312+
resolve(data.byteLength === 0 ? null : data)
313+
}
314+
reader.onerror = reject
315+
316+
const end = offset + chunkSize
317+
const slice = blob.slice(offset, end)
318+
reader.readAsArrayBuffer(slice)
319+
offset = end
320+
})
321+
322+
while (true) {
323+
const data = await getNextChunk()
324+
325+
if (data == null) {
326+
return
327+
}
328+
329+
yield Buffer.from(data)
330+
}
331+
}

0 commit comments

Comments
 (0)