@helia/unixfs 1.0.3 → 1.0.4

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.
@@ -1,8 +1,4 @@
1
1
  import * as dagPB from '@ipld/dag-pb'
2
- import {
3
- Bucket,
4
- createHAMT
5
- } from 'hamt-sharding'
6
2
  import { DirSharded } from './dir-sharded.js'
7
3
  import { logger } from '@libp2p/logger'
8
4
  import { UnixFS } from 'ipfs-unixfs'
@@ -13,131 +9,23 @@ import {
13
9
  hamtHashFn,
14
10
  hamtBucketBits
15
11
  } from './hamt-constants.js'
16
- import type { PBLink, PBNode } from '@ipld/dag-pb/interface'
17
12
  import type { Blockstore } from 'interface-blockstore'
18
13
  import type { Mtime } from 'ipfs-unixfs'
19
- import type { Directory } from './cid-to-directory.js'
20
14
  import type { AbortOptions } from '@libp2p/interfaces'
21
15
  import type { ImportResult } from 'ipfs-unixfs-importer'
22
16
  import { persist } from './persist.js'
17
+ import { InfiniteHash, wrapHash } from './consumable-hash.js'
18
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
19
+ // @ts-expect-error no types
20
+ import SparseArray from 'sparse-array'
21
+ import type { PersistOptions } from './persist.js'
23
22
 
24
23
  const log = logger('helia:unixfs:commands:utils:hamt-utils')
25
24
 
26
- export interface UpdateHamtResult {
27
- node: PBNode
28
- cid: CID
29
- size: number
30
- }
31
-
32
25
  export interface UpdateHamtDirectoryOptions extends AbortOptions {
33
26
  cidVersion: Version
34
27
  }
35
28
 
36
- export const updateHamtDirectory = async (pbNode: PBNode, blockstore: Blockstore, bucket: Bucket<any>, options: UpdateHamtDirectoryOptions): Promise<UpdateHamtResult> => {
37
- if (pbNode.Data == null) {
38
- throw new Error('Could not update HAMT directory because parent had no data')
39
- }
40
-
41
- // update parent with new bit field
42
- const node = UnixFS.unmarshal(pbNode.Data)
43
- const dir = new UnixFS({
44
- type: 'hamt-sharded-directory',
45
- data: Uint8Array.from(bucket._children.bitField().reverse()),
46
- fanout: BigInt(bucket.tableSize()),
47
- hashType: hamtHashCode,
48
- mode: node.mode,
49
- mtime: node.mtime
50
- })
51
-
52
- const updatedPbNode = {
53
- Data: dir.marshal(),
54
- Links: pbNode.Links
55
- }
56
-
57
- const buf = dagPB.encode(dagPB.prepare(updatedPbNode))
58
- const cid = await persist(buf, blockstore, options)
59
-
60
- return {
61
- node: updatedPbNode,
62
- cid,
63
- size: pbNode.Links.reduce((sum, link) => sum + (link.Tsize ?? 0), buf.byteLength)
64
- }
65
- }
66
-
67
- export const recreateHamtLevel = async (blockstore: Blockstore, links: PBLink[], rootBucket: Bucket<any>, parentBucket: Bucket<any>, positionAtParent: number, options: AbortOptions): Promise<Bucket<any>> => {
68
- // recreate this level of the HAMT
69
- const bucket = new Bucket({
70
- hash: rootBucket._options.hash,
71
- bits: rootBucket._options.bits
72
- }, parentBucket, positionAtParent)
73
- parentBucket._putObjectAt(positionAtParent, bucket)
74
-
75
- await addLinksToHamtBucket(blockstore, links, bucket, rootBucket, options)
76
-
77
- return bucket
78
- }
79
-
80
- export const recreateInitialHamtLevel = async (links: PBLink[]): Promise<Bucket<any>> => {
81
- const bucket = createHAMT<any>({
82
- hashFn: hamtHashFn,
83
- bits: hamtBucketBits
84
- })
85
-
86
- // populate sub bucket but do not recurse as we do not want to load the whole shard
87
- await Promise.all(
88
- links.map(async link => {
89
- const linkName = (link.Name ?? '')
90
-
91
- if (linkName.length === 2) {
92
- const pos = parseInt(linkName, 16)
93
- const subBucket = new Bucket({
94
- hash: bucket._options.hash,
95
- bits: bucket._options.bits
96
- }, bucket, pos)
97
-
98
- bucket._putObjectAt(pos, subBucket)
99
- return
100
- }
101
-
102
- await bucket.put(linkName.substring(2), {
103
- size: link.Tsize,
104
- cid: link.Hash
105
- })
106
- })
107
- )
108
-
109
- return bucket
110
- }
111
-
112
- export const addLinksToHamtBucket = async (blockstore: Blockstore, links: PBLink[], bucket: Bucket<any>, rootBucket: Bucket<any>, options: AbortOptions): Promise<void> => {
113
- await Promise.all(
114
- links.map(async link => {
115
- const linkName = (link.Name ?? '')
116
-
117
- if (linkName.length === 2) {
118
- log('Populating sub bucket', linkName)
119
- const pos = parseInt(linkName, 16)
120
- const block = await blockstore.get(link.Hash, options)
121
- const node = dagPB.decode(block)
122
-
123
- const subBucket = new Bucket({
124
- hash: rootBucket._options.hash,
125
- bits: rootBucket._options.bits
126
- }, bucket, pos)
127
- bucket._putObjectAt(pos, subBucket)
128
-
129
- await addLinksToHamtBucket(blockstore, node.Links, subBucket, rootBucket, options)
130
- return
131
- }
132
-
133
- await rootBucket.put(linkName.substring(2), {
134
- size: link.Tsize,
135
- cid: link.Hash
136
- })
137
- })
138
- )
139
- }
140
-
141
29
  export const toPrefix = (position: number): string => {
142
30
  return position
143
31
  .toString(16)
@@ -146,149 +34,175 @@ export const toPrefix = (position: number): string => {
146
34
  .substring(0, 2)
147
35
  }
148
36
 
149
- export interface HamtPathSegment {
150
- bucket?: Bucket<any>
151
- prefix?: string
152
- node?: PBNode
153
- cid?: CID
154
- size?: number
37
+ export interface CreateShardOptions {
38
+ mtime?: Mtime
39
+ mode?: number
40
+ cidVersion: Version
155
41
  }
156
42
 
157
- export const generatePath = async (root: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise<HamtPathSegment[]> => {
158
- // start at the root bucket and descend, loading nodes as we go
159
- const rootBucket = await recreateInitialHamtLevel(root.node.Links)
160
- const position = await rootBucket._findNewBucketAndPos(name)
161
- const path: HamtPathSegment[] = [{
162
- bucket: position.bucket,
163
- prefix: toPrefix(position.pos)
164
- }]
165
- let currentBucket = position.bucket
166
-
167
- while (currentBucket !== rootBucket) {
168
- path.push({
169
- bucket: currentBucket,
170
- prefix: toPrefix(currentBucket._posAtParent)
43
+ export const createShard = async (blockstore: Blockstore, contents: Array<{ name: string, size: bigint, cid: CID }>, options: CreateShardOptions): Promise<ImportResult> => {
44
+ const shard = new DirSharded({
45
+ root: true,
46
+ dir: true,
47
+ parent: undefined,
48
+ parentKey: undefined,
49
+ path: '',
50
+ dirty: true,
51
+ flat: false,
52
+ mtime: options.mtime,
53
+ mode: options.mode
54
+ }, options)
55
+
56
+ for (let i = 0; i < contents.length; i++) {
57
+ await shard._bucket.put(contents[i].name, {
58
+ size: contents[i].size,
59
+ cid: contents[i].cid
171
60
  })
61
+ }
172
62
 
173
- if (currentBucket._parent == null) {
174
- break
175
- }
63
+ const res = await last(shard.flush(blockstore))
176
64
 
177
- currentBucket = currentBucket._parent
65
+ if (res == null) {
66
+ throw new Error('Flushing shard yielded no result')
178
67
  }
179
68
 
180
- // add the root bucket to the path
181
- path.push({
182
- bucket: rootBucket,
183
- node: root.node
184
- })
69
+ return res
70
+ }
71
+
72
+ export interface HAMTPath {
73
+ prefix: string
74
+ children: SparseArray
75
+ node: dagPB.PBNode
76
+ }
77
+
78
+ export const updateShardedDirectory = async (path: HAMTPath[], blockstore: Blockstore, options: PersistOptions): Promise<{ cid: CID, node: dagPB.PBNode }> => {
79
+ // persist any metadata on the shard root
80
+ const shardRoot = UnixFS.unmarshal(path[0].node.Data ?? new Uint8Array(0))
185
81
 
82
+ // this is always the same
83
+ const fanout = BigInt(Math.pow(2, hamtBucketBits))
84
+
85
+ // start from the leaf and ascend to the root
186
86
  path.reverse()
187
87
 
188
- // load PbNode for each path segment
189
- for (let i = 1; i < path.length; i++) {
190
- const segment = path[i]
191
- const previousSegment = path[i - 1]
88
+ let cid: CID | undefined
89
+ let node: dagPB.PBNode | undefined
192
90
 
193
- if (previousSegment.node == null) {
194
- throw new Error('Could not generate HAMT path')
195
- }
91
+ for (let i = 0; i < path.length; i++) {
92
+ const isRoot = i === path.length - 1
93
+ const segment = path[i]
196
94
 
197
- // find prefix in links
198
- const link = previousSegment.node.Links
199
- .filter(link => (link.Name ?? '').substring(0, 2) === segment.prefix)
200
- .pop()
95
+ // go-ipfs uses little endian, that's why we have to
96
+ // reverse the bit field before storing it
97
+ const data = Uint8Array.from(segment.children.bitField().reverse())
98
+ const dir = new UnixFS({
99
+ type: 'hamt-sharded-directory',
100
+ data,
101
+ fanout,
102
+ hashType: hamtHashCode
103
+ })
201
104
 
202
- // entry was not in shard
203
- if (link == null) {
204
- // reached bottom of tree, file will be added to the current bucket
205
- log(`Link ${segment.prefix}${name} will be added`)
206
- // return path
207
- continue
105
+ if (isRoot) {
106
+ dir.mtime = shardRoot.mtime
107
+ dir.mode = shardRoot.mode
208
108
  }
209
109
 
210
- const linkName = link.Name ?? ''
211
-
212
- // found entry
213
- if (linkName === `${segment.prefix}${name}`) {
214
- log(`Link ${segment.prefix}${name} will be replaced`)
215
- // file already existed, file will be added to the current bucket
216
- // return path
217
- continue
110
+ node = {
111
+ Data: dir.marshal(),
112
+ Links: segment.node.Links
218
113
  }
219
114
 
220
- // found subshard
221
- log(`Found subshard ${segment.prefix}`)
222
- const block = await blockstore.get(link.Hash)
223
- const node = segment.node = dagPB.decode(block)
115
+ const block = dagPB.encode(dagPB.prepare(node))
224
116
 
225
- // subshard hasn't been loaded, descend to the next level of the HAMT
226
- if (path[i + 1] == null) {
227
- log(`Loaded new subshard ${segment.prefix}`)
117
+ cid = await persist(block, blockstore, options)
228
118
 
229
- if (segment.bucket == null || segment.prefix == null) {
230
- throw new Error('Shard was invalid')
119
+ if (!isRoot) {
120
+ // update link in parent sub-shard
121
+ const nextSegment = path[i + 1]
122
+
123
+ if (nextSegment == null) {
124
+ throw new Error('Was not operating on shard root but also had no parent?')
231
125
  }
232
126
 
233
- await recreateHamtLevel(blockstore, node.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options)
234
- const position = await rootBucket._findNewBucketAndPos(name)
127
+ log('updating link in parent sub-shard with prefix %s', nextSegment.prefix)
235
128
 
236
- // i--
237
- path.push({
238
- bucket: position.bucket,
239
- prefix: toPrefix(position.pos),
240
- node
129
+ nextSegment.node.Links = nextSegment.node.Links.filter(l => l.Name !== nextSegment.prefix)
130
+ nextSegment.node.Links.push({
131
+ Name: nextSegment.prefix,
132
+ Hash: cid,
133
+ Tsize: segment.node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), block.byteLength)
241
134
  })
242
-
243
- continue
244
- }
245
-
246
- if (segment.bucket == null) {
247
- throw new Error('Shard was invalid')
248
135
  }
249
-
250
- // add intermediate links to bucket
251
- await addLinksToHamtBucket(blockstore, node.Links, segment.bucket, rootBucket, options)
252
136
  }
253
137
 
254
- await rootBucket.put(name, true)
255
-
256
- path.reverse()
138
+ if (cid == null || node == null) {
139
+ throw new Error('Noting persisted')
140
+ }
257
141
 
258
- return path
142
+ return { cid, node }
259
143
  }
260
144
 
261
- export interface CreateShardOptions {
262
- mtime?: Mtime
263
- mode?: number
264
- cidVersion: Version
265
- }
145
+ export const recreateShardedDirectory = async (cid: CID, fileName: string, blockstore: Blockstore, options: AbortOptions): Promise<{ path: HAMTPath[], hash: InfiniteHash }> => {
146
+ const wrapped = wrapHash(hamtHashFn)
147
+ const hash = wrapped(uint8ArrayFromString(fileName))
148
+ const path: HAMTPath[] = []
266
149
 
267
- export const createShard = async (blockstore: Blockstore, contents: Array<{ name: string, size: bigint, cid: CID }>, options: CreateShardOptions): Promise<ImportResult> => {
268
- const shard = new DirSharded({
269
- root: true,
270
- dir: true,
271
- parent: undefined,
272
- parentKey: undefined,
273
- path: '',
274
- dirty: true,
275
- flat: false,
276
- mtime: options.mtime,
277
- mode: options.mode
278
- }, options)
150
+ // descend the HAMT, loading each layer as we head towards the target child
151
+ while (true) {
152
+ const block = await blockstore.get(cid, options)
153
+ const node = dagPB.decode(block)
154
+ const children = new SparseArray()
155
+ const index = await hash.take(hamtBucketBits)
156
+ const prefix = toPrefix(index)
279
157
 
280
- for (let i = 0; i < contents.length; i++) {
281
- await shard._bucket.put(contents[i].name, {
282
- size: contents[i].size,
283
- cid: contents[i].cid
158
+ path.push({
159
+ prefix,
160
+ children,
161
+ node
284
162
  })
285
- }
286
163
 
287
- const res = await last(shard.flush(blockstore))
164
+ let childLink: dagPB.PBLink | undefined
288
165
 
289
- if (res == null) {
290
- throw new Error('Flushing shard yielded no result')
166
+ // update sparsearray child layout - the bitfield is used as the data field for the
167
+ // intermediate DAG node so this is required to generate consistent hashes
168
+ for (const link of node.Links) {
169
+ const linkName = link.Name ?? ''
170
+
171
+ if (linkName.length < 2) {
172
+ throw new Error('Invalid HAMT - link name was too short')
173
+ }
174
+
175
+ const position = parseInt(linkName.substring(0, 2), 16)
176
+ children.set(position, true)
177
+
178
+ // we found the child we are looking for
179
+ if (linkName.startsWith(prefix)) {
180
+ childLink = link
181
+ }
182
+ }
183
+
184
+ if (childLink == null) {
185
+ log('no link found with prefix %s for %s', prefix, fileName)
186
+ // hash.untake(hamtBucketBits)
187
+ break
188
+ }
189
+
190
+ const linkName = childLink.Name ?? ''
191
+
192
+ if (linkName.length < 2) {
193
+ throw new Error('Invalid HAMT - link name was too short')
194
+ }
195
+
196
+ if (linkName.length === 2) {
197
+ // found sub-shard
198
+ cid = childLink.Hash
199
+ log('descend into sub-shard with prefix %s', linkName)
200
+
201
+ continue
202
+ }
203
+
204
+ break
291
205
  }
292
206
 
293
- return res
207
+ return { path, hash }
294
208
  }
@@ -4,10 +4,9 @@ import type { CID, Version } from 'multiformats/cid'
4
4
  import { logger } from '@libp2p/logger'
5
5
  import { UnixFS } from 'ipfs-unixfs'
6
6
  import {
7
- generatePath,
8
- HamtPathSegment,
9
- updateHamtDirectory,
10
- UpdateHamtDirectoryOptions
7
+ recreateShardedDirectory,
8
+ UpdateHamtDirectoryOptions,
9
+ updateShardedDirectory
11
10
  } from './hamt-utils.js'
12
11
  import type { PBNode } from '@ipld/dag-pb'
13
12
  import type { Blockstore } from 'interface-blockstore'
@@ -77,131 +76,60 @@ const removeFromDirectory = async (parent: Directory, name: string, blockstore:
77
76
  }
78
77
 
79
78
  const removeFromShardedDirectory = async (parent: Directory, name: string, blockstore: Blockstore, options: UpdateHamtDirectoryOptions): Promise<{ cid: CID, node: PBNode }> => {
80
- const path = await generatePath(parent, name, blockstore, options)
79
+ const { path } = await recreateShardedDirectory(parent.cid, name, blockstore, options)
80
+ const finalSegment = path[path.length - 1]
81
81
 
82
- // remove file from root bucket
83
- const rootBucket = path[path.length - 1].bucket
84
-
85
- if (rootBucket == null) {
86
- throw new Error('Could not generate HAMT path')
82
+ if (finalSegment == null) {
83
+ throw new Error('Invalid HAMT, could not generate path')
87
84
  }
88
85
 
89
- await rootBucket.del(name)
90
-
91
- // update all nodes in the shard path
92
- return await updateShard(path, name, blockstore, options)
93
- }
94
-
95
- /**
96
- * The `path` param is a list of HAMT path segments starting with th
97
- */
98
- const updateShard = async (path: HamtPathSegment[], name: string, blockstore: Blockstore, options: UpdateHamtDirectoryOptions): Promise<{ node: PBNode, cid: CID }> => {
99
- const fileName = `${path[0].prefix}${name}`
100
-
101
- // skip first path segment as it is the file to remove
102
- for (let i = 1; i < path.length; i++) {
103
- const lastPrefix = path[i - 1].prefix
104
- const segment = path[i]
105
-
106
- if (segment.node == null) {
107
- throw new InvalidParametersError('Path segment had no associated PBNode')
108
- }
109
-
110
- const link = segment.node.Links
111
- .find(link => (link.Name ?? '').substring(0, 2) === lastPrefix)
86
+ const linkName = finalSegment.node.Links.filter(l => (l.Name ?? '').substring(2) === name).map(l => l.Name).pop()
112
87
 
113
- if (link == null) {
114
- throw new InvalidParametersError(`No link found with prefix ${lastPrefix} for file ${name}`)
115
- }
88
+ if (linkName == null) {
89
+ throw new Error('File not found')
90
+ }
116
91
 
117
- if (link.Name == null) {
118
- throw new InvalidParametersError(`${lastPrefix} link had no name`)
119
- }
92
+ const prefix = linkName.substring(0, 2)
93
+ const index = parseInt(prefix, 16)
120
94
 
121
- if (link.Name === fileName) {
122
- log(`removing existing link ${link.Name}`)
95
+ // remove the file from the shard
96
+ finalSegment.node.Links = finalSegment.node.Links.filter(link => link.Name !== linkName)
97
+ finalSegment.children.unset(index)
123
98
 
124
- const links = segment.node.Links.filter((nodeLink) => {
125
- return nodeLink.Name !== link.Name
126
- })
127
-
128
- if (segment.bucket == null) {
129
- throw new Error('Segment bucket was missing')
99
+ if (finalSegment.node.Links.length === 1) {
100
+ // replace the subshard with the last remaining file in the parent
101
+ while (true) {
102
+ if (path.length === 1) {
103
+ break
130
104
  }
131
105
 
132
- await segment.bucket.del(name)
133
-
134
- const result = await updateHamtDirectory({
135
- Data: segment.node.Data,
136
- Links: links
137
- }, blockstore, segment.bucket, options)
106
+ const segment = path[path.length - 1]
138
107
 
139
- segment.node = result.node
140
- segment.cid = result.cid
141
- segment.size = result.size
142
- }
143
-
144
- if (link.Name === lastPrefix) {
145
- log(`updating subshard with prefix ${lastPrefix}`)
146
-
147
- const lastSegment = path[i - 1]
148
-
149
- if (lastSegment.node?.Links.length === 1) {
150
- log(`removing subshard for ${lastPrefix}`)
151
-
152
- // convert subshard back to normal file entry
153
- const link = lastSegment.node.Links[0]
154
- link.Name = `${lastPrefix}${(link.Name ?? '').substring(2)}`
155
-
156
- // remove existing prefix
157
- segment.node.Links = segment.node.Links.filter((link) => {
158
- return link.Name !== lastPrefix
159
- })
160
-
161
- // add new child
162
- segment.node.Links.push(link)
163
- } else {
164
- // replace subshard entry
165
- log(`replacing subshard for ${lastPrefix}`)
108
+ if (segment == null || segment.node.Links.length > 1) {
109
+ break
110
+ }
166
111
 
167
- // remove existing prefix
168
- segment.node.Links = segment.node.Links.filter((link) => {
169
- return link.Name !== lastPrefix
170
- })
112
+ // remove final segment
113
+ path.pop()
171
114
 
172
- if (lastSegment.cid == null) {
173
- throw new Error('Did not persist previous segment')
174
- }
115
+ const nextSegment = path[path.length - 1]
175
116
 
176
- // add new child
177
- segment.node.Links.push({
178
- Name: lastPrefix,
179
- Hash: lastSegment.cid,
180
- Tsize: lastSegment.size
181
- })
117
+ if (nextSegment == null) {
118
+ break
182
119
  }
183
120
 
184
- if (segment.bucket == null) {
185
- throw new Error('Segment bucket was missing')
186
- }
121
+ const link = segment.node.Links[0]
187
122
 
188
- const result = await updateHamtDirectory(segment.node, blockstore, segment.bucket, options)
189
- segment.node = result.node
190
- segment.cid = result.cid
191
- segment.size = result.size
123
+ nextSegment.node.Links = nextSegment.node.Links.filter(l => !(l.Name ?? '').startsWith(nextSegment.prefix))
124
+ nextSegment.node.Links.push({
125
+ Hash: link.Hash,
126
+ Name: `${nextSegment.prefix}${(link.Name ?? '').substring(2)}`,
127
+ Tsize: link.Tsize
128
+ })
192
129
  }
193
130
  }
194
131
 
195
- const rootSegment = path[path.length - 1]
196
-
197
- if (rootSegment == null || rootSegment.cid == null || rootSegment.node == null) {
198
- throw new InvalidParametersError('Failed to update shard')
199
- }
200
-
201
- return {
202
- cid: rootSegment.cid,
203
- node: rootSegment.node
204
- }
132
+ return await updateShardedDirectory(path, blockstore, options)
205
133
  }
206
134
 
207
135
  const convertToFlatDirectory = async (parent: Directory, blockstore: Blockstore, options: RmLinkOptions): Promise<RemoveLinkResult> => {
@@ -8,7 +8,7 @@ import { cidToDirectory } from './cid-to-directory.js'
8
8
  import { cidToPBLink } from './cid-to-pblink.js'
9
9
  import type { Blockstore } from 'interface-blockstore'
10
10
 
11
- const log = logger('helia:unixfs:components:utils:add-link')
11
+ const log = logger('helia:unixfs:components:utils:resolve')
12
12
 
13
13
  export interface Segment {
14
14
  name: string
@@ -33,12 +33,12 @@ export interface ResolveResult {
33
33
  }
34
34
 
35
35
  export async function resolve (cid: CID, path: string | undefined, blockstore: Blockstore, options: AbortOptions): Promise<ResolveResult> {
36
- log('resolve "%s" under %c', path, cid)
37
-
38
36
  if (path == null || path === '') {
39
37
  return { cid }
40
38
  }
41
39
 
40
+ log('resolve "%s" under %c', path, cid)
41
+
42
42
  const parts = path.split('/').filter(Boolean)
43
43
  const segments: Segment[] = [{
44
44
  name: '',
@@ -50,6 +50,8 @@ export async function resolve (cid: CID, path: string | undefined, blockstore: B
50
50
  const part = parts[i]
51
51
  const result = await exporter(cid, blockstore, options)
52
52
 
53
+ log('resolving "%s"', part, result)
54
+
53
55
  if (result.type === 'file') {
54
56
  if (i < parts.length - 1) {
55
57
  throw new InvalidParametersError('Path was invalid')
@@ -62,6 +64,7 @@ export async function resolve (cid: CID, path: string | undefined, blockstore: B
62
64
  for await (const entry of result.content()) {
63
65
  if (entry.name === part) {
64
66
  dirCid = entry.cid
67
+ break
65
68
  }
66
69
  }
67
70
 
@@ -81,6 +84,8 @@ export async function resolve (cid: CID, path: string | undefined, blockstore: B
81
84
  }
82
85
  }
83
86
 
87
+ log('resolved %s to %c', path, cid)
88
+
84
89
  return {
85
90
  cid,
86
91
  path,