@helia/unixfs 0.0.0 → 1.0.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 (110) hide show
  1. package/README.md +10 -4
  2. package/dist/index.min.js +1 -1
  3. package/dist/src/commands/cat.d.ts +1 -1
  4. package/dist/src/commands/cat.d.ts.map +1 -1
  5. package/dist/src/commands/cat.js +1 -1
  6. package/dist/src/commands/cat.js.map +1 -1
  7. package/dist/src/commands/chmod.d.ts.map +1 -1
  8. package/dist/src/commands/chmod.js +12 -7
  9. package/dist/src/commands/chmod.js.map +1 -1
  10. package/dist/src/commands/cp.d.ts +1 -1
  11. package/dist/src/commands/cp.d.ts.map +1 -1
  12. package/dist/src/commands/cp.js +5 -2
  13. package/dist/src/commands/cp.js.map +1 -1
  14. package/dist/src/commands/ls.d.ts +2 -1
  15. package/dist/src/commands/ls.d.ts.map +1 -1
  16. package/dist/src/commands/ls.js +1 -1
  17. package/dist/src/commands/ls.js.map +1 -1
  18. package/dist/src/commands/mkdir.d.ts.map +1 -1
  19. package/dist/src/commands/mkdir.js +4 -2
  20. package/dist/src/commands/mkdir.js.map +1 -1
  21. package/dist/src/commands/rm.d.ts +1 -1
  22. package/dist/src/commands/rm.d.ts.map +1 -1
  23. package/dist/src/commands/rm.js +9 -3
  24. package/dist/src/commands/rm.js.map +1 -1
  25. package/dist/src/commands/stat.d.ts +1 -1
  26. package/dist/src/commands/stat.d.ts.map +1 -1
  27. package/dist/src/commands/stat.js +19 -15
  28. package/dist/src/commands/stat.js.map +1 -1
  29. package/dist/src/commands/touch.d.ts.map +1 -1
  30. package/dist/src/commands/touch.js +13 -8
  31. package/dist/src/commands/touch.js.map +1 -1
  32. package/dist/src/commands/utils/add-link.d.ts +4 -2
  33. package/dist/src/commands/utils/add-link.d.ts.map +1 -1
  34. package/dist/src/commands/utils/add-link.js +24 -24
  35. package/dist/src/commands/utils/add-link.js.map +1 -1
  36. package/dist/src/commands/utils/cid-to-directory.d.ts +3 -3
  37. package/dist/src/commands/utils/cid-to-directory.d.ts.map +1 -1
  38. package/dist/src/commands/utils/cid-to-directory.js +1 -1
  39. package/dist/src/commands/utils/cid-to-directory.js.map +1 -1
  40. package/dist/src/commands/utils/cid-to-pblink.d.ts +3 -3
  41. package/dist/src/commands/utils/cid-to-pblink.d.ts.map +1 -1
  42. package/dist/src/commands/utils/cid-to-pblink.js.map +1 -1
  43. package/dist/src/commands/utils/constants.d.ts +2 -0
  44. package/dist/src/commands/utils/constants.d.ts.map +1 -0
  45. package/dist/src/commands/utils/constants.js +2 -0
  46. package/dist/src/commands/utils/constants.js.map +1 -0
  47. package/dist/src/commands/utils/dir-sharded.d.ts +47 -41
  48. package/dist/src/commands/utils/dir-sharded.d.ts.map +1 -1
  49. package/dist/src/commands/utils/dir-sharded.js +99 -15
  50. package/dist/src/commands/utils/dir-sharded.js.map +1 -1
  51. package/dist/src/commands/utils/errors.d.ts +22 -6
  52. package/dist/src/commands/utils/errors.d.ts.map +1 -1
  53. package/dist/src/commands/utils/errors.js +34 -8
  54. package/dist/src/commands/utils/errors.js.map +1 -1
  55. package/dist/src/commands/utils/hamt-constants.d.ts +1 -1
  56. package/dist/src/commands/utils/hamt-constants.d.ts.map +1 -1
  57. package/dist/src/commands/utils/hamt-constants.js +1 -1
  58. package/dist/src/commands/utils/hamt-constants.js.map +1 -1
  59. package/dist/src/commands/utils/hamt-utils.d.ts +15 -12
  60. package/dist/src/commands/utils/hamt-utils.d.ts.map +1 -1
  61. package/dist/src/commands/utils/hamt-utils.js +40 -39
  62. package/dist/src/commands/utils/hamt-utils.js.map +1 -1
  63. package/dist/src/commands/utils/is-over-shard-threshold.d.ts +10 -0
  64. package/dist/src/commands/utils/is-over-shard-threshold.d.ts.map +1 -0
  65. package/dist/src/commands/utils/is-over-shard-threshold.js +62 -0
  66. package/dist/src/commands/utils/is-over-shard-threshold.js.map +1 -0
  67. package/dist/src/commands/utils/persist.d.ts +7 -6
  68. package/dist/src/commands/utils/persist.d.ts.map +1 -1
  69. package/dist/src/commands/utils/persist.js +6 -3
  70. package/dist/src/commands/utils/persist.js.map +1 -1
  71. package/dist/src/commands/utils/remove-link.d.ts +6 -2
  72. package/dist/src/commands/utils/remove-link.d.ts.map +1 -1
  73. package/dist/src/commands/utils/remove-link.js +143 -55
  74. package/dist/src/commands/utils/remove-link.js.map +1 -1
  75. package/dist/src/commands/utils/resolve.d.ts +6 -3
  76. package/dist/src/commands/utils/resolve.d.ts.map +1 -1
  77. package/dist/src/commands/utils/resolve.js +4 -4
  78. package/dist/src/commands/utils/resolve.js.map +1 -1
  79. package/dist/src/index.d.ts +14 -7
  80. package/dist/src/index.d.ts.map +1 -1
  81. package/dist/src/index.js +0 -7
  82. package/dist/src/index.js.map +1 -1
  83. package/dist/typedoc-urls.json +14 -0
  84. package/package.json +8 -7
  85. package/src/commands/cat.ts +3 -2
  86. package/src/commands/chmod.ts +12 -7
  87. package/src/commands/cp.ts +7 -4
  88. package/src/commands/ls.ts +4 -3
  89. package/src/commands/mkdir.ts +5 -3
  90. package/src/commands/rm.ts +9 -5
  91. package/src/commands/stat.ts +22 -17
  92. package/src/commands/touch.ts +14 -9
  93. package/src/commands/utils/add-link.ts +32 -31
  94. package/src/commands/utils/cid-to-directory.ts +4 -4
  95. package/src/commands/utils/cid-to-pblink.ts +3 -3
  96. package/src/commands/utils/constants.ts +2 -0
  97. package/src/commands/utils/dir-sharded.ts +162 -63
  98. package/src/commands/utils/errors.ts +42 -8
  99. package/src/commands/utils/hamt-constants.ts +1 -1
  100. package/src/commands/utils/hamt-utils.ts +59 -50
  101. package/src/commands/utils/is-over-shard-threshold.ts +78 -0
  102. package/src/commands/utils/persist.ts +13 -8
  103. package/src/commands/utils/remove-link.ts +178 -77
  104. package/src/commands/utils/resolve.ts +12 -7
  105. package/src/index.ts +15 -17
  106. package/dist/src/commands/add.d.ts +0 -6
  107. package/dist/src/commands/add.d.ts.map +0 -1
  108. package/dist/src/commands/add.js +0 -38
  109. package/dist/src/commands/add.js.map +0 -1
  110. package/src/commands/add.ts +0 -46
@@ -1,39 +1,28 @@
1
- import { encode, prepare } from '@ipld/dag-pb'
1
+ import { encode, PBLink, prepare } from '@ipld/dag-pb'
2
2
  import { UnixFS } from 'ipfs-unixfs'
3
- import { persist } from './persist.js'
3
+ import { persist, PersistOptions } from './persist.js'
4
4
  import { createHAMT, Bucket, BucketChild } from 'hamt-sharding'
5
5
  import {
6
6
  hamtHashCode,
7
- hamtHashFn,
8
- hamtBucketBits
7
+ hamtHashFn
9
8
  } from './hamt-constants.js'
10
- import type { CID, Version } from 'multiformats/cid'
11
- import type { PBNode } from '@ipld/dag-pb/interface'
9
+ import { CID } from 'multiformats/cid'
12
10
  import type { Mtime } from 'ipfs-unixfs'
13
- import type { BlockCodec } from 'multiformats/codecs/interface'
14
- import type { Blockstore } from 'ipfs-unixfs-importer'
11
+ import type { Blockstore } from 'interface-blockstore'
15
12
 
16
- export interface ImportResult {
17
- cid: CID
18
- node: PBNode
19
- size: number
20
- }
21
-
22
- export interface DirContents {
23
- cid?: CID
24
- size?: number
13
+ interface InProgressImportResult extends ImportResult {
14
+ single?: boolean
15
+ originalPath?: string
25
16
  }
26
17
 
27
- export interface DirOptions {
28
- mtime?: Mtime
29
- mode?: number
30
- codec?: BlockCodec<any, any>
31
- cidVersion?: Version
32
- onlyHash?: boolean
33
- signal?: AbortSignal
18
+ interface ImportResult {
19
+ cid: CID
20
+ size: bigint
21
+ path?: string
22
+ unixfs?: UnixFS
34
23
  }
35
24
 
36
- export interface DirProps {
25
+ interface DirProps {
37
26
  root: boolean
38
27
  dir: boolean
39
28
  path: string
@@ -46,23 +35,25 @@ export interface DirProps {
46
35
  mtime?: Mtime
47
36
  }
48
37
 
49
- export abstract class Dir {
50
- protected options: DirOptions
51
- protected root: boolean
52
- protected dir: boolean
53
- protected path: string
54
- protected dirty: boolean
55
- protected flat: boolean
56
- protected parent?: Dir
57
- protected parentKey?: string
58
- protected unixfs?: UnixFS
59
- protected mode?: number
38
+ abstract class Dir {
39
+ public options: PersistOptions
40
+ public root: boolean
41
+ public dir: boolean
42
+ public path: string
43
+ public dirty: boolean
44
+ public flat: boolean
45
+ public parent?: Dir
46
+ public parentKey?: string
47
+ public unixfs?: UnixFS
48
+ public mode?: number
60
49
  public mtime?: Mtime
61
- protected cid?: CID
62
- protected size?: number
50
+ public cid?: CID
51
+ public size?: number
52
+ public nodeSize?: number
63
53
 
64
- constructor (props: DirProps, options: DirOptions) {
54
+ constructor (props: DirProps, options: PersistOptions) {
65
55
  this.options = options ?? {}
56
+
66
57
  this.root = props.root
67
58
  this.dir = props.dir
68
59
  this.path = props.path
@@ -74,26 +65,36 @@ export abstract class Dir {
74
65
  this.mode = props.mode
75
66
  this.mtime = props.mtime
76
67
  }
68
+
69
+ abstract put (name: string, value: InProgressImportResult | Dir): Promise<void>
70
+ abstract get (name: string): Promise<InProgressImportResult | Dir | undefined>
71
+ abstract eachChildSeries (): AsyncIterable<{ key: string, child: InProgressImportResult | Dir }>
72
+ abstract flush (blockstore: Blockstore): AsyncGenerator<ImportResult>
73
+ abstract estimateNodeSize (): number
74
+ abstract childCount (): number
77
75
  }
78
76
 
79
77
  export class DirSharded extends Dir {
80
- public _bucket: Bucket<DirContents>
78
+ public _bucket: Bucket<InProgressImportResult | Dir>
81
79
 
82
- constructor (props: DirProps, options: DirOptions) {
80
+ constructor (props: DirProps, options: PersistOptions) {
83
81
  super(props, options)
84
82
 
85
- /** @type {Bucket<DirContents>} */
86
83
  this._bucket = createHAMT({
87
84
  hashFn: hamtHashFn,
88
- bits: hamtBucketBits
85
+ bits: 8
89
86
  })
90
87
  }
91
88
 
92
- async put (name: string, value: DirContents): Promise<void> {
89
+ async put (name: string, value: InProgressImportResult | Dir): Promise<void> {
90
+ this.cid = undefined
91
+ this.size = undefined
92
+ this.nodeSize = undefined
93
+
93
94
  await this._bucket.put(name, value)
94
95
  }
95
96
 
96
- async get (name: string): Promise<DirContents | undefined> {
97
+ async get (name: string): Promise<InProgressImportResult | Dir | undefined> {
97
98
  return await this._bucket.get(name)
98
99
  }
99
100
 
@@ -105,11 +106,11 @@ export class DirSharded extends Dir {
105
106
  return this._bucket.childrenCount()
106
107
  }
107
108
 
108
- onlyChild (): Bucket<DirContents> | BucketChild<DirContents> {
109
+ onlyChild (): Bucket<InProgressImportResult | Dir> | BucketChild<InProgressImportResult | Dir> {
109
110
  return this._bucket.onlyChild()
110
111
  }
111
112
 
112
- async * eachChildSeries (): AsyncGenerator<{ key: string, child: DirContents }> {
113
+ async * eachChildSeries (): AsyncGenerator<{ key: string, child: InProgressImportResult | Dir }> {
113
114
  for await (const { key, value } of this._bucket.eachLeafSeries()) {
114
115
  yield {
115
116
  key,
@@ -118,15 +119,30 @@ export class DirSharded extends Dir {
118
119
  }
119
120
  }
120
121
 
121
- async * flush (blockstore: Blockstore): AsyncIterable<ImportResult> {
122
- yield * flush(this._bucket, blockstore, this, this.options)
122
+ estimateNodeSize (): number {
123
+ if (this.nodeSize !== undefined) {
124
+ return this.nodeSize
125
+ }
126
+
127
+ this.nodeSize = calculateSize(this._bucket, this, this.options)
128
+
129
+ return this.nodeSize
130
+ }
131
+
132
+ async * flush (blockstore: Blockstore): AsyncGenerator<ImportResult> {
133
+ for await (const entry of flush(this._bucket, blockstore, this, this.options)) {
134
+ yield {
135
+ ...entry,
136
+ path: this.path
137
+ }
138
+ }
123
139
  }
124
140
  }
125
141
 
126
- async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot: any, options: DirOptions): AsyncIterable<ImportResult> {
142
+ async function * flush (bucket: Bucket<Dir | InProgressImportResult>, blockstore: Blockstore, shardRoot: DirSharded | null, options: PersistOptions): AsyncIterable<ImportResult> {
127
143
  const children = bucket._children
128
- const links = []
129
- let childrenSize = 0
144
+ const links: PBLink[] = []
145
+ let childrenSize = 0n
130
146
 
131
147
  for (let i = 0; i < children.length; i++) {
132
148
  const child = children.get(i)
@@ -138,7 +154,7 @@ async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot:
138
154
  const labelPrefix = i.toString(16).toUpperCase().padStart(2, '0')
139
155
 
140
156
  if (child instanceof Bucket) {
141
- let shard: ImportResult | undefined
157
+ let shard
142
158
 
143
159
  for await (const subShard of flush(child, blockstore, null, options)) {
144
160
  shard = subShard
@@ -150,13 +166,13 @@ async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot:
150
166
 
151
167
  links.push({
152
168
  Name: labelPrefix,
153
- Tsize: shard.size,
169
+ Tsize: Number(shard.size),
154
170
  Hash: shard.cid
155
171
  })
156
172
  childrenSize += shard.size
157
- } else if (typeof child.value.flush === 'function') {
173
+ } else if (isDir(child.value)) {
158
174
  const dir = child.value
159
- let flushedDir
175
+ let flushedDir: ImportResult | undefined
160
176
 
161
177
  for await (const entry of dir.flush(blockstore)) {
162
178
  flushedDir = entry
@@ -164,14 +180,18 @@ async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot:
164
180
  yield flushedDir
165
181
  }
166
182
 
183
+ if (flushedDir == null) {
184
+ throw new Error('Did not flush dir')
185
+ }
186
+
167
187
  const label = labelPrefix + child.key
168
188
  links.push({
169
189
  Name: label,
170
- Tsize: flushedDir.size,
190
+ Tsize: Number(flushedDir.size),
171
191
  Hash: flushedDir.cid
172
192
  })
173
193
 
174
- childrenSize += flushedDir.size // eslint-disable-line @typescript-eslint/restrict-plus-operands
194
+ childrenSize += flushedDir.size
175
195
  } else {
176
196
  const value = child.value
177
197
 
@@ -184,10 +204,10 @@ async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot:
184
204
 
185
205
  links.push({
186
206
  Name: label,
187
- Tsize: size,
207
+ Tsize: Number(size),
188
208
  Hash: value.cid
189
209
  })
190
- childrenSize += size ?? 0 // eslint-disable-line @typescript-eslint/restrict-plus-operands
210
+ childrenSize += BigInt(size ?? 0)
191
211
  }
192
212
  }
193
213
 
@@ -197,7 +217,7 @@ async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot:
197
217
  const dir = new UnixFS({
198
218
  type: 'hamt-sharded-directory',
199
219
  data,
200
- fanout: bucket.tableSize(),
220
+ fanout: BigInt(bucket.tableSize()),
201
221
  hashType: hamtHashCode,
202
222
  mtime: shardRoot?.mtime,
203
223
  mode: shardRoot?.mode
@@ -209,11 +229,90 @@ async function * flush (bucket: Bucket<any>, blockstore: Blockstore, shardRoot:
209
229
  }
210
230
  const buffer = encode(prepare(node))
211
231
  const cid = await persist(buffer, blockstore, options)
212
- const size = buffer.length + childrenSize
232
+ const size = BigInt(buffer.byteLength) + childrenSize
213
233
 
214
234
  yield {
215
235
  cid,
216
- node,
236
+ unixfs: dir,
217
237
  size
218
238
  }
219
239
  }
240
+
241
+ function isDir (obj: any): obj is Dir {
242
+ return typeof obj.flush === 'function'
243
+ }
244
+
245
+ function calculateSize (bucket: Bucket<any>, shardRoot: DirSharded | null, options: PersistOptions): number {
246
+ const children = bucket._children
247
+ const links: PBLink[] = []
248
+
249
+ for (let i = 0; i < children.length; i++) {
250
+ const child = children.get(i)
251
+
252
+ if (child == null) {
253
+ continue
254
+ }
255
+
256
+ const labelPrefix = i.toString(16).toUpperCase().padStart(2, '0')
257
+
258
+ if (child instanceof Bucket) {
259
+ const size = calculateSize(child, null, options)
260
+
261
+ links.push({
262
+ Name: labelPrefix,
263
+ Tsize: Number(size),
264
+ Hash: options.cidVersion === 0 ? CID_V0 : CID_V1
265
+ })
266
+ } else if (typeof child.value.flush === 'function') {
267
+ const dir = child.value
268
+ const size = dir.nodeSize()
269
+
270
+ links.push({
271
+ Name: labelPrefix + child.key,
272
+ Tsize: Number(size),
273
+ Hash: options.cidVersion === 0 ? CID_V0 : CID_V1
274
+ })
275
+ } else {
276
+ const value = child.value
277
+
278
+ if (value.cid == null) {
279
+ continue
280
+ }
281
+
282
+ const label = labelPrefix + child.key
283
+ const size = value.size
284
+
285
+ links.push({
286
+ Name: label,
287
+ Tsize: Number(size),
288
+ Hash: value.cid
289
+ })
290
+ }
291
+ }
292
+
293
+ // go-ipfs uses little endian, that's why we have to
294
+ // reverse the bit field before storing it
295
+ const data = Uint8Array.from(children.bitField().reverse())
296
+ const dir = new UnixFS({
297
+ type: 'hamt-sharded-directory',
298
+ data,
299
+ fanout: BigInt(bucket.tableSize()),
300
+ hashType: hamtHashCode,
301
+ mtime: shardRoot?.mtime,
302
+ mode: shardRoot?.mode
303
+ })
304
+
305
+ const buffer = encode(prepare({
306
+ Data: dir.marshal(),
307
+ Links: links
308
+ }))
309
+
310
+ return buffer.length
311
+ }
312
+
313
+ // we use these to calculate the node size to use as a check for whether a directory
314
+ // should be sharded or not. Since CIDs have a constant length and We're only
315
+ // interested in the data length and not the actual content identifier we can use
316
+ // any old CID instead of having to hash the data which is expensive.
317
+ export const CID_V0 = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
318
+ export const CID_V1 = CID.parse('zdj7WbTaiJT1fgatdet9Ei9iDB5hdCxkbVyhyh8YTUnXMiwYi')
@@ -1,31 +1,65 @@
1
- import { HeliaError } from '@helia/interface/errors'
1
+ export abstract class UnixFSError extends Error {
2
+ public readonly name: string
3
+ public readonly code: string
2
4
 
3
- export class NotUnixFSError extends HeliaError {
5
+ constructor (message: string, name: string, code: string) {
6
+ super(message)
7
+
8
+ this.name = name
9
+ this.code = code
10
+ }
11
+ }
12
+
13
+ export class NotUnixFSError extends UnixFSError {
4
14
  constructor (message = 'not a Unixfs node') {
5
15
  super(message, 'NotUnixFSError', 'ERR_NOT_UNIXFS')
6
16
  }
7
17
  }
8
18
 
9
- export class InvalidPBNodeError extends HeliaError {
19
+ export class InvalidPBNodeError extends UnixFSError {
10
20
  constructor (message = 'invalid PBNode') {
11
21
  super(message, 'InvalidPBNodeError', 'ERR_INVALID_PBNODE')
12
22
  }
13
23
  }
14
24
 
15
- export class UnknownError extends HeliaError {
25
+ export class UnknownError extends UnixFSError {
16
26
  constructor (message = 'unknown error') {
17
27
  super(message, 'InvalidPBNodeError', 'ERR_UNKNOWN_ERROR')
18
28
  }
19
29
  }
20
30
 
21
- export class AlreadyExistsError extends HeliaError {
31
+ export class AlreadyExistsError extends UnixFSError {
22
32
  constructor (message = 'path already exists') {
23
- super(message, 'NotUnixFSError', 'ERR_ALREADY_EXISTS')
33
+ super(message, 'AlreadyExistsError', 'ERR_ALREADY_EXISTS')
24
34
  }
25
35
  }
26
36
 
27
- export class DoesNotExistError extends HeliaError {
37
+ export class DoesNotExistError extends UnixFSError {
28
38
  constructor (message = 'path does not exist') {
29
- super(message, 'NotUnixFSError', 'ERR_DOES_NOT_EXIST')
39
+ super(message, 'DoesNotExistError', 'ERR_DOES_NOT_EXIST')
40
+ }
41
+ }
42
+
43
+ export class NoContentError extends UnixFSError {
44
+ constructor (message = 'no content') {
45
+ super(message, 'NoContentError', 'ERR_NO_CONTENT')
46
+ }
47
+ }
48
+
49
+ export class NotAFileError extends UnixFSError {
50
+ constructor (message = 'not a file') {
51
+ super(message, 'NotAFileError', 'ERR_NOT_A_FILE')
52
+ }
53
+ }
54
+
55
+ export class NotADirectoryError extends UnixFSError {
56
+ constructor (message = 'not a directory') {
57
+ super(message, 'NotADirectoryError', 'ERR_NOT_A_DIRECTORY')
58
+ }
59
+ }
60
+
61
+ export class InvalidParametersError extends UnixFSError {
62
+ constructor (message = 'invalid parameters') {
63
+ super(message, 'InvalidParametersError', 'ERR_INVALID_PARAMETERS')
30
64
  }
31
65
  }
@@ -1,6 +1,6 @@
1
1
  import { murmur3128 } from '@multiformats/murmur3'
2
2
 
3
- export const hamtHashCode = murmur3128.code
3
+ export const hamtHashCode = BigInt(murmur3128.code)
4
4
  export const hamtBucketBits = 8
5
5
 
6
6
  export async function hamtHashFn (buf: Uint8Array): Promise<Uint8Array> {
@@ -7,19 +7,19 @@ import { DirSharded } from './dir-sharded.js'
7
7
  import { logger } from '@libp2p/logger'
8
8
  import { UnixFS } from 'ipfs-unixfs'
9
9
  import last from 'it-last'
10
- import { CID } from 'multiformats/cid'
10
+ import type { CID, Version } from 'multiformats/cid'
11
11
  import {
12
12
  hamtHashCode,
13
13
  hamtHashFn,
14
14
  hamtBucketBits
15
15
  } from './hamt-constants.js'
16
16
  import type { PBLink, PBNode } from '@ipld/dag-pb/interface'
17
- import { sha256 } from 'multiformats/hashes/sha2'
18
17
  import type { Blockstore } from 'interface-blockstore'
19
18
  import type { Mtime } from 'ipfs-unixfs'
20
19
  import type { Directory } from './cid-to-directory.js'
21
20
  import type { AbortOptions } from '@libp2p/interfaces'
22
21
  import type { ImportResult } from 'ipfs-unixfs-importer'
22
+ import { persist } from './persist.js'
23
23
 
24
24
  const log = logger('helia:unixfs:commands:utils:hamt-utils')
25
25
 
@@ -29,37 +29,38 @@ export interface UpdateHamtResult {
29
29
  size: number
30
30
  }
31
31
 
32
- export const updateHamtDirectory = async (parent: Directory, blockstore: Blockstore, links: PBLink[], bucket: Bucket<any>, options: AbortOptions): Promise<UpdateHamtResult> => {
33
- if (parent.node.Data == null) {
32
+ export interface UpdateHamtDirectoryOptions extends AbortOptions {
33
+ cidVersion: Version
34
+ }
35
+
36
+ export const updateHamtDirectory = async (pbNode: PBNode, blockstore: Blockstore, bucket: Bucket<any>, options: UpdateHamtDirectoryOptions): Promise<UpdateHamtResult> => {
37
+ if (pbNode.Data == null) {
34
38
  throw new Error('Could not update HAMT directory because parent had no data')
35
39
  }
36
40
 
37
41
  // update parent with new bit field
38
- const data = Uint8Array.from(bucket._children.bitField().reverse())
39
- const node = UnixFS.unmarshal(parent.node.Data)
42
+ const node = UnixFS.unmarshal(pbNode.Data)
40
43
  const dir = new UnixFS({
41
44
  type: 'hamt-sharded-directory',
42
- data,
43
- fanout: bucket.tableSize(),
45
+ data: Uint8Array.from(bucket._children.bitField().reverse()),
46
+ fanout: BigInt(bucket.tableSize()),
44
47
  hashType: hamtHashCode,
45
48
  mode: node.mode,
46
49
  mtime: node.mtime
47
50
  })
48
51
 
49
- parent.node = {
52
+ const updatedPbNode = {
50
53
  Data: dir.marshal(),
51
- Links: links.sort((a, b) => (a.Name ?? '').localeCompare(b.Name ?? ''))
54
+ Links: pbNode.Links
52
55
  }
53
- const buf = dagPB.encode(parent.node)
54
- const hash = await sha256.digest(buf)
55
- const cid = CID.create(parent.cid.version, dagPB.code, hash)
56
56
 
57
- await blockstore.put(cid, buf, options)
57
+ const buf = dagPB.encode(dagPB.prepare(updatedPbNode))
58
+ const cid = await persist(buf, blockstore, options)
58
59
 
59
60
  return {
60
- node: parent.node,
61
+ node: updatedPbNode,
61
62
  cid,
62
- size: links.reduce((sum, link) => sum + (link.Tsize ?? 0), buf.length)
63
+ size: pbNode.Links.reduce((sum, link) => sum + (link.Tsize ?? 0), buf.byteLength)
63
64
  }
64
65
  }
65
66
 
@@ -77,26 +78,24 @@ export const recreateHamtLevel = async (blockstore: Blockstore, links: PBLink[],
77
78
  }
78
79
 
79
80
  export const recreateInitialHamtLevel = async (links: PBLink[]): Promise<Bucket<any>> => {
80
- const bucket = createHAMT({
81
+ const bucket = createHAMT<any>({
81
82
  hashFn: hamtHashFn,
82
83
  bits: hamtBucketBits
83
84
  })
84
85
 
85
- // populate sub bucket but do not recurse as we do not want to pull whole shard in
86
+ // populate sub bucket but do not recurse as we do not want to load the whole shard
86
87
  await Promise.all(
87
88
  links.map(async link => {
88
89
  const linkName = (link.Name ?? '')
89
90
 
90
91
  if (linkName.length === 2) {
91
92
  const pos = parseInt(linkName, 16)
92
-
93
93
  const subBucket = new Bucket({
94
94
  hash: bucket._options.hash,
95
95
  bits: bucket._options.bits
96
96
  }, bucket, pos)
97
- bucket._putObjectAt(pos, subBucket)
98
97
 
99
- await Promise.resolve(); return
98
+ bucket._putObjectAt(pos, subBucket)
100
99
  }
101
100
 
102
101
  await bucket.put(linkName.substring(2), {
@@ -127,8 +126,6 @@ export const addLinksToHamtBucket = async (blockstore: Blockstore, links: PBLink
127
126
  bucket._putObjectAt(pos, subBucket)
128
127
 
129
128
  await addLinksToHamtBucket(blockstore, node.Links, subBucket, rootBucket, options)
130
-
131
- await Promise.resolve(); return
132
129
  }
133
130
 
134
131
  await rootBucket.put(linkName.substring(2), {
@@ -147,22 +144,19 @@ export const toPrefix = (position: number): string => {
147
144
  .substring(0, 2)
148
145
  }
149
146
 
150
- export interface HamtPath {
151
- rootBucket: Bucket<any>
152
- path: Array<{
153
- bucket: Bucket<any>
154
- prefix: string
155
- node?: PBNode
156
- }>
147
+ export interface HamtPathSegment {
148
+ bucket?: Bucket<any>
149
+ prefix?: string
150
+ node?: PBNode
151
+ cid?: CID
152
+ size?: number
157
153
  }
158
154
 
159
- export const generatePath = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise<HamtPath> => {
155
+ export const generatePath = async (root: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise<HamtPathSegment[]> => {
160
156
  // start at the root bucket and descend, loading nodes as we go
161
- const rootBucket = await recreateInitialHamtLevel(parent.node.Links)
157
+ const rootBucket = await recreateInitialHamtLevel(root.node.Links)
162
158
  const position = await rootBucket._findNewBucketAndPos(name)
163
-
164
- // the path to the root bucket
165
- const path: Array<{ bucket: Bucket<any>, prefix: string, node?: PBNode }> = [{
159
+ const path: HamtPathSegment[] = [{
166
160
  bucket: position.bucket,
167
161
  prefix: toPrefix(position.pos)
168
162
  }]
@@ -174,23 +168,32 @@ export const generatePath = async (parent: Directory, name: string, blockstore:
174
168
  prefix: toPrefix(currentBucket._posAtParent)
175
169
  })
176
170
 
177
- // @ts-expect-error - only the root bucket's parent will be undefined
171
+ if (currentBucket._parent == null) {
172
+ break
173
+ }
174
+
178
175
  currentBucket = currentBucket._parent
179
176
  }
180
177
 
178
+ // add the root bucket to the path
179
+ path.push({
180
+ bucket: rootBucket,
181
+ node: root.node
182
+ })
183
+
181
184
  path.reverse()
182
- path[0].node = parent.node
183
185
 
184
186
  // load PbNode for each path segment
185
- for (let i = 0; i < path.length; i++) {
187
+ for (let i = 1; i < path.length; i++) {
186
188
  const segment = path[i]
189
+ const previousSegment = path[i - 1]
187
190
 
188
- if (segment.node == null) {
191
+ if (previousSegment.node == null) {
189
192
  throw new Error('Could not generate HAMT path')
190
193
  }
191
194
 
192
195
  // find prefix in links
193
- const link = segment.node.Links
196
+ const link = previousSegment.node.Links
194
197
  .filter(link => (link.Name ?? '').substring(0, 2) === segment.prefix)
195
198
  .pop()
196
199
 
@@ -202,8 +205,10 @@ export const generatePath = async (parent: Directory, name: string, blockstore:
202
205
  continue
203
206
  }
204
207
 
208
+ const linkName = link.Name ?? ''
209
+
205
210
  // found entry
206
- if (link.Name === `${segment.prefix}${name}`) {
211
+ if (linkName === `${segment.prefix}${name}`) {
207
212
  log(`Link ${segment.prefix}${name} will be replaced`)
208
213
  // file already existed, file will be added to the current bucket
209
214
  // return path
@@ -212,13 +217,17 @@ export const generatePath = async (parent: Directory, name: string, blockstore:
212
217
 
213
218
  // found subshard
214
219
  log(`Found subshard ${segment.prefix}`)
215
- const block = await blockstore.get(link.Hash, options)
220
+ const block = await blockstore.get(link.Hash)
216
221
  const node = dagPB.decode(block)
217
222
 
218
223
  // subshard hasn't been loaded, descend to the next level of the HAMT
219
224
  if (path[i + 1] == null) {
220
225
  log(`Loaded new subshard ${segment.prefix}`)
221
226
 
227
+ if (segment.bucket == null || segment.prefix == null) {
228
+ throw new Error('Shard was invalid')
229
+ }
230
+
222
231
  await recreateHamtLevel(blockstore, node.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options)
223
232
  const position = await rootBucket._findNewBucketAndPos(name)
224
233
 
@@ -232,30 +241,30 @@ export const generatePath = async (parent: Directory, name: string, blockstore:
232
241
  continue
233
242
  }
234
243
 
235
- const nextSegment = path[i + 1]
244
+ if (segment.bucket == null) {
245
+ throw new Error('Shard was invalid')
246
+ }
236
247
 
237
248
  // add intermediate links to bucket
238
- await addLinksToHamtBucket(blockstore, node.Links, nextSegment.bucket, rootBucket, options)
249
+ await addLinksToHamtBucket(blockstore, node.Links, segment.bucket, rootBucket, options)
239
250
 
240
- nextSegment.node = node
251
+ segment.node = node
241
252
  }
242
253
 
243
254
  await rootBucket.put(name, true)
244
255
 
245
256
  path.reverse()
246
257
 
247
- return {
248
- rootBucket,
249
- path
250
- }
258
+ return path
251
259
  }
252
260
 
253
261
  export interface CreateShardOptions {
254
262
  mtime?: Mtime
255
263
  mode?: number
264
+ cidVersion: Version
256
265
  }
257
266
 
258
- export const createShard = async (blockstore: Blockstore, contents: Array<{ name: string, size: number, cid: CID }>, options: CreateShardOptions = {}): Promise<ImportResult> => {
267
+ export const createShard = async (blockstore: Blockstore, contents: Array<{ name: string, size: bigint, cid: CID }>, options: CreateShardOptions): Promise<ImportResult> => {
259
268
  const shard = new DirSharded({
260
269
  root: true,
261
270
  dir: true,