@helia/car 4.0.3 → 4.0.4-bb2ab74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +34 -17
  2. package/dist/index.min.js +2 -1
  3. package/dist/index.min.js.map +7 -0
  4. package/dist/src/car.d.ts +17 -0
  5. package/dist/src/car.d.ts.map +1 -0
  6. package/dist/src/car.js +162 -0
  7. package/dist/src/car.js.map +1 -0
  8. package/dist/src/constants.d.ts +10 -0
  9. package/dist/src/constants.d.ts.map +1 -0
  10. package/dist/src/constants.js +10 -0
  11. package/dist/src/constants.js.map +1 -0
  12. package/dist/src/errors.d.ts +9 -0
  13. package/dist/src/errors.d.ts.map +1 -0
  14. package/dist/src/errors.js +9 -0
  15. package/dist/src/errors.js.map +1 -0
  16. package/dist/src/export-strategies/block-exporter.d.ts +10 -0
  17. package/dist/src/export-strategies/block-exporter.d.ts.map +1 -0
  18. package/dist/src/export-strategies/block-exporter.js +12 -0
  19. package/dist/src/export-strategies/block-exporter.js.map +1 -0
  20. package/dist/src/export-strategies/index.d.ts +4 -0
  21. package/dist/src/export-strategies/index.d.ts.map +1 -0
  22. package/dist/src/export-strategies/index.js +4 -0
  23. package/dist/src/export-strategies/index.js.map +1 -0
  24. package/dist/src/export-strategies/subgraph-exporter.d.ts +14 -0
  25. package/dist/src/export-strategies/subgraph-exporter.d.ts.map +1 -0
  26. package/dist/src/export-strategies/subgraph-exporter.js +17 -0
  27. package/dist/src/export-strategies/subgraph-exporter.js.map +1 -0
  28. package/dist/src/export-strategies/unixfs-exporter.d.ts +11 -0
  29. package/dist/src/export-strategies/unixfs-exporter.d.ts.map +1 -0
  30. package/dist/src/export-strategies/unixfs-exporter.js +18 -0
  31. package/dist/src/export-strategies/unixfs-exporter.js.map +1 -0
  32. package/dist/src/index.d.ts +89 -24
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +38 -95
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/traversal-strategies/cid-path.d.ts +16 -0
  37. package/dist/src/traversal-strategies/cid-path.d.ts.map +1 -0
  38. package/dist/src/traversal-strategies/cid-path.js +22 -0
  39. package/dist/src/traversal-strategies/cid-path.js.map +1 -0
  40. package/dist/src/traversal-strategies/graph-search.d.ts +14 -0
  41. package/dist/src/traversal-strategies/graph-search.d.ts.map +1 -0
  42. package/dist/src/traversal-strategies/graph-search.js +19 -0
  43. package/dist/src/traversal-strategies/graph-search.js.map +1 -0
  44. package/dist/src/traversal-strategies/index.d.ts +4 -0
  45. package/dist/src/traversal-strategies/index.d.ts.map +1 -0
  46. package/dist/src/traversal-strategies/index.js +4 -0
  47. package/dist/src/traversal-strategies/index.js.map +1 -0
  48. package/dist/src/traversal-strategies/unixfs-path.d.ts +13 -0
  49. package/dist/src/traversal-strategies/unixfs-path.d.ts.map +1 -0
  50. package/dist/src/traversal-strategies/unixfs-path.js +42 -0
  51. package/dist/src/traversal-strategies/unixfs-path.js.map +1 -0
  52. package/package.json +12 -6
  53. package/src/car.ts +218 -0
  54. package/src/constants.ts +11 -0
  55. package/src/errors.ts +8 -0
  56. package/src/export-strategies/block-exporter.ts +13 -0
  57. package/src/export-strategies/index.ts +3 -0
  58. package/src/export-strategies/subgraph-exporter.ts +18 -0
  59. package/src/export-strategies/unixfs-exporter.ts +22 -0
  60. package/src/index.ts +99 -119
  61. package/src/traversal-strategies/cid-path.ts +29 -0
  62. package/src/traversal-strategies/graph-search.ts +25 -0
  63. package/src/traversal-strategies/index.ts +3 -0
  64. package/src/traversal-strategies/unixfs-path.ts +58 -0
  65. package/dist/typedoc-urls.json +0 -10
@@ -0,0 +1,18 @@
1
+ import { type CID } from 'multiformats/cid'
2
+ import { type ExportStrategy } from '../index.js'
3
+ import type { BlockView } from 'multiformats/block/interface'
4
+
5
+ /**
6
+ * Traverses the DAG breadth-first starting at the target CID and yields all
7
+ * encountered blocks.
8
+ *
9
+ * Blocks linked to from the target block are traversed using codecs defined in
10
+ * the helia config.
11
+ */
12
+ export class SubgraphExporter implements ExportStrategy {
13
+ async * export (_cid: CID, block: BlockView<any, any, any, 0 | 1>): AsyncGenerator<CID, void, undefined> {
14
+ for await (const [, linkedCid] of block.links()) {
15
+ yield linkedCid
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,22 @@
1
+ import { DAG_PB_CODEC_CODE, RAW_PB_CODEC_CODE } from '../constants.js'
2
+ import { NotUnixFSError } from '../errors.js'
3
+ import type { ExportStrategy } from '../index.js'
4
+ import type { BlockView } from 'multiformats/block/interface'
5
+ import type { CID } from 'multiformats/cid'
6
+
7
+ /**
8
+ * This exporter is used when you want to generate a car file that contains a
9
+ * single UnixFS file or directory
10
+ */
11
+ export class UnixFSExporter implements ExportStrategy {
12
+ async * export (cid: CID, block: BlockView<any, any, any, 0 | 1>): AsyncGenerator<CID, void, undefined> {
13
+ if (cid.code !== DAG_PB_CODEC_CODE && cid.code !== RAW_PB_CODEC_CODE) {
14
+ throw new NotUnixFSError('Target CID was not UnixFS - use the SubGraphExporter to export arbitrary graphs')
15
+ }
16
+
17
+ // yield all the blocks that make up the file or directory
18
+ for await (const [, linkedCid] of block.links()) {
19
+ yield linkedCid
20
+ }
21
+ }
22
+ }
package/src/index.ts CHANGED
@@ -1,39 +1,56 @@
1
1
  /**
2
2
  * @packageDocumentation
3
3
  *
4
- * `@helia/car` provides `import` and `export` methods to read/write Car files to {@link https://github.com/ipfs/helia Helia}'s blockstore.
4
+ * `@helia/car` provides `import` and `export` methods to read/write Car files
5
+ * to {@link https://github.com/ipfs/helia Helia}'s blockstore.
5
6
  *
6
7
  * See the {@link Car} interface for all available operations.
7
8
  *
8
- * By default it supports `dag-pb`, `dag-cbor`, `dag-json` and `raw` CIDs, more esoteric DAG walkers can be passed as an init option.
9
+ * By default it supports `dag-pb`, `dag-cbor`, `dag-json` and `raw` CIDs, more
10
+ * esoteric DAG walkers can be passed as an init option.
9
11
  *
10
12
  * @example Exporting a DAG as a CAR file
11
13
  *
12
14
  * ```typescript
13
15
  * import { createHelia } from 'helia'
14
- * import { unixfs } from '@helia/unixfs'
15
16
  * import { car } from '@helia/car'
16
- * import { CarWriter } from '@ipld/car'
17
- * import { Readable } from 'node:stream'
17
+ * import { CID } from 'multiformats/cid'
18
18
  * import nodeFs from 'node:fs'
19
19
  *
20
- * const helia = await createHelia({
21
- * // ... helia config
22
- * })
23
- * const fs = unixfs(helia)
20
+ * const helia = await createHelia()
21
+ * const cid = CID.parse('QmFoo...')
24
22
  *
25
- * // add some UnixFS data
26
- * const cid = await fs.addBytes(Uint8Array.from([0, 1, 2, 3, 4]))
23
+ * const c = car(helia)
24
+ * const out = nodeFs.createWriteStream('example.car')
25
+ *
26
+ * for await (const buf of c.stream(cid)) {
27
+ * out.write(buf)
28
+ * }
29
+ *
30
+ * out.end()
31
+ * ```
32
+ *
33
+ * @example Exporting a part of a UnixFS DAG as a CAR file
34
+ *
35
+ * ```typescript
36
+ * import { createHelia } from 'helia'
37
+ * import { car, UnixFSPath } from '@helia/car'
38
+ * import { CID } from 'multiformats/cid'
39
+ * import nodeFs from 'node:fs'
40
+ *
41
+ * const helia = await createHelia()
42
+ * const cid = CID.parse('QmFoo...')
27
43
  *
28
- * // export it as a Car
29
44
  * const c = car(helia)
30
- * const { writer, out } = await CarWriter.create(cid)
45
+ * const out = nodeFs.createWriteStream('example.car')
31
46
  *
32
- * // `out` needs to be directed somewhere, see the @ipld/car docs for more information
33
- * Readable.from(out).pipe(nodeFs.createWriteStream('example.car'))
47
+ * for await (const buf of c.stream(cid, {
48
+ * traversal: new UnixFSPath('/foo/bar/baz.txt')
49
+ * })) {
50
+ * out.write(buf)
51
+ * }
34
52
  *
35
- * // write the DAG behind `cid` into the writer
36
- * await c.export(cid, writer)
53
+ * out.end()
37
54
  * ```
38
55
  *
39
56
  * @example Importing all blocks from a CAR file
@@ -59,31 +76,83 @@
59
76
  * ```
60
77
  */
61
78
 
62
- import { CarWriter } from '@ipld/car'
63
- import drain from 'it-drain'
64
- import map from 'it-map'
65
- import { createUnsafe } from 'multiformats/block'
66
- import defer from 'p-defer'
67
- import PQueue from 'p-queue'
79
+ import { Car as CarClass } from './car.js'
68
80
  import type { CodecLoader } from '@helia/interface'
69
- import type { GetBlockProgressEvents, PutManyBlocksProgressEvents } from '@helia/interface/blocks'
70
- import type { CarReader } from '@ipld/car'
71
- import type { AbortOptions } from '@libp2p/interface'
81
+ import type { PutManyBlocksProgressEvents, GetBlockProgressEvents } from '@helia/interface/blocks'
82
+ import type { CarWriter, CarReader } from '@ipld/car'
83
+ import type { AbortOptions, ComponentLogger } from '@libp2p/interface'
72
84
  import type { Filter } from '@libp2p/utils/filters'
73
85
  import type { Blockstore } from 'interface-blockstore'
86
+ import type { BlockView } from 'multiformats/block/interface'
74
87
  import type { CID } from 'multiformats/cid'
75
88
  import type { ProgressOptions } from 'progress-events'
76
89
 
77
90
  export interface CarComponents {
91
+ logger: ComponentLogger
78
92
  blockstore: Blockstore
79
93
  getCodec: CodecLoader
80
94
  }
81
95
 
96
+ /**
97
+ * Interface for different traversal strategies.
98
+ *
99
+ * While traversing the DAG, it will yield blocks that it has traversed.
100
+ */
101
+ export interface TraversalStrategy {
102
+ /**
103
+ * Traverse the DAG and yield the next CID to traverse
104
+ */
105
+ traverse<T extends BlockView<any, any, any, 0 | 1>>(cid: CID, block: T): AsyncGenerator<CID, void, undefined>
106
+
107
+ /**
108
+ * Returns true if the current CID is the target and we should switch to the
109
+ * export strategy
110
+ */
111
+ isTarget(cid: CID): boolean
112
+ }
113
+
114
+ /**
115
+ * Interface for different export strategies.
116
+ *
117
+ * When traversal has ended the export begins starting at the target CID, and
118
+ * the export strategy may do further traversal and writing to the car file.
119
+ */
120
+ export interface ExportStrategy {
121
+ /**
122
+ * Export the DAG and yield the next CID to traverse
123
+ */
124
+ export<T extends BlockView<any, any, any, 0 | 1>>(cid: CID, block: T): AsyncGenerator<CID, void, undefined>
125
+ }
126
+
127
+ export * from './export-strategies/index.js'
128
+ export * from './traversal-strategies/index.js'
129
+
82
130
  export interface ExportCarOptions extends AbortOptions, ProgressOptions<GetBlockProgressEvents> {
131
+
132
+ /**
133
+ * If true, the blockstore will not do any network requests.
134
+ *
135
+ * @default false
136
+ */
137
+ offline?: boolean
138
+
83
139
  /**
84
- * If a filter is passed it will be used to deduplicate blocks exported in the car file
140
+ * If a filter is passed it will be used to deduplicate blocks exported in the
141
+ * car file
85
142
  */
86
143
  blockFilter?: Filter
144
+
145
+ /**
146
+ * The traversal strategy to use for the export. This determines how the dag
147
+ * is traversed: either depth first, breadth first, or a custom strategy.
148
+ */
149
+ traversal?: TraversalStrategy
150
+
151
+ /**
152
+ * Export strategy to use for the export. This should be used to change the
153
+ * blocks included in the exported car file. (e.g. https://specs.ipfs.tech/http-gateways/trustless-gateway/#dag-scope-request-query-parameter)
154
+ */
155
+ exporter?: ExportStrategy
87
156
  }
88
157
 
89
158
  /**
@@ -152,7 +221,7 @@ export interface Car {
152
221
  *
153
222
  * ```typescript
154
223
  * import { createHelia } from 'helia'
155
- * import { car } from '@helia/car
224
+ * import { car } from '@helia/car'
156
225
  * import { CID } from 'multiformats/cid'
157
226
  *
158
227
  * const helia = await createHelia()
@@ -165,101 +234,12 @@ export interface Car {
165
234
  * }
166
235
  * ```
167
236
  */
168
- stream(root: CID | CID[], options?: AbortOptions & ProgressOptions<GetBlockProgressEvents>): AsyncGenerator<Uint8Array>
169
- }
170
-
171
- const DAG_WALK_QUEUE_CONCURRENCY = 1
172
-
173
- class DefaultCar implements Car {
174
- private readonly components: CarComponents
175
-
176
- constructor (components: CarComponents, init: any) {
177
- this.components = components
178
- }
179
-
180
- async import (reader: Pick<CarReader, 'blocks'>, options?: AbortOptions & ProgressOptions<PutManyBlocksProgressEvents>): Promise<void> {
181
- await drain(this.components.blockstore.putMany(
182
- map(reader.blocks(), ({ cid, bytes }) => ({ cid, block: bytes })),
183
- options
184
- ))
185
- }
186
-
187
- async export (root: CID | CID[], writer: Pick<CarWriter, 'put' | 'close'>, options?: ExportCarOptions): Promise<void> {
188
- const deferred = defer<Error | undefined>()
189
- const roots = Array.isArray(root) ? root : [root]
190
-
191
- // use a queue to walk the DAG instead of recursion so we can traverse very large DAGs
192
- const queue = new PQueue({
193
- concurrency: DAG_WALK_QUEUE_CONCURRENCY
194
- })
195
- queue.on('idle', () => {
196
- deferred.resolve()
197
- })
198
- queue.on('error', (err) => {
199
- queue.clear()
200
- deferred.reject(err)
201
- })
202
-
203
- for (const root of roots) {
204
- void queue.add(async () => {
205
- await this.#walkDag(root, queue, async (cid, bytes) => {
206
- // if a filter has been passed, skip blocks that have already been written
207
- if (options?.blockFilter?.has(cid.multihash.bytes) === true) {
208
- return
209
- }
210
-
211
- options?.blockFilter?.add(cid.multihash.bytes)
212
- await writer.put({ cid, bytes })
213
- }, options)
214
- })
215
- .catch(() => {})
216
- }
217
-
218
- // wait for the writer to end
219
- try {
220
- await deferred.promise
221
- } finally {
222
- await writer.close()
223
- }
224
- }
225
-
226
- async * stream (root: CID | CID[], options?: ExportCarOptions): AsyncGenerator<Uint8Array, void, undefined> {
227
- const { writer, out } = CarWriter.create(root)
228
-
229
- // has to be done async so we write to `writer` and read from `out` at the
230
- // same time
231
- this.export(root, writer, options)
232
- .catch(() => {})
233
-
234
- for await (const buf of out) {
235
- yield buf
236
- }
237
- }
238
-
239
- /**
240
- * Walk the DAG behind the passed CID, ensure all blocks are present in the blockstore
241
- * and update the pin count for them
242
- */
243
- async #walkDag (cid: CID, queue: PQueue, withBlock: (cid: CID, block: Uint8Array) => Promise<void>, options?: AbortOptions & ProgressOptions<GetBlockProgressEvents>): Promise<void> {
244
- const codec = await this.components.getCodec(cid.code)
245
- const bytes = await this.components.blockstore.get(cid, options)
246
-
247
- await withBlock(cid, bytes)
248
-
249
- const block = createUnsafe({ bytes, cid, codec })
250
-
251
- // walk dag, ensure all blocks are present
252
- for await (const [,cid] of block.links()) {
253
- void queue.add(async () => {
254
- await this.#walkDag(cid, queue, withBlock, options)
255
- })
256
- }
257
- }
237
+ stream(root: CID | CID[], options?: ExportCarOptions): AsyncGenerator<Uint8Array, void, undefined>
258
238
  }
259
239
 
260
240
  /**
261
241
  * Create a {@link Car} instance for use with {@link https://github.com/ipfs/helia Helia}
262
242
  */
263
243
  export function car (helia: CarComponents, init: any = {}): Car {
264
- return new DefaultCar(helia, init)
244
+ return new CarClass(helia, init)
265
245
  }
@@ -0,0 +1,29 @@
1
+ import type { TraversalStrategy } from '../index.js'
2
+ import type { BlockView } from 'multiformats/block/interface'
3
+ import type { CID } from 'multiformats/cid'
4
+
5
+ /**
6
+ * Simple strategy that traverses a known path to a target CID.
7
+ *
8
+ * All this strategy does is yield the next CID in the known path.
9
+ */
10
+ export class CIDPath implements TraversalStrategy {
11
+ private readonly pathToTarget: CID[]
12
+ private readonly target: CID
13
+
14
+ constructor (pathToTarget: CID[]) {
15
+ this.pathToTarget = pathToTarget
16
+ this.target = pathToTarget[pathToTarget.length - 1]
17
+ }
18
+
19
+ isTarget (cid: CID): boolean {
20
+ return this.target.equals(cid)
21
+ }
22
+
23
+ async * traverse <T extends BlockView<any, any, any, 0 | 1>>(cid: CID, _block?: T): AsyncGenerator<CID, void, undefined> {
24
+ const givenCidIndex = this.pathToTarget.indexOf(cid)
25
+ const nextCid = this.pathToTarget[givenCidIndex + 1]
26
+
27
+ yield nextCid
28
+ }
29
+ }
@@ -0,0 +1,25 @@
1
+ import type { TraversalStrategy } from '../index.js'
2
+ import type { BlockView } from 'multiformats/block/interface'
3
+ import type { CID } from 'multiformats/cid'
4
+
5
+ /**
6
+ * A traversal strategy that performs a breadth-first search (so as to not load blocks unnecessarily) looking for a
7
+ * target CID. Traversal stops when we reach the target CID or run out of nodes.
8
+ */
9
+ export class GraphSearch implements TraversalStrategy {
10
+ private readonly target: CID
11
+
12
+ constructor (target: CID) {
13
+ this.target = target
14
+ }
15
+
16
+ isTarget (cid: CID): boolean {
17
+ return this.target.equals(cid)
18
+ }
19
+
20
+ async * traverse <T extends BlockView<any, any, any, 0 | 1>>(cid: CID, block: T): AsyncGenerator<CID, void, undefined> {
21
+ for await (const [, linkedCid] of block.links()) {
22
+ yield linkedCid
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ export * from './graph-search.js'
2
+ export * from './cid-path.js'
3
+ export * from './unixfs-path.js'
@@ -0,0 +1,58 @@
1
+ import { decode } from '@ipld/dag-pb'
2
+ import { UnixFS } from 'ipfs-unixfs'
3
+ import { DAG_PB_CODEC_CODE } from '../constants.js'
4
+ import { NotUnixFSError } from '../errors.js'
5
+ import type { TraversalStrategy } from '../index.js'
6
+ import type { BlockView } from 'multiformats/block/interface'
7
+ import type { CID } from 'multiformats/cid'
8
+
9
+ /**
10
+ * Traverses a DAG containing UnixFS directories
11
+ */
12
+ export class UnixFSPath implements TraversalStrategy {
13
+ private readonly path: string[]
14
+
15
+ constructor (path: string) {
16
+ // "/foo/bar/baz.txt" -> ['foo', 'bar', 'baz.txt']
17
+ this.path = path.replace(/^\//, '').split('/')
18
+ }
19
+
20
+ isTarget (): boolean {
21
+ return this.path.length === 0
22
+ }
23
+
24
+ async * traverse <T extends BlockView<any, any, any, 0 | 1>>(cid: CID, block: T): AsyncGenerator<CID, void, undefined> {
25
+ if (cid.code !== DAG_PB_CODEC_CODE) {
26
+ throw new NotUnixFSError('Target CID is not UnixFS')
27
+ }
28
+
29
+ const segment = this.path.shift()
30
+
31
+ if (segment == null) {
32
+ return
33
+ }
34
+
35
+ const pb = decode(block.bytes)
36
+
37
+ if (pb.Data == null) {
38
+ throw new NotUnixFSError('Target CID has no UnixFS data in decoded bytes')
39
+ }
40
+
41
+ const unixfs = UnixFS.unmarshal(pb.Data)
42
+
43
+ if (unixfs.type === 'directory') {
44
+ const link = pb.Links.filter(link => link.Name === segment).pop()
45
+
46
+ if (link == null) {
47
+ throw new NotUnixFSError(`Target CID directory has no link with name ${segment}`)
48
+ }
49
+
50
+ yield link.Hash
51
+ return
52
+ }
53
+
54
+ // TODO: HAMT support
55
+
56
+ throw new NotUnixFSError('Target CID is not a UnixFS directory')
57
+ }
58
+ }
@@ -1,10 +0,0 @@
1
- {
2
- "Car": "https://ipfs.github.io/helia/interfaces/_helia_car.Car.html",
3
- ".:Car": "https://ipfs.github.io/helia/interfaces/_helia_car.Car.html",
4
- "CarComponents": "https://ipfs.github.io/helia/interfaces/_helia_car.CarComponents.html",
5
- ".:CarComponents": "https://ipfs.github.io/helia/interfaces/_helia_car.CarComponents.html",
6
- "ExportCarOptions": "https://ipfs.github.io/helia/interfaces/_helia_car.ExportCarOptions.html",
7
- ".:ExportCarOptions": "https://ipfs.github.io/helia/interfaces/_helia_car.ExportCarOptions.html",
8
- "car": "https://ipfs.github.io/helia/functions/_helia_car.car-1.html",
9
- ".:car": "https://ipfs.github.io/helia/functions/_helia_car.car-1.html"
10
- }