@helia/unixfs 1.0.2 → 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,282 +9,200 @@ 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
- }
29
+ export const toPrefix = (position: number): string => {
30
+ return position
31
+ .toString(16)
32
+ .toUpperCase()
33
+ .padStart(2, '0')
34
+ .substring(0, 2)
35
+ }
40
36
 
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
- }
37
+ export interface CreateShardOptions {
38
+ mtime?: Mtime
39
+ mode?: number
40
+ cidVersion: Version
41
+ }
56
42
 
57
- const buf = dagPB.encode(dagPB.prepare(updatedPbNode))
58
- const cid = await persist(buf, blockstore, options)
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)
59
55
 
60
- return {
61
- node: updatedPbNode,
62
- cid,
63
- size: pbNode.Links.reduce((sum, link) => sum + (link.Tsize ?? 0), buf.byteLength)
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
60
+ })
64
61
  }
65
- }
66
62
 
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)
63
+ const res = await last(shard.flush(blockstore))
74
64
 
75
- await addLinksToHamtBucket(blockstore, links, bucket, rootBucket, options)
65
+ if (res == null) {
66
+ throw new Error('Flushing shard yielded no result')
67
+ }
76
68
 
77
- return bucket
69
+ return res
78
70
  }
79
71
 
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
- }
100
-
101
- await bucket.put(linkName.substring(2), {
102
- size: link.Tsize,
103
- cid: link.Hash
104
- })
105
- })
106
- )
107
-
108
- return bucket
72
+ export interface HAMTPath {
73
+ prefix: string
74
+ children: SparseArray
75
+ node: dagPB.PBNode
109
76
  }
110
77
 
111
- export const addLinksToHamtBucket = async (blockstore: Blockstore, links: PBLink[], bucket: Bucket<any>, rootBucket: Bucket<any>, options: AbortOptions): Promise<void> => {
112
- await Promise.all(
113
- links.map(async link => {
114
- const linkName = (link.Name ?? '')
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))
115
81
 
116
- if (linkName.length === 2) {
117
- log('Populating sub bucket', linkName)
118
- const pos = parseInt(linkName, 16)
119
- const block = await blockstore.get(link.Hash, options)
120
- const node = dagPB.decode(block)
82
+ // this is always the same
83
+ const fanout = BigInt(Math.pow(2, hamtBucketBits))
121
84
 
122
- const subBucket = new Bucket({
123
- hash: rootBucket._options.hash,
124
- bits: rootBucket._options.bits
125
- }, bucket, pos)
126
- bucket._putObjectAt(pos, subBucket)
127
-
128
- await addLinksToHamtBucket(blockstore, node.Links, subBucket, rootBucket, options)
129
- }
85
+ // start from the leaf and ascend to the root
86
+ path.reverse()
130
87
 
131
- await rootBucket.put(linkName.substring(2), {
132
- size: link.Tsize,
133
- cid: link.Hash
134
- })
135
- })
136
- )
137
- }
88
+ let cid: CID | undefined
89
+ let node: dagPB.PBNode | undefined
138
90
 
139
- export const toPrefix = (position: number): string => {
140
- return position
141
- .toString(16)
142
- .toUpperCase()
143
- .padStart(2, '0')
144
- .substring(0, 2)
145
- }
146
-
147
- export interface HamtPathSegment {
148
- bucket?: Bucket<any>
149
- prefix?: string
150
- node?: PBNode
151
- cid?: CID
152
- size?: number
153
- }
91
+ for (let i = 0; i < path.length; i++) {
92
+ const isRoot = i === path.length - 1
93
+ const segment = path[i]
154
94
 
155
- export const generatePath = async (root: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise<HamtPathSegment[]> => {
156
- // start at the root bucket and descend, loading nodes as we go
157
- const rootBucket = await recreateInitialHamtLevel(root.node.Links)
158
- const position = await rootBucket._findNewBucketAndPos(name)
159
- const path: HamtPathSegment[] = [{
160
- bucket: position.bucket,
161
- prefix: toPrefix(position.pos)
162
- }]
163
- let currentBucket = position.bucket
164
-
165
- while (currentBucket !== rootBucket) {
166
- path.push({
167
- bucket: currentBucket,
168
- prefix: toPrefix(currentBucket._posAtParent)
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
169
103
  })
170
104
 
171
- if (currentBucket._parent == null) {
172
- break
105
+ if (isRoot) {
106
+ dir.mtime = shardRoot.mtime
107
+ dir.mode = shardRoot.mode
173
108
  }
174
109
 
175
- currentBucket = currentBucket._parent
176
- }
110
+ node = {
111
+ Data: dir.marshal(),
112
+ Links: segment.node.Links
113
+ }
177
114
 
178
- // add the root bucket to the path
179
- path.push({
180
- bucket: rootBucket,
181
- node: root.node
182
- })
115
+ const block = dagPB.encode(dagPB.prepare(node))
183
116
 
184
- path.reverse()
117
+ cid = await persist(block, blockstore, options)
185
118
 
186
- // load PbNode for each path segment
187
- for (let i = 1; i < path.length; i++) {
188
- const segment = path[i]
189
- const previousSegment = path[i - 1]
119
+ if (!isRoot) {
120
+ // update link in parent sub-shard
121
+ const nextSegment = path[i + 1]
190
122
 
191
- if (previousSegment.node == null) {
192
- throw new Error('Could not generate HAMT path')
193
- }
123
+ if (nextSegment == null) {
124
+ throw new Error('Was not operating on shard root but also had no parent?')
125
+ }
194
126
 
195
- // find prefix in links
196
- const link = previousSegment.node.Links
197
- .filter(link => (link.Name ?? '').substring(0, 2) === segment.prefix)
198
- .pop()
127
+ log('updating link in parent sub-shard with prefix %s', nextSegment.prefix)
199
128
 
200
- // entry was not in shard
201
- if (link == null) {
202
- // reached bottom of tree, file will be added to the current bucket
203
- log(`Link ${segment.prefix}${name} will be added`)
204
- // return path
205
- continue
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)
134
+ })
206
135
  }
136
+ }
207
137
 
208
- const linkName = link.Name ?? ''
138
+ if (cid == null || node == null) {
139
+ throw new Error('Noting persisted')
140
+ }
209
141
 
210
- // found entry
211
- if (linkName === `${segment.prefix}${name}`) {
212
- log(`Link ${segment.prefix}${name} will be replaced`)
213
- // file already existed, file will be added to the current bucket
214
- // return path
215
- continue
216
- }
142
+ return { cid, node }
143
+ }
217
144
 
218
- // found subshard
219
- log(`Found subshard ${segment.prefix}`)
220
- const block = await blockstore.get(link.Hash)
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[] = []
149
+
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)
221
153
  const node = dagPB.decode(block)
154
+ const children = new SparseArray()
155
+ const index = await hash.take(hamtBucketBits)
156
+ const prefix = toPrefix(index)
222
157
 
223
- // subshard hasn't been loaded, descend to the next level of the HAMT
224
- if (path[i + 1] == null) {
225
- log(`Loaded new subshard ${segment.prefix}`)
158
+ path.push({
159
+ prefix,
160
+ children,
161
+ node
162
+ })
226
163
 
227
- if (segment.bucket == null || segment.prefix == null) {
228
- throw new Error('Shard was invalid')
229
- }
164
+ let childLink: dagPB.PBLink | undefined
230
165
 
231
- await recreateHamtLevel(blockstore, node.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options)
232
- const position = await rootBucket._findNewBucketAndPos(name)
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 ?? ''
233
170
 
234
- // i--
235
- path.push({
236
- bucket: position.bucket,
237
- prefix: toPrefix(position.pos),
238
- node
239
- })
171
+ if (linkName.length < 2) {
172
+ throw new Error('Invalid HAMT - link name was too short')
173
+ }
240
174
 
241
- continue
242
- }
175
+ const position = parseInt(linkName.substring(0, 2), 16)
176
+ children.set(position, true)
243
177
 
244
- if (segment.bucket == null) {
245
- throw new Error('Shard was invalid')
178
+ // we found the child we are looking for
179
+ if (linkName.startsWith(prefix)) {
180
+ childLink = link
181
+ }
246
182
  }
247
183
 
248
- // add intermediate links to bucket
249
- await addLinksToHamtBucket(blockstore, node.Links, segment.bucket, rootBucket, options)
250
-
251
- segment.node = node
252
- }
253
-
254
- await rootBucket.put(name, true)
255
-
256
- path.reverse()
257
-
258
- return path
259
- }
184
+ if (childLink == null) {
185
+ log('no link found with prefix %s for %s', prefix, fileName)
186
+ // hash.untake(hamtBucketBits)
187
+ break
188
+ }
260
189
 
261
- export interface CreateShardOptions {
262
- mtime?: Mtime
263
- mode?: number
264
- cidVersion: Version
265
- }
190
+ const linkName = childLink.Name ?? ''
266
191
 
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)
192
+ if (linkName.length < 2) {
193
+ throw new Error('Invalid HAMT - link name was too short')
194
+ }
279
195
 
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
284
- })
285
- }
196
+ if (linkName.length === 2) {
197
+ // found sub-shard
198
+ cid = childLink.Hash
199
+ log('descend into sub-shard with prefix %s', linkName)
286
200
 
287
- const res = await last(shard.flush(blockstore))
201
+ continue
202
+ }
288
203
 
289
- if (res == null) {
290
- throw new Error('Flushing shard yielded no result')
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,