Skip to content

Commit 27394f6

Browse files
authored
feat: storeDirectory accepts files as AsyncIterable<File> (#1920)
* client storeDirectory accepts an asyncIterable of files * storeDirectory accepts an AsyncIterable, not just Iterable
1 parent a5a5e72 commit 27394f6

4 files changed

Lines changed: 82 additions & 22 deletions

File tree

packages/client/src/lib.js

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as Token from './token.js'
2424
import { fetch, File, Blob, FormData, Blockstore } from './platform.js'
2525
import { toGatewayURL } from './gateway.js'
2626
import { BlockstoreCarReader } from './bs-car-reader.js'
27+
import pipe from 'it-pipe'
2728

2829
const MAX_STORE_RETRIES = 5
2930
const MAX_CONCURRENT_UPLOADS = 3
@@ -215,7 +216,7 @@ class NFTStorage {
215216
* `foo/bla/baz.json` is ok but `foo/bar.png`, `bla/baz.json` is not.
216217
*
217218
* @param {Service} service
218-
* @param {Iterable<File>} files
219+
* @param {Iterable<File>|AsyncIterable<File>} files
219220
* @returns {Promise<CIDString>}
220221
*/
221222
static async storeDirectory(service, files) {
@@ -455,29 +456,32 @@ class NFTStorage {
455456
* await client.storeCar(car)
456457
* ```
457458
*
458-
* @param {Iterable<File>} files
459+
* @param {Iterable<File>|AsyncIterable<File>} files
459460
* @param {object} [options]
460461
* @param {BlockstoreI} [options.blockstore]
461462
* @returns {Promise<{ cid: CID, car: CarReader }>}
462463
*/
463464
static async encodeDirectory(files, { blockstore } = {}) {
464-
const input = []
465465
let size = 0
466-
for (const file of files) {
467-
input.push(toImportCandidate(file.name, file))
468-
size += file.size
469-
}
470-
466+
const input = pipe(
467+
isIterable(files) ? toAsyncIterable(files) : files,
468+
async function* (files) {
469+
for await (const file of files) {
470+
yield toImportCandidate(file.name, file)
471+
size += file.size
472+
}
473+
}
474+
)
475+
const packed = await packCar(input, {
476+
blockstore,
477+
wrapWithDirectory: true,
478+
})
471479
if (size === 0) {
472480
throw new Error(
473481
'Total size of files should exceed 0, make sure to provide some content'
474482
)
475483
}
476-
477-
return packCar(input, {
478-
blockstore,
479-
wrapWithDirectory: true,
480-
})
484+
return packed
481485
}
482486

483487
// Just a sugar so you don't have to pass around endpoint and token around.
@@ -557,7 +561,7 @@ class NFTStorage {
557561
* Argument can be a [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList)
558562
* instance as well, in which case directory structure will be retained.
559563
*
560-
* @param {Iterable<File>} files
564+
* @param {AsyncIterable<File>|Iterable<File>} files
561565
*/
562566
storeDirectory(files) {
563567
return NFTStorage.storeDirectory(this, files)
@@ -654,6 +658,29 @@ class NFTStorage {
654658
}
655659
}
656660

661+
/**
662+
* type guard checking for Iterable
663+
* @param {any} x;
664+
* @returns {x is Iterable<unknown>}
665+
*/
666+
function isIterable(x) {
667+
return Symbol.iterator in x
668+
}
669+
670+
/**
671+
* Cast an iterable to an asyncIterable
672+
* @template T
673+
* @param {Iterable<T>} iterable
674+
* @returns {AsyncIterable<T>}
675+
*/
676+
export function toAsyncIterable(iterable) {
677+
return (async function* () {
678+
for (const item of iterable) {
679+
yield item
680+
}
681+
})()
682+
}
683+
657684
/**
658685
* @template {import('./lib/interface.js').TokenInput} T
659686
* @param {T} metadata
@@ -686,7 +713,7 @@ For more context please see ERC-721 specification https://eips.ethereum.org/EIPS
686713
}
687714

688715
/**
689-
* @param {Array<{ path: string, content: import('./platform.js').ReadableStream }>} input
716+
* @param {import('ipfs-car/pack').ImportCandidateStream|Array<{ path: string, content: import('./platform.js').ReadableStream }>} input
690717
* @param {object} [options]
691718
* @param {BlockstoreI} [options.blockstore]
692719
* @param {boolean} [options.wrapWithDirectory]

packages/client/src/lib/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export interface API {
100100
* be within the same directory, otherwise error is raised e.g. `foo/bar.png`,
101101
* `foo/bla/baz.json` is ok but `foo/bar.png`, `bla/baz.json` is not.
102102
*/
103-
storeDirectory(service: Service, files: Iterable<File>): Promise<CIDString>
103+
storeDirectory(service: Service, files: Iterable<File>|AsyncIterable<File>): Promise<CIDString>
104104
/**
105105
* Returns current status of the stored NFT by its CID. Note the NFT must
106106
* have previously been stored by this account.

packages/client/test/lib.spec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { pack } from 'ipfs-car/pack'
99
import { CarWriter } from '@ipld/car'
1010
import * as dagJson from '@ipld/dag-json'
1111
import { randomCar } from './helpers.js'
12+
import { toAsyncIterable } from '../src/lib.js'
1213

1314
const GATEWAY_LINK = 'nftstorage.link'
1415

@@ -222,6 +223,24 @@ describe('client', () => {
222223
)
223224
})
224225

226+
it('upload multiple files as asyncIterable', async () => {
227+
const client = new NFTStorage({ token, endpoint })
228+
const cid = await client.storeDirectory(
229+
toAsyncIterable([
230+
new File(['hello world'], 'hello.txt'),
231+
new File(
232+
[JSON.stringify({ from: 'incognito' }, null, 2)],
233+
'metadata.json'
234+
),
235+
])
236+
)
237+
238+
assert.equal(
239+
cid,
240+
'bafybeigkms36pnnjsa7t2mq2g4mx77s4no2hilirs4wqx3eebbffy2ay3a'
241+
)
242+
})
243+
225244
it('upload empty files', async () => {
226245
const client = new NFTStorage({ token, endpoint })
227246
try {

yarn.lock

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,13 @@
10851085
"@babel/template" "^7.16.7"
10861086
"@babel/types" "^7.17.0"
10871087

1088+
"@babel/helper-get-function-arity@^7.16.7":
1089+
version "7.16.7"
1090+
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419"
1091+
integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==
1092+
dependencies:
1093+
"@babel/types" "^7.16.7"
1094+
10881095
"@babel/helper-hoist-variables@^7.16.7":
10891096
version "7.16.7"
10901097
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246"
@@ -16390,6 +16397,13 @@ react-debounce-input@=3.2.4:
1639016397
lodash.debounce "^4"
1639116398
prop-types "^15.7.2"
1639216399

16400+
react-device-detect@^2.2.2:
16401+
version "2.2.2"
16402+
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.2.tgz#dbabbce798ec359c83f574c3edb24cf1cca641a5"
16403+
integrity sha512-zSN1gIAztUekp5qUT/ybHwQ9fmOqVT1psxpSlTn1pe0CO+fnJHKRLOWWac5nKxOxvOpD/w84hk1I+EydrJp7SA==
16404+
dependencies:
16405+
ua-parser-js "^1.0.2"
16406+
1639316407
react-docgen-typescript@^2.1.1:
1639416408
version "2.2.2"
1639516409
resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"
@@ -18925,12 +18939,7 @@ typedoc@^0.22.14:
1892518939
minimatch "^5.0.1"
1892618940
shiki "^0.10.1"
1892718941

18928-
typescript@4.4.4:
18929-
version "4.4.4"
18930-
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
18931-
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
18932-
18933-
typescript@4.5.3:
18942+
typescript@4.4.4, typescript@4.5.3:
1893418943
version "4.5.3"
1893518944
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c"
1893618945
integrity sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==
@@ -18940,6 +18949,11 @@ typical@^6.0.1:
1894018949
resolved "https://registry.yarnpkg.com/typical/-/typical-6.0.1.tgz#89bd1a6aa5e5e96fa907fb6b7579223bff558a06"
1894118950
integrity sha512-+g3NEp7fJLe9DPa1TArHm9QAA7YciZmWnfAqEaFrBihQ7epOv9i99rjtgb6Iz0wh3WuQDjsCTDfgRoGnmHN81A==
1894218951

18952+
ua-parser-js@^1.0.2:
18953+
version "1.0.2"
18954+
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
18955+
integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==
18956+
1894318957
ucan-storage@^1.0.0:
1894418958
version "1.1.3"
1894518959
resolved "https://registry.yarnpkg.com/ucan-storage/-/ucan-storage-1.1.3.tgz#6874f887a8c7646f2d521f90d67345ac74532c59"

0 commit comments

Comments
 (0)