@helia/unixfs 0.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 (104) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +53 -0
  3. package/dist/index.min.js +3 -0
  4. package/dist/src/commands/add.d.ts +6 -0
  5. package/dist/src/commands/add.d.ts.map +1 -0
  6. package/dist/src/commands/add.js +38 -0
  7. package/dist/src/commands/add.js.map +1 -0
  8. package/dist/src/commands/cat.d.ts +5 -0
  9. package/dist/src/commands/cat.d.ts.map +1 -0
  10. package/dist/src/commands/cat.js +22 -0
  11. package/dist/src/commands/cat.js.map +1 -0
  12. package/dist/src/commands/chmod.d.ts +5 -0
  13. package/dist/src/commands/chmod.d.ts.map +1 -0
  14. package/dist/src/commands/chmod.js +108 -0
  15. package/dist/src/commands/chmod.js.map +1 -0
  16. package/dist/src/commands/cp.d.ts +5 -0
  17. package/dist/src/commands/cp.d.ts.map +1 -0
  18. package/dist/src/commands/cp.js +28 -0
  19. package/dist/src/commands/cp.js.map +1 -0
  20. package/dist/src/commands/ls.d.ts +5 -0
  21. package/dist/src/commands/ls.d.ts.map +1 -0
  22. package/dist/src/commands/ls.js +26 -0
  23. package/dist/src/commands/ls.js.map +1 -0
  24. package/dist/src/commands/mkdir.d.ts +5 -0
  25. package/dist/src/commands/mkdir.d.ts.map +1 -0
  26. package/dist/src/commands/mkdir.js +53 -0
  27. package/dist/src/commands/mkdir.js.map +1 -0
  28. package/dist/src/commands/rm.d.ts +5 -0
  29. package/dist/src/commands/rm.d.ts.map +1 -0
  30. package/dist/src/commands/rm.js +19 -0
  31. package/dist/src/commands/rm.js.map +1 -0
  32. package/dist/src/commands/stat.d.ts +5 -0
  33. package/dist/src/commands/stat.d.ts.map +1 -0
  34. package/dist/src/commands/stat.js +108 -0
  35. package/dist/src/commands/stat.js.map +1 -0
  36. package/dist/src/commands/touch.d.ts +5 -0
  37. package/dist/src/commands/touch.d.ts.map +1 -0
  38. package/dist/src/commands/touch.js +111 -0
  39. package/dist/src/commands/touch.js.map +1 -0
  40. package/dist/src/commands/utils/add-link.d.ts +21 -0
  41. package/dist/src/commands/utils/add-link.d.ts.map +1 -0
  42. package/dist/src/commands/utils/add-link.js +224 -0
  43. package/dist/src/commands/utils/add-link.js.map +1 -0
  44. package/dist/src/commands/utils/cid-to-directory.d.ts +10 -0
  45. package/dist/src/commands/utils/cid-to-directory.d.ts.map +1 -0
  46. package/dist/src/commands/utils/cid-to-directory.js +13 -0
  47. package/dist/src/commands/utils/cid-to-directory.js.map +1 -0
  48. package/dist/src/commands/utils/cid-to-pblink.d.ts +6 -0
  49. package/dist/src/commands/utils/cid-to-pblink.d.ts.map +1 -0
  50. package/dist/src/commands/utils/cid-to-pblink.js +19 -0
  51. package/dist/src/commands/utils/cid-to-pblink.js.map +1 -0
  52. package/dist/src/commands/utils/dir-sharded.d.ts +67 -0
  53. package/dist/src/commands/utils/dir-sharded.d.ts.map +1 -0
  54. package/dist/src/commands/utils/dir-sharded.js +136 -0
  55. package/dist/src/commands/utils/dir-sharded.js.map +1 -0
  56. package/dist/src/commands/utils/errors.d.ts +17 -0
  57. package/dist/src/commands/utils/errors.d.ts.map +1 -0
  58. package/dist/src/commands/utils/errors.js +27 -0
  59. package/dist/src/commands/utils/errors.js.map +1 -0
  60. package/dist/src/commands/utils/hamt-constants.d.ts +4 -0
  61. package/dist/src/commands/utils/hamt-constants.d.ts.map +1 -0
  62. package/dist/src/commands/utils/hamt-constants.js +13 -0
  63. package/dist/src/commands/utils/hamt-constants.js.map +1 -0
  64. package/dist/src/commands/utils/hamt-utils.d.ts +37 -0
  65. package/dist/src/commands/utils/hamt-utils.d.ts.map +1 -0
  66. package/dist/src/commands/utils/hamt-utils.js +202 -0
  67. package/dist/src/commands/utils/hamt-utils.js.map +1 -0
  68. package/dist/src/commands/utils/persist.d.ts +10 -0
  69. package/dist/src/commands/utils/persist.d.ts.map +1 -0
  70. package/dist/src/commands/utils/persist.js +12 -0
  71. package/dist/src/commands/utils/persist.js.map +1 -0
  72. package/dist/src/commands/utils/remove-link.d.ts +11 -0
  73. package/dist/src/commands/utils/remove-link.d.ts.map +1 -0
  74. package/dist/src/commands/utils/remove-link.js +92 -0
  75. package/dist/src/commands/utils/remove-link.js.map +1 -0
  76. package/dist/src/commands/utils/resolve.d.ts +28 -0
  77. package/dist/src/commands/utils/resolve.d.ts.map +1 -0
  78. package/dist/src/commands/utils/resolve.js +85 -0
  79. package/dist/src/commands/utils/resolve.js.map +1 -0
  80. package/dist/src/index.d.ts +97 -0
  81. package/dist/src/index.d.ts.map +1 -0
  82. package/dist/src/index.js +48 -0
  83. package/dist/src/index.js.map +1 -0
  84. package/package.json +166 -0
  85. package/src/commands/add.ts +46 -0
  86. package/src/commands/cat.ts +31 -0
  87. package/src/commands/chmod.ts +133 -0
  88. package/src/commands/cp.ts +41 -0
  89. package/src/commands/ls.ts +36 -0
  90. package/src/commands/mkdir.ts +71 -0
  91. package/src/commands/rm.ts +31 -0
  92. package/src/commands/stat.ts +137 -0
  93. package/src/commands/touch.ts +136 -0
  94. package/src/commands/utils/add-link.ts +319 -0
  95. package/src/commands/utils/cid-to-directory.ts +23 -0
  96. package/src/commands/utils/cid-to-pblink.ts +26 -0
  97. package/src/commands/utils/dir-sharded.ts +219 -0
  98. package/src/commands/utils/errors.ts +31 -0
  99. package/src/commands/utils/hamt-constants.ts +14 -0
  100. package/src/commands/utils/hamt-utils.ts +285 -0
  101. package/src/commands/utils/persist.ts +22 -0
  102. package/src/commands/utils/remove-link.ts +151 -0
  103. package/src/commands/utils/resolve.ts +130 -0
  104. package/src/index.ts +174 -0
@@ -0,0 +1,137 @@
1
+ import { Blockstore, exporter } from 'ipfs-unixfs-exporter'
2
+ import type { CID } from 'multiformats/cid'
3
+ import type { StatOptions, UnixFSStats } from '../index.js'
4
+ import mergeOpts from 'merge-options'
5
+ import { logger } from '@libp2p/logger'
6
+ import { UnixFS } from 'ipfs-unixfs'
7
+ import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js'
8
+ import * as dagPb from '@ipld/dag-pb'
9
+ import type { AbortOptions } from '@libp2p/interfaces'
10
+ import type { Mtime } from 'ipfs-unixfs'
11
+ import { resolve } from './utils/resolve.js'
12
+ import * as raw from 'multiformats/codecs/raw'
13
+
14
+ const mergeOptions = mergeOpts.bind({ ignoreUndefined: true })
15
+ const log = logger('helia:unixfs:stat')
16
+
17
+ const defaultOptions = {
18
+
19
+ }
20
+
21
+ export async function stat (cid: CID, blockstore: Blockstore, options: Partial<StatOptions> = {}): Promise<UnixFSStats> {
22
+ const opts: StatOptions = mergeOptions(defaultOptions, options)
23
+ const resolved = await resolve(cid, options.path, blockstore, opts)
24
+
25
+ log('stat %c', resolved.cid)
26
+
27
+ const result = await exporter(resolved.cid, blockstore, opts)
28
+
29
+ if (result.type !== 'file' && result.type !== 'directory' && result.type !== 'raw') {
30
+ throw new NotUnixFSError()
31
+ }
32
+
33
+ let fileSize: number = 0
34
+ let dagSize: number = 0
35
+ let localFileSize: number = 0
36
+ let localDagSize: number = 0
37
+ let blocks: number = 0
38
+ let mode: number | undefined
39
+ let mtime: Mtime | undefined
40
+ const type = result.type
41
+
42
+ if (result.type === 'raw') {
43
+ fileSize = result.node.byteLength
44
+ dagSize = result.node.byteLength
45
+ localFileSize = result.node.byteLength
46
+ localDagSize = result.node.byteLength
47
+ blocks = 1
48
+ }
49
+
50
+ if (result.type === 'directory') {
51
+ fileSize = 0
52
+ dagSize = result.unixfs.marshal().byteLength
53
+ localFileSize = 0
54
+ localDagSize = dagSize
55
+ blocks = 1
56
+ mode = result.unixfs.mode
57
+ mtime = result.unixfs.mtime
58
+ }
59
+
60
+ if (result.type === 'file') {
61
+ const results = await inspectDag(resolved.cid, blockstore, opts)
62
+
63
+ fileSize = result.unixfs.fileSize()
64
+ dagSize = (result.node.Data?.byteLength ?? 0) + result.node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0)
65
+ localFileSize = results.localFileSize
66
+ localDagSize = results.localDagSize
67
+ blocks = results.blocks
68
+ mode = result.unixfs.mode
69
+ mtime = result.unixfs.mtime
70
+ }
71
+
72
+ return {
73
+ cid: resolved.cid,
74
+ mode,
75
+ mtime,
76
+ fileSize,
77
+ dagSize,
78
+ localFileSize,
79
+ localDagSize,
80
+ blocks,
81
+ type
82
+ }
83
+ }
84
+
85
+ interface InspectDagResults {
86
+ localFileSize: number
87
+ localDagSize: number
88
+ blocks: number
89
+ }
90
+
91
+ async function inspectDag (cid: CID, blockstore: Blockstore, options: AbortOptions): Promise<InspectDagResults> {
92
+ const results = {
93
+ localFileSize: 0,
94
+ localDagSize: 0,
95
+ blocks: 0
96
+ }
97
+
98
+ if (await blockstore.has(cid, options)) {
99
+ const block = await blockstore.get(cid, options)
100
+ results.blocks++
101
+ results.localDagSize += block.byteLength
102
+
103
+ if (cid.code === raw.code) {
104
+ results.localFileSize += block.byteLength
105
+ } else if (cid.code === dagPb.code) {
106
+ const pbNode = dagPb.decode(block)
107
+
108
+ if (pbNode.Links.length > 0) {
109
+ // intermediate node
110
+ for (const link of pbNode.Links) {
111
+ const linkResult = await inspectDag(link.Hash, blockstore, options)
112
+
113
+ results.localFileSize += linkResult.localFileSize
114
+ results.localDagSize += linkResult.localDagSize
115
+ results.blocks += linkResult.blocks
116
+ }
117
+ } else {
118
+ // leaf node
119
+ if (pbNode.Data == null) {
120
+ throw new InvalidPBNodeError(`PBNode ${cid.toString()} had no data`)
121
+ }
122
+
123
+ const unixfs = UnixFS.unmarshal(pbNode.Data)
124
+
125
+ if (unixfs.data == null) {
126
+ throw new InvalidPBNodeError(`UnixFS node ${cid.toString()} had no data`)
127
+ }
128
+
129
+ results.localFileSize += unixfs.data.byteLength ?? 0
130
+ }
131
+ } else {
132
+ throw new UnknownError(`${cid.toString()} was neither DAG_PB nor RAW`)
133
+ }
134
+ }
135
+
136
+ return results
137
+ }
@@ -0,0 +1,136 @@
1
+ import { recursive } from 'ipfs-unixfs-exporter'
2
+ import { CID } from 'multiformats/cid'
3
+ import type { TouchOptions } from '../index.js'
4
+ import mergeOpts from 'merge-options'
5
+ import { logger } from '@libp2p/logger'
6
+ import { UnixFS } from 'ipfs-unixfs'
7
+ import { pipe } from 'it-pipe'
8
+ import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js'
9
+ import * as dagPB from '@ipld/dag-pb'
10
+ import type { PBNode, PBLink } from '@ipld/dag-pb'
11
+ import { importer } from 'ipfs-unixfs-importer'
12
+ import { persist } from './utils/persist.js'
13
+ import type { Blockstore } from 'interface-blockstore'
14
+ import last from 'it-last'
15
+ import { sha256 } from 'multiformats/hashes/sha2'
16
+ import { resolve, updatePathCids } from './utils/resolve.js'
17
+ import * as raw from 'multiformats/codecs/raw'
18
+
19
+ const mergeOptions = mergeOpts.bind({ ignoreUndefined: true })
20
+ const log = logger('helia:unixfs:touch')
21
+
22
+ const defaultOptions = {
23
+ recursive: false
24
+ }
25
+
26
+ export async function touch (cid: CID, blockstore: Blockstore, options: Partial<TouchOptions> = {}): Promise<CID> {
27
+ const opts: TouchOptions = mergeOptions(defaultOptions, options)
28
+ const resolved = await resolve(cid, opts.path, blockstore, opts)
29
+ const mtime = opts.mtime ?? {
30
+ secs: Date.now() / 1000,
31
+ nsecs: 0
32
+ }
33
+
34
+ log('touch %c %o', resolved.cid, mtime)
35
+
36
+ if (opts.recursive) {
37
+ // recursively export from root CID, change perms of each entry then reimport
38
+ // but do not reimport files, only manipulate dag-pb nodes
39
+ const root = await pipe(
40
+ async function * () {
41
+ for await (const entry of recursive(resolved.cid, blockstore)) {
42
+ let metadata: UnixFS
43
+ let links: PBLink[]
44
+
45
+ if (entry.type === 'raw') {
46
+ metadata = new UnixFS({ data: entry.node })
47
+ links = []
48
+ } else if (entry.type === 'file' || entry.type === 'directory') {
49
+ metadata = entry.unixfs
50
+ links = entry.node.Links
51
+ } else {
52
+ throw new NotUnixFSError()
53
+ }
54
+
55
+ metadata.mtime = mtime
56
+
57
+ const node = {
58
+ Data: metadata.marshal(),
59
+ Links: links
60
+ }
61
+
62
+ yield {
63
+ path: entry.path,
64
+ content: node
65
+ }
66
+ }
67
+ },
68
+ // @ts-expect-error we account for the incompatible source type with our custom dag builder below
69
+ (source) => importer(source, blockstore, {
70
+ ...opts,
71
+ pin: false,
72
+ dagBuilder: async function * (source, block, opts) {
73
+ for await (const entry of source) {
74
+ yield async function () {
75
+ // @ts-expect-error cannot derive type
76
+ const node: PBNode = entry.content
77
+
78
+ const buf = dagPB.encode(node)
79
+ const cid = await persist(buf, block, opts)
80
+
81
+ if (node.Data == null) {
82
+ throw new InvalidPBNodeError(`${cid} had no data`)
83
+ }
84
+
85
+ const unixfs = UnixFS.unmarshal(node.Data)
86
+
87
+ return {
88
+ cid,
89
+ size: buf.length,
90
+ path: entry.path,
91
+ unixfs
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }),
97
+ async (nodes) => await last(nodes)
98
+ )
99
+
100
+ if (root == null) {
101
+ throw new UnknownError(`Could not chmod ${resolved.cid.toString()}`)
102
+ }
103
+
104
+ return await updatePathCids(root.cid, resolved, blockstore, options)
105
+ }
106
+
107
+ const block = await blockstore.get(resolved.cid)
108
+ let metadata: UnixFS
109
+ let links: PBLink[] = []
110
+
111
+ if (resolved.cid.code === raw.code) {
112
+ metadata = new UnixFS({ data: block })
113
+ } else {
114
+ const node = dagPB.decode(block)
115
+ links = node.Links
116
+
117
+ if (node.Data == null) {
118
+ throw new InvalidPBNodeError(`${resolved.cid.toString()} had no data`)
119
+ }
120
+
121
+ metadata = UnixFS.unmarshal(node.Data)
122
+ }
123
+
124
+ metadata.mtime = mtime
125
+ const updatedBlock = dagPB.encode({
126
+ Data: metadata.marshal(),
127
+ Links: links
128
+ })
129
+
130
+ const hash = await sha256.digest(updatedBlock)
131
+ const updatedCid = CID.create(resolved.cid.version, dagPB.code, hash)
132
+
133
+ await blockstore.put(updatedCid, updatedBlock)
134
+
135
+ return await updatePathCids(updatedCid, resolved, blockstore, options)
136
+ }
@@ -0,0 +1,319 @@
1
+ import * as dagPB from '@ipld/dag-pb'
2
+ import { CID } from 'multiformats/cid'
3
+ import { logger } from '@libp2p/logger'
4
+ import { UnixFS } from 'ipfs-unixfs'
5
+ import { DirSharded } from './dir-sharded.js'
6
+ import {
7
+ updateHamtDirectory,
8
+ recreateHamtLevel,
9
+ recreateInitialHamtLevel,
10
+ createShard,
11
+ toPrefix,
12
+ addLinksToHamtBucket
13
+ } from './hamt-utils.js'
14
+ import last from 'it-last'
15
+ import type { Blockstore } from 'ipfs-unixfs-exporter'
16
+ import type { PBNode, PBLink } from '@ipld/dag-pb/interface'
17
+ import { sha256 } from 'multiformats/hashes/sha2'
18
+ import type { Bucket } from 'hamt-sharding'
19
+ import { AlreadyExistsError, InvalidPBNodeError } from './errors.js'
20
+ import { InvalidParametersError } from '@helia/interface/errors'
21
+ import type { ImportResult } from 'ipfs-unixfs-importer'
22
+ import type { AbortOptions } from '@libp2p/interfaces'
23
+ import type { Directory } from './cid-to-directory.js'
24
+
25
+ const log = logger('helia:unixfs:components:utils:add-link')
26
+
27
+ export interface AddLinkResult {
28
+ node: PBNode
29
+ cid: CID
30
+ size: number
31
+ }
32
+
33
+ export interface AddLinkOptions extends AbortOptions {
34
+ allowOverwriting: boolean
35
+ }
36
+
37
+ export async function addLink (parent: Directory, child: Required<PBLink>, blockstore: Blockstore, options: AddLinkOptions): Promise<AddLinkResult> {
38
+ if (parent.node.Data == null) {
39
+ throw new InvalidParametersError('Invalid parent passed to addLink')
40
+ }
41
+
42
+ // FIXME: this should work on block size not number of links
43
+ if (parent.node.Links.length >= 1000) {
44
+ log('converting directory to sharded directory')
45
+
46
+ const result = await convertToShardedDirectory(parent, blockstore)
47
+ parent.cid = result.cid
48
+ parent.node = dagPB.decode(await blockstore.get(result.cid))
49
+ }
50
+
51
+ if (parent.node.Data == null) {
52
+ throw new InvalidParametersError('Invalid parent passed to addLink')
53
+ }
54
+
55
+ const meta = UnixFS.unmarshal(parent.node.Data)
56
+
57
+ if (meta.type === 'hamt-sharded-directory') {
58
+ log('adding link to sharded directory')
59
+
60
+ return await addToShardedDirectory(parent, child, blockstore, options)
61
+ }
62
+
63
+ log(`adding ${child.Name} (${child.Hash}) to regular directory`)
64
+
65
+ return await addToDirectory(parent, child, blockstore, options)
66
+ }
67
+
68
+ const convertToShardedDirectory = async (parent: Directory, blockstore: Blockstore): Promise<ImportResult> => {
69
+ if (parent.node.Data == null) {
70
+ throw new InvalidParametersError('Invalid parent passed to convertToShardedDirectory')
71
+ }
72
+
73
+ const unixfs = UnixFS.unmarshal(parent.node.Data)
74
+
75
+ const result = await createShard(blockstore, parent.node.Links.map(link => ({
76
+ name: (link.Name ?? ''),
77
+ size: link.Tsize ?? 0,
78
+ cid: link.Hash
79
+ })), {
80
+ mode: unixfs.mode,
81
+ mtime: unixfs.mtime
82
+ })
83
+
84
+ log(`Converted directory to sharded directory ${result.cid}`)
85
+
86
+ return result
87
+ }
88
+
89
+ const addToDirectory = async (parent: Directory, child: PBLink, blockstore: Blockstore, options: AddLinkOptions): Promise<AddLinkResult> => {
90
+ // Remove existing link if it exists
91
+ const parentLinks = parent.node.Links.filter((link) => {
92
+ const matches = link.Name === child.Name
93
+
94
+ if (matches && !options.allowOverwriting) {
95
+ throw new AlreadyExistsError()
96
+ }
97
+
98
+ return !matches
99
+ })
100
+ parentLinks.push(child)
101
+
102
+ if (parent.node.Data == null) {
103
+ throw new InvalidPBNodeError('Parent node with no data passed to addToDirectory')
104
+ }
105
+
106
+ const node = UnixFS.unmarshal(parent.node.Data)
107
+
108
+ let data
109
+ if (node.mtime != null) {
110
+ // Update mtime if previously set
111
+ const ms = Date.now()
112
+ const secs = Math.floor(ms / 1000)
113
+
114
+ node.mtime = {
115
+ secs,
116
+ nsecs: (ms - (secs * 1000)) * 1000
117
+ }
118
+
119
+ data = node.marshal()
120
+ } else {
121
+ data = parent.node.Data
122
+ }
123
+ parent.node = dagPB.prepare({
124
+ Data: data,
125
+ Links: parentLinks
126
+ })
127
+
128
+ // Persist the new parent PbNode
129
+ const buf = dagPB.encode(parent.node)
130
+ const hash = await sha256.digest(buf)
131
+ const cid = CID.create(parent.cid.version, dagPB.code, hash)
132
+
133
+ await blockstore.put(cid, buf)
134
+
135
+ return {
136
+ node: parent.node,
137
+ cid,
138
+ size: buf.length
139
+ }
140
+ }
141
+
142
+ const addToShardedDirectory = async (parent: Directory, child: Required<PBLink>, blockstore: Blockstore, options: AddLinkOptions): Promise<AddLinkResult> => {
143
+ const {
144
+ shard, path
145
+ } = await addFileToShardedDirectory(parent, child, blockstore, options)
146
+ const result = await last(shard.flush(blockstore))
147
+
148
+ if (result == null) {
149
+ throw new Error('No result from flushing shard')
150
+ }
151
+
152
+ const block = await blockstore.get(result.cid)
153
+ const node = dagPB.decode(block)
154
+
155
+ // we have written out the shard, but only one sub-shard will have been written so replace it in the original shard
156
+ const parentLinks = parent.node.Links.filter((link) => {
157
+ const matches = (link.Name ?? '').substring(0, 2) === path[0].prefix
158
+
159
+ if (matches && !options.allowOverwriting) {
160
+ throw new AlreadyExistsError()
161
+ }
162
+
163
+ return !matches
164
+ })
165
+
166
+ const newLink = node.Links
167
+ .find(link => (link.Name ?? '').substring(0, 2) === path[0].prefix)
168
+
169
+ if (newLink == null) {
170
+ throw new Error(`No link found with prefix ${path[0].prefix}`)
171
+ }
172
+
173
+ parentLinks.push(newLink)
174
+
175
+ return await updateHamtDirectory(parent, blockstore, parentLinks, path[0].bucket, options)
176
+ }
177
+
178
+ const addFileToShardedDirectory = async (parent: Directory, child: Required<PBLink>, blockstore: Blockstore, options: AddLinkOptions): Promise<{ shard: DirSharded, path: BucketPath[] }> => {
179
+ if (parent.node.Data == null) {
180
+ throw new InvalidPBNodeError('Parent node with no data passed to addFileToShardedDirectory')
181
+ }
182
+
183
+ // start at the root bucket and descend, loading nodes as we go
184
+ const rootBucket = await recreateInitialHamtLevel(parent.node.Links)
185
+ const node = UnixFS.unmarshal(parent.node.Data)
186
+
187
+ const shard = new DirSharded({
188
+ root: true,
189
+ dir: true,
190
+ parent: undefined,
191
+ parentKey: undefined,
192
+ path: '',
193
+ dirty: true,
194
+ flat: false,
195
+ mode: node.mode
196
+ }, {
197
+ ...options,
198
+ cidVersion: parent.cid.version
199
+ })
200
+ shard._bucket = rootBucket
201
+
202
+ if (node.mtime != null) {
203
+ // update mtime if previously set
204
+ shard.mtime = {
205
+ secs: Math.round(Date.now() / 1000)
206
+ }
207
+ }
208
+
209
+ // load subshards until the bucket & position no longer changes
210
+ const position = await rootBucket._findNewBucketAndPos(child.Name)
211
+ const path = toBucketPath(position)
212
+ path[0].node = parent.node
213
+ let index = 0
214
+
215
+ while (index < path.length) {
216
+ const segment = path[index]
217
+ index++
218
+ const node = segment.node
219
+
220
+ if (node == null) {
221
+ throw new Error('Segment had no node')
222
+ }
223
+
224
+ const link = node.Links
225
+ .find(link => (link.Name ?? '').substring(0, 2) === segment.prefix)
226
+
227
+ if (link == null) {
228
+ // prefix is new, file will be added to the current bucket
229
+ log(`Link ${segment.prefix}${child.Name} will be added`)
230
+ index = path.length
231
+
232
+ break
233
+ }
234
+
235
+ if (link.Name === `${segment.prefix}${child.Name}`) {
236
+ // file already existed, file will be added to the current bucket
237
+ log(`Link ${segment.prefix}${child.Name} will be replaced`)
238
+ index = path.length
239
+
240
+ break
241
+ }
242
+
243
+ if ((link.Name ?? '').length > 2) {
244
+ // another file had the same prefix, will be replaced with a subshard
245
+ log(`Link ${link.Name} ${link.Hash} will be replaced with a subshard`)
246
+ index = path.length
247
+
248
+ break
249
+ }
250
+
251
+ // load sub-shard
252
+ log(`Found subshard ${segment.prefix}`)
253
+ const block = await blockstore.get(link.Hash)
254
+ const subShard = dagPB.decode(block)
255
+
256
+ // subshard hasn't been loaded, descend to the next level of the HAMT
257
+ if (path[index] == null) {
258
+ log(`Loaded new subshard ${segment.prefix}`)
259
+ await recreateHamtLevel(blockstore, subShard.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options)
260
+
261
+ const position = await rootBucket._findNewBucketAndPos(child.Name)
262
+
263
+ path.push({
264
+ bucket: position.bucket,
265
+ prefix: toPrefix(position.pos),
266
+ node: subShard
267
+ })
268
+
269
+ break
270
+ }
271
+
272
+ const nextSegment = path[index]
273
+
274
+ // add next levels worth of links to bucket
275
+ await addLinksToHamtBucket(blockstore, subShard.Links, nextSegment.bucket, rootBucket, options)
276
+
277
+ nextSegment.node = subShard
278
+ }
279
+
280
+ // finally add the new file into the shard
281
+ await shard._bucket.put(child.Name, {
282
+ size: child.Tsize,
283
+ cid: child.Hash
284
+ })
285
+
286
+ return {
287
+ shard, path
288
+ }
289
+ }
290
+
291
+ export interface BucketPath {
292
+ bucket: Bucket<any>
293
+ prefix: string
294
+ node?: PBNode
295
+ }
296
+
297
+ const toBucketPath = (position: { pos: number, bucket: Bucket<any> }): BucketPath[] => {
298
+ const path = [{
299
+ bucket: position.bucket,
300
+ prefix: toPrefix(position.pos)
301
+ }]
302
+
303
+ let bucket = position.bucket._parent
304
+ let positionInBucket = position.bucket._posAtParent
305
+
306
+ while (bucket != null) {
307
+ path.push({
308
+ bucket,
309
+ prefix: toPrefix(positionInBucket)
310
+ })
311
+
312
+ positionInBucket = bucket._posAtParent
313
+ bucket = bucket._parent
314
+ }
315
+
316
+ path.reverse()
317
+
318
+ return path
319
+ }
@@ -0,0 +1,23 @@
1
+ import { NotADirectoryError } from '@helia/interface/errors'
2
+ import { Blockstore, exporter } from 'ipfs-unixfs-exporter'
3
+ import type { CID } from 'multiformats/cid'
4
+ import type { PBNode } from '@ipld/dag-pb'
5
+ import type { AbortOptions } from '@libp2p/interfaces'
6
+
7
+ export interface Directory {
8
+ cid: CID
9
+ node: PBNode
10
+ }
11
+
12
+ export async function cidToDirectory (cid: CID, blockstore: Blockstore, options: AbortOptions = {}): Promise<Directory> {
13
+ const entry = await exporter(cid, blockstore, options)
14
+
15
+ if (entry.type !== 'directory') {
16
+ throw new NotADirectoryError(`${cid.toString()} was not a UnixFS directory`)
17
+ }
18
+
19
+ return {
20
+ cid,
21
+ node: entry.node
22
+ }
23
+ }
@@ -0,0 +1,26 @@
1
+ import { Blockstore, exporter } from 'ipfs-unixfs-exporter'
2
+ import type { CID } from 'multiformats/cid'
3
+ import { NotUnixFSError } from './errors.js'
4
+ import * as dagPb from '@ipld/dag-pb'
5
+ import type { PBNode, PBLink } from '@ipld/dag-pb'
6
+ import type { AbortOptions } from '@libp2p/interfaces'
7
+
8
+ export async function cidToPBLink (cid: CID, name: string, blockstore: Blockstore, options?: AbortOptions): Promise<Required<PBLink>> {
9
+ const sourceEntry = await exporter(cid, blockstore, options)
10
+
11
+ if (sourceEntry.type !== 'directory' && sourceEntry.type !== 'file' && sourceEntry.type !== 'raw') {
12
+ throw new NotUnixFSError(`${cid.toString()} was not a UnixFS node`)
13
+ }
14
+
15
+ return {
16
+ Name: name,
17
+ Tsize: sourceEntry.node instanceof Uint8Array ? sourceEntry.node.byteLength : dagNodeTsize(sourceEntry.node),
18
+ Hash: cid
19
+ }
20
+ }
21
+
22
+ function dagNodeTsize (node: PBNode): number {
23
+ const linkSizes = node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0)
24
+
25
+ return dagPb.encode(node).byteLength + linkSizes
26
+ }