@helia/car 5.0.1 → 5.1.0

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 (50) hide show
  1. package/README.md +54 -4
  2. package/dist/index.min.js +1 -1
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/car.d.ts +1 -2
  5. package/dist/src/car.d.ts.map +1 -1
  6. package/dist/src/car.js +46 -124
  7. package/dist/src/car.js.map +1 -1
  8. package/dist/src/errors.d.ts +8 -0
  9. package/dist/src/errors.d.ts.map +1 -1
  10. package/dist/src/errors.js +8 -0
  11. package/dist/src/errors.js.map +1 -1
  12. package/dist/src/export-strategies/block-exporter.d.ts +5 -2
  13. package/dist/src/export-strategies/block-exporter.d.ts.map +1 -1
  14. package/dist/src/export-strategies/block-exporter.js +9 -3
  15. package/dist/src/export-strategies/block-exporter.js.map +1 -1
  16. package/dist/src/export-strategies/subgraph-exporter.d.ts +5 -2
  17. package/dist/src/export-strategies/subgraph-exporter.d.ts.map +1 -1
  18. package/dist/src/export-strategies/subgraph-exporter.js +8 -3
  19. package/dist/src/export-strategies/subgraph-exporter.js.map +1 -1
  20. package/dist/src/export-strategies/unixfs-exporter.d.ts +10 -4
  21. package/dist/src/export-strategies/unixfs-exporter.d.ts.map +1 -1
  22. package/dist/src/export-strategies/unixfs-exporter.js +15 -8
  23. package/dist/src/export-strategies/unixfs-exporter.js.map +1 -1
  24. package/dist/src/index.d.ts +64 -12
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js +56 -6
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/traversal-strategies/cid-path.d.ts +9 -6
  29. package/dist/src/traversal-strategies/cid-path.d.ts.map +1 -1
  30. package/dist/src/traversal-strategies/cid-path.js +35 -12
  31. package/dist/src/traversal-strategies/cid-path.js.map +1 -1
  32. package/dist/src/traversal-strategies/graph-search.d.ts +15 -7
  33. package/dist/src/traversal-strategies/graph-search.d.ts.map +1 -1
  34. package/dist/src/traversal-strategies/graph-search.js +57 -11
  35. package/dist/src/traversal-strategies/graph-search.js.map +1 -1
  36. package/dist/src/traversal-strategies/unixfs-path.d.ts +53 -3
  37. package/dist/src/traversal-strategies/unixfs-path.d.ts.map +1 -1
  38. package/dist/src/traversal-strategies/unixfs-path.js +70 -30
  39. package/dist/src/traversal-strategies/unixfs-path.js.map +1 -1
  40. package/dist/typedoc-urls.json +1 -0
  41. package/package.json +4 -2
  42. package/src/car.ts +54 -163
  43. package/src/errors.ts +10 -0
  44. package/src/export-strategies/block-exporter.ts +13 -4
  45. package/src/export-strategies/subgraph-exporter.ts +13 -4
  46. package/src/export-strategies/unixfs-exporter.ts +20 -9
  47. package/src/index.ts +66 -15
  48. package/src/traversal-strategies/cid-path.ts +43 -13
  49. package/src/traversal-strategies/graph-search.ts +70 -12
  50. package/src/traversal-strategies/unixfs-path.ts +77 -41
package/src/car.ts CHANGED
@@ -1,50 +1,22 @@
1
1
  import { CarWriter } from '@ipld/car'
2
- import { Queue } from '@libp2p/utils'
3
2
  import drain from 'it-drain'
4
3
  import map from 'it-map'
5
- import toBuffer from 'it-to-buffer'
6
- import { createUnsafe } from 'multiformats/block'
7
4
  import { raceSignal } from 'race-signal'
8
- import { DAG_WALK_QUEUE_CONCURRENCY } from './constants.js'
5
+ import { DAG_PB_CODEC_CODE } from './constants.ts'
9
6
  import { SubgraphExporter } from './export-strategies/subgraph-exporter.js'
10
- import { GraphSearch } from './traversal-strategies/graph-search.js'
11
- import type { CarComponents, Car as CarInterface, ExportCarOptions, ExportStrategy, TraversalStrategy } from './index.js'
7
+ import { UnixFSExporter } from './index.js'
8
+ import type { CarComponents, Car as CarInterface, ExportCarOptions } from './index.js'
12
9
  import type { PutManyBlocksProgressEvents } from '@helia/interface/blocks'
13
10
  import type { CarReader } from '@ipld/car'
14
11
  import type { AbortOptions, Logger } from '@libp2p/interface'
15
12
  import type { CID } from 'multiformats/cid'
16
13
  import type { ProgressOptions } from 'progress-events'
17
14
 
18
- /**
19
- * Context for the traversal process.
20
- */
21
- interface TraversalContext {
22
- currentPath: CID[]
23
- pathsToTarget: CID[][] | null // collect all target paths
24
- }
25
-
26
- interface WalkDagContext<Strategy> {
27
- cid: CID
28
- queue: Queue
29
- strategy: Strategy
30
- options?: ExportCarOptions
31
- }
32
-
33
- interface ExportWalkDagContext extends WalkDagContext<ExportStrategy> {
34
- writer: Pick<CarWriter, 'put'>
35
- recursive?: boolean
36
- }
37
-
38
- interface TraversalWalkDagContext extends WalkDagContext<TraversalStrategy> {
39
- traversalContext: TraversalContext
40
- parentPath: CID[]
41
- }
42
-
43
15
  export class Car implements CarInterface {
44
16
  private readonly components: CarComponents
45
17
  private readonly log: Logger
46
18
 
47
- constructor (components: CarComponents, init: any) {
19
+ constructor (components: CarComponents) {
48
20
  this.components = components
49
21
  this.log = components.logger.forComponent('helia:car')
50
22
  }
@@ -91,148 +63,67 @@ export class Car implements CarInterface {
91
63
  }
92
64
 
93
65
  private async _export (root: CID | CID[], writer: Pick<CarWriter, 'put' | 'close'>, options?: ExportCarOptions): Promise<void> {
94
- const deferred = Promise.withResolvers<Error | void>()
95
66
  const roots = Array.isArray(root) ? root : [root]
96
-
97
- // Create traversal-specific context
98
- const traversalContext: TraversalContext = {
99
- currentPath: [],
100
- pathsToTarget: null
101
- }
102
-
103
67
  const traversalStrategy = options?.traversal
104
- const exportStrategy = options?.exporter ?? new SubgraphExporter()
105
-
106
- // use a queue to walk the DAG instead of recursion so we can traverse very
107
- // large DAGs
108
- const queue = new Queue({
109
- concurrency: DAG_WALK_QUEUE_CONCURRENCY
110
- })
111
-
112
- let startedExport = false
113
- queue.addEventListener('idle', () => {
114
- if (startedExport) {
115
- // idle event was called, and started exporting, so we are done.
116
- deferred.resolve()
117
- } else if (!startedExport && traversalContext.pathsToTarget?.length === roots.length) {
118
- // queue is idle, we haven't started exporting yet, and we have path(s)
119
- // to the target(s), so we can start the export process.
120
- this.log.trace('starting export of blocks to the car file')
121
- startedExport = true
122
-
123
- for (const path of traversalContext.pathsToTarget) {
124
- const targetIndex = path.length - 1
125
- const targetCid = path[targetIndex]
126
-
127
- // Process all verification blocks in the path except the target
128
- path.slice(0, -1).forEach(cid => {
129
- queue.add(async () => {
130
- await this.#exportDagNode({ cid, queue, writer, strategy: exportStrategy, options, recursive: false })
131
- })
132
- .catch((err) => {
133
- this.log.error('error during queue operation - %e', err)
134
- })
135
- })
136
-
137
- // Process the target block (which will recursively export its DAG)
138
- queue.add(async () => {
139
- await this.#exportDagNode({ cid: targetCid, queue, writer, strategy: exportStrategy, options })
140
- })
141
- .catch((err) => {
142
- this.log.error('error during queue operation - %e', err)
68
+
69
+ for (const root of roots) {
70
+ const exportStrategy = options?.exporter ?? (root.code === DAG_PB_CODEC_CODE ? new UnixFSExporter() : new SubgraphExporter())
71
+ let current = root
72
+ let underRoot = false
73
+
74
+ if (traversalStrategy != null) {
75
+ for await (const { cid, bytes } of traversalStrategy.traverse(current, this.components.blockstore, this.components.getCodec, options)) {
76
+ this.log.trace('next CID on path to %c is %c', root, cid)
77
+ current = cid
78
+
79
+ // the traversal is under the root we are exporting
80
+ if (root.equals(cid)) {
81
+ underRoot = true
82
+ }
83
+
84
+ // include the traversal block if we are under the root CID or it has
85
+ // been explicitly requested
86
+ if (underRoot || options?.includeTraversalBlocks === true) {
87
+ // eslint-disable-next-line max-depth
88
+ if (options?.blockFilter?.has(cid.multihash.bytes) === true) {
89
+ continue
90
+ }
91
+
92
+ // mark as processed
93
+ options?.blockFilter?.add(cid.multihash.bytes)
94
+
95
+ // store block on path to target
96
+ await writer.put({
97
+ cid,
98
+ bytes
143
99
  })
100
+ }
144
101
  }
145
- } else {
146
- // queue is idle, we haven't started exporting yet, and we don't have
147
- // path(s) to the target(s), so we can't start the export process.
148
- // this should not happen without a separate error during traversal, but
149
- // we'll handle it here anyway.
150
- this.log.trace('no paths to target, skipping export')
151
- deferred.reject(new Error('Could not traverse to target CID(s)'))
152
102
  }
153
- })
154
- queue.addEventListener('failure', (evt) => {
155
- queue.clear()
156
- deferred.reject(evt.detail.error)
157
- })
158
-
159
- for (const root of roots) {
160
- void queue.add(async () => {
161
- this.log.trace('traversing dag from %c', root)
162
- await this.#traverseDagNode({ cid: root, queue, strategy: traversalStrategy ?? new GraphSearch(root), traversalContext, parentPath: [], options })
163
- })
164
- .catch((err) => {
165
- this.log.error('error during queue operation - %e', err)
166
- })
167
- }
168
103
 
169
- // wait for the writer to end
170
- try {
171
- await deferred.promise
172
- } finally {
173
- await writer.close()
174
- }
175
- }
176
-
177
- /**
178
- * Traverse a DAG and stop when we reach the target node
179
- */
180
- async #traverseDagNode ({ cid, queue, strategy, traversalContext, parentPath, options }: TraversalWalkDagContext): Promise<void> {
181
- // if we are traversing, we need to gather path(s) to the target(s)
182
- const currentPath = [...parentPath, cid]
183
-
184
- if (strategy.isTarget(cid)) {
185
- traversalContext.pathsToTarget = traversalContext.pathsToTarget ?? []
186
- traversalContext.pathsToTarget.push([...currentPath])
187
- this.log.trace('found path to target %c', cid)
188
- return
189
- }
190
-
191
- const codec = await this.components.getCodec(cid.code)
192
- const bytes = await toBuffer(this.components.blockstore.get(cid, options))
193
-
194
- // we are recursively traversing the dag
195
- const decodedBlock = createUnsafe({ bytes, cid, codec })
196
-
197
- for await (const nextCid of strategy.traverse(cid, decodedBlock)) {
198
- void queue.add(async () => {
199
- await this.#traverseDagNode({ cid: nextCid, queue, strategy, traversalContext, parentPath: currentPath ?? [], options })
200
- })
201
- .catch((err) => {
202
- this.log.error('error during traversal queue operation - %e', err)
203
- })
204
- }
205
- }
206
-
207
- /**
208
- * Use an ExportStrategy to export part of all of a DAG
209
- */
210
- async #exportDagNode ({ cid, queue, writer, strategy, options, recursive = true }: ExportWalkDagContext): Promise<void> {
211
- if (options?.blockFilter?.has(cid.multihash.bytes) === true) {
212
- return
213
- }
214
-
215
- const codec = await this.components.getCodec(cid.code)
216
- const bytes = await toBuffer(this.components.blockstore.get(cid, options))
217
-
218
- // Mark as processed
219
- options?.blockFilter?.add(cid.multihash.bytes)
104
+ for await (const { cid, bytes } of exportStrategy.export(current, this.components.blockstore, this.components.getCodec, options)) {
105
+ if (options?.blockFilter?.has(cid.multihash.bytes) === true) {
106
+ continue
107
+ }
220
108
 
221
- // Write to CAR
222
- await writer.put({ cid, bytes })
109
+ // skip the export target block if we have already included it during
110
+ // traversal
111
+ if (underRoot && cid.equals(current)) {
112
+ continue
113
+ }
223
114
 
224
- if (recursive) {
225
- // we are recursively traversing the dag
226
- const decodedBlock = createUnsafe({ bytes, cid, codec })
115
+ // mark as processed
116
+ options?.blockFilter?.add(cid.multihash.bytes)
227
117
 
228
- for await (const nextCid of strategy.export(cid, decodedBlock)) {
229
- void queue.add(async () => {
230
- await this.#exportDagNode({ cid: nextCid, queue, writer, strategy, options })
118
+ // write to CAR
119
+ await writer.put({
120
+ cid,
121
+ bytes
231
122
  })
232
- .catch((err) => {
233
- this.log.error('error during export queue operation - %e', err)
234
- })
235
123
  }
236
124
  }
125
+
126
+ // wait for the writer to end
127
+ await writer.close()
237
128
  }
238
129
  }
package/src/errors.ts CHANGED
@@ -6,3 +6,13 @@ export class NotUnixFSError extends Error {
6
6
  message = 'Not a UnixFS node'
7
7
  name = 'NotUnixFSError'
8
8
  }
9
+
10
+ export class NotDescendantError extends Error {
11
+ static name = 'NotDescendantError'
12
+ name = 'NotDescendantError'
13
+ }
14
+
15
+ export class InvalidTraversalError extends Error {
16
+ static name = 'InvalidTraversalError'
17
+ name = 'InvalidTraversalError'
18
+ }
@@ -1,13 +1,22 @@
1
+ import toBuffer from 'it-to-buffer'
2
+ import { createUnsafe } from 'multiformats/block'
1
3
  import type { ExportStrategy } from '../index.js'
2
- import type { BlockView } from 'multiformats/block/interface'
4
+ import type { CodecLoader } from '@helia/interface'
5
+ import type { AbortOptions } from '@libp2p/interface'
6
+ import type { Blockstore } from 'interface-blockstore'
7
+ import type { BlockView } from 'multiformats'
3
8
  import type { CID } from 'multiformats/cid'
4
9
 
5
10
  /**
6
11
  * Yields the first block from the first CID and stops
7
12
  */
8
13
  export class BlockExporter implements ExportStrategy {
9
- async * export (cid: CID, block: BlockView<any, any, any, 0 | 1>): AsyncGenerator<CID, void, undefined> {
10
- // don't yield the block, index.ts will add it to the car file and then
11
- // we're done
14
+ async * export (cid: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined> {
15
+ const bytes = await toBuffer(blockstore.get(cid, options))
16
+ yield createUnsafe({
17
+ cid,
18
+ bytes,
19
+ codec: await getCodec(cid.code)
20
+ })
12
21
  }
13
22
  }
@@ -1,5 +1,9 @@
1
+ import { breadthFirstWalker } from '@helia/utils'
1
2
  import type { ExportStrategy } from '../index.js'
2
- import type { BlockView } from 'multiformats/block/interface'
3
+ import type { CodecLoader } from '@helia/interface'
4
+ import type { AbortOptions } from '@libp2p/interface'
5
+ import type { Blockstore } from 'interface-blockstore'
6
+ import type { BlockView } from 'multiformats'
3
7
  import type { CID } from 'multiformats/cid'
4
8
 
5
9
  /**
@@ -10,9 +14,14 @@ import type { CID } from 'multiformats/cid'
10
14
  * the helia config.
11
15
  */
12
16
  export class SubgraphExporter implements ExportStrategy {
13
- async * export (_cid: CID, block: BlockView<any, any, any, 0 | 1>): AsyncGenerator<CID, void, undefined> {
14
- for (const [, linkedCid] of block.links()) {
15
- yield linkedCid
17
+ async * export (cid: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined> {
18
+ const walker = breadthFirstWalker({
19
+ blockstore,
20
+ getCodec
21
+ })
22
+
23
+ for await (const node of walker.walk(cid, options)) {
24
+ yield node.block
16
25
  }
17
26
  }
18
27
  }
@@ -1,22 +1,33 @@
1
- import { DAG_PB_CODEC_CODE, RAW_PB_CODEC_CODE } from '../constants.js'
2
- import { NotUnixFSError } from '../errors.js'
1
+ import { depthFirstWalker } from '@helia/utils'
2
+ import { DAG_PB_CODEC_CODE, RAW_PB_CODEC_CODE } from '../constants.ts'
3
+ import { NotUnixFSError } from '../errors.ts'
3
4
  import type { ExportStrategy } from '../index.js'
4
- import type { BlockView } from 'multiformats/block/interface'
5
+ import type { CodecLoader } from '@helia/interface'
6
+ import type { AbortOptions } from '@libp2p/interface'
7
+ import type { Blockstore } from 'interface-blockstore'
8
+ import type { BlockView } from 'multiformats'
5
9
  import type { CID } from 'multiformats/cid'
6
10
 
7
11
  /**
8
- * This exporter is used when you want to generate a car file that contains a
9
- * single UnixFS file or directory
12
+ * Traverses the DAG depth-first starting at the target CID and yields all
13
+ * encountered blocks.
14
+ *
15
+ * Blocks linked to from the target block are traversed using codecs defined in
16
+ * the helia config.
10
17
  */
11
18
  export class UnixFSExporter implements ExportStrategy {
12
- async * export (cid: CID, block: BlockView<any, any, any, 0 | 1>): AsyncGenerator<CID, void, undefined> {
19
+ async * export (cid: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined> {
13
20
  if (cid.code !== DAG_PB_CODEC_CODE && cid.code !== RAW_PB_CODEC_CODE) {
14
21
  throw new NotUnixFSError('Target CID was not UnixFS - use the SubGraphExporter to export arbitrary graphs')
15
22
  }
16
23
 
17
- // yield all the blocks that make up the file or directory
18
- for (const [, linkedCid] of block.links()) {
19
- yield linkedCid
24
+ const walker = depthFirstWalker({
25
+ blockstore,
26
+ getCodec
27
+ })
28
+
29
+ for await (const node of walker.walk(cid, options)) {
30
+ yield node.block
20
31
  }
21
32
  }
22
33
  }
package/src/index.ts CHANGED
@@ -23,7 +23,9 @@
23
23
  * const c = car(helia)
24
24
  * const out = nodeFs.createWriteStream('example.car')
25
25
  *
26
- * for await (const buf of c.export(cid)) {
26
+ * for await (const buf of c.export(cid, {
27
+ * signal: AbortSignal.timeout(5_000)
28
+ * })) {
27
29
  * out.write(buf)
28
30
  * }
29
31
  *
@@ -32,6 +34,14 @@
32
34
  *
33
35
  * @example Exporting a part of a UnixFS DAG as a CAR file
34
36
  *
37
+ * Here the graph traversal will start at `root` and include the blocks for
38
+ * `root`, `/foo`, `/bar`, and all the blocks that make up `baz.txt`.
39
+ *
40
+ * If there are other files/directories in the UnixFS DAG under `root`, they
41
+ * will not be included.
42
+ *
43
+ * `root` will be the only entry in the CAR file roots.
44
+ *
35
45
  * ```typescript
36
46
  * import { createHelia } from 'helia'
37
47
  * import { car, UnixFSPath } from '@helia/car'
@@ -39,12 +49,13 @@
39
49
  * import nodeFs from 'node:fs'
40
50
  *
41
51
  * const helia = await createHelia()
42
- * const cid = CID.parse('QmFoo...')
52
+ * const root = CID.parse('QmFoo...')
43
53
  *
44
54
  * const c = car(helia)
45
55
  * const out = nodeFs.createWriteStream('example.car')
46
56
  *
47
- * for await (const buf of c.export(cid, {
57
+ * for await (const buf of c.export(root, {
58
+ * signal: AbortSignal.timeout(5_000),
48
59
  * traversal: new UnixFSPath('/foo/bar/baz.txt')
49
60
  * })) {
50
61
  * out.write(buf)
@@ -53,6 +64,43 @@
53
64
  * out.end()
54
65
  * ```
55
66
  *
67
+ * @example Including traversal path above the root in a CAR
68
+ *
69
+ * The `includeTraversalBlocks` option will include the traversal blocks in the
70
+ * CAR when they would otherwise be excluded (for example when the traversal
71
+ * starts in a parent of the export root).
72
+ *
73
+ * Here `baz` is the CID for `baz.txt`.
74
+ *
75
+ * The CAR file will include the blocks for `parent`, `/foo`, `/bar`, and
76
+ * `/baz.txt`.
77
+ *
78
+ * `baz` will be the only entry in the CAR file roots.
79
+ *
80
+ * ```typescript
81
+ * import { createHelia } from 'helia'
82
+ * import { car, UnixFSPath } from '@helia/car'
83
+ * import { CID } from 'multiformats/cid'
84
+ * import nodeFs from 'node:fs'
85
+ *
86
+ * const helia = await createHelia()
87
+ * const parent = CID.parse('QmFoo...')
88
+ * const baz = CID.parse('QmBar...')
89
+ *
90
+ * const c = car(helia)
91
+ * const out = nodeFs.createWriteStream('example.car')
92
+ *
93
+ * for await (const buf of c.export(baz, {
94
+ * signal: AbortSignal.timeout(5_000),
95
+ * traversal: new UnixFSPath(parent, '/foo/bar/baz.txt'),
96
+ * includeTraversalBlocks: true
97
+ * })) {
98
+ * out.write(buf)
99
+ * }
100
+ *
101
+ * out.end()
102
+ * ```
103
+ *
56
104
  * @example Importing all blocks from a CAR file
57
105
  *
58
106
  * ```typescript
@@ -72,7 +120,9 @@
72
120
  * const reader = await CarReader.fromIterable(inStream)
73
121
  *
74
122
  * const c = car(helia)
75
- * await c.import(reader)
123
+ * await c.import(reader, {
124
+ * signal: AbortSignal.timeout(5_000)
125
+ * })
76
126
  * ```
77
127
  */
78
128
 
@@ -102,13 +152,7 @@ export interface TraversalStrategy {
102
152
  /**
103
153
  * Traverse the DAG and yield the next CID to traverse
104
154
  */
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
155
+ traverse(root: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined>
112
156
  }
113
157
 
114
158
  /**
@@ -121,14 +165,13 @@ export interface ExportStrategy {
121
165
  /**
122
166
  * Export the DAG and yield the next CID to traverse
123
167
  */
124
- export<T extends BlockView<any, any, any, 0 | 1>>(cid: CID, block: T): AsyncGenerator<CID, void, undefined>
168
+ export(cid: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined>
125
169
  }
126
170
 
127
171
  export * from './export-strategies/index.js'
128
172
  export * from './traversal-strategies/index.js'
129
173
 
130
174
  export interface ExportCarOptions extends AbortOptions, ProgressOptions<GetBlockProgressEvents>, ProviderOptions {
131
-
132
175
  /**
133
176
  * If true, the blockstore will not do any network requests.
134
177
  *
@@ -153,6 +196,14 @@ export interface ExportCarOptions extends AbortOptions, ProgressOptions<GetBlock
153
196
  * blocks included in the exported car file. (e.g. https://specs.ipfs.tech/http-gateways/trustless-gateway/#dag-scope-request-query-parameter)
154
197
  */
155
198
  exporter?: ExportStrategy
199
+
200
+ /**
201
+ * If `true`, and the traversal strategy starts above the root, include the
202
+ * traversed blocks in the CAR file before the root and subsequent blocks
203
+ *
204
+ * @default false
205
+ */
206
+ includeTraversalBlocks?: boolean
156
207
  }
157
208
 
158
209
  /**
@@ -208,6 +259,6 @@ export interface Car {
208
259
  /**
209
260
  * Create a {@link Car} instance for use with {@link https://github.com/ipfs/helia Helia}
210
261
  */
211
- export function car (helia: CarComponents, init: any = {}): Car {
212
- return new CarClass(helia, init)
262
+ export function car (helia: CarComponents): Car {
263
+ return new CarClass(helia)
213
264
  }
@@ -1,29 +1,59 @@
1
+ import toBuffer from 'it-to-buffer'
2
+ import { createUnsafe } from 'multiformats/block'
3
+ import { InvalidTraversalError, NotDescendantError } from '../errors.js'
1
4
  import type { TraversalStrategy } from '../index.js'
2
- import type { BlockView } from 'multiformats/block/interface'
5
+ import type { CodecLoader } from '@helia/interface'
6
+ import type { AbortOptions } from '@libp2p/interface'
7
+ import type { Blockstore } from 'interface-blockstore'
8
+ import type { BlockView } from 'multiformats'
3
9
  import type { CID } from 'multiformats/cid'
4
10
 
5
11
  /**
6
12
  * Simple strategy that traverses a known path to a target CID.
7
13
  *
8
14
  * All this strategy does is yield the next CID in the known path.
15
+ *
16
+ * The path should end with the CID to be exported
9
17
  */
10
18
  export class CIDPath implements TraversalStrategy {
11
- private readonly pathToTarget: CID[]
12
- private readonly target: CID
19
+ private readonly path: CID[]
13
20
 
14
- constructor (pathToTarget: CID[]) {
15
- this.pathToTarget = pathToTarget
16
- this.target = pathToTarget[pathToTarget.length - 1]
21
+ constructor (path: CID[]) {
22
+ this.path = path
17
23
  }
18
24
 
19
- isTarget (cid: CID): boolean {
20
- return this.target.equals(cid)
21
- }
25
+ async * traverse (root: CID, blockstore: Blockstore, getCodec: CodecLoader, options?: AbortOptions): AsyncGenerator<BlockView<unknown, number, number, 0 | 1>, void, undefined> {
26
+ if (!this.path.some(c => c.equals(root))) {
27
+ throw new InvalidTraversalError(`CIDPath traversal must include ${root}`)
28
+ }
29
+
30
+ let parentBlock: BlockView<unknown, number, number, 0 | 1> | undefined
31
+
32
+ for (const cid of this.path) {
33
+ if (parentBlock != null) {
34
+ let isChild = false
35
+
36
+ for (const [, child] of parentBlock.links()) {
37
+ if (child.equals(cid)) {
38
+ isChild = true
39
+ break
40
+ }
41
+ }
42
+
43
+ if (!isChild) {
44
+ throw new NotDescendantError(`${cid} is not a child of ${parentBlock.cid}`)
45
+ }
46
+ }
22
47
 
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]
48
+ const bytes = await toBuffer(blockstore.get(cid, options))
49
+ const block = createUnsafe({
50
+ cid,
51
+ bytes,
52
+ codec: await getCodec(cid.code)
53
+ })
26
54
 
27
- yield nextCid
55
+ parentBlock = block
56
+ yield block
57
+ }
28
58
  }
29
59
  }