@helia/unixfs 4.0.3 → 5.0.0-daaa511

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,31 +1,36 @@
1
- import { type ByteStream, type DirectoryCandidate, type FileCandidate, importBytes, importByteStream, type ImportCandidateStream, importDirectory, importer, type ImporterOptions, importFile, type ImportResult } from 'ipfs-unixfs-importer'
1
+ import { importBytes, importByteStream, importer } from 'ipfs-unixfs-importer'
2
2
  import { fixedSize } from 'ipfs-unixfs-importer/chunker'
3
3
  import { balanced } from 'ipfs-unixfs-importer/layout'
4
+ import first from 'it-first'
5
+ import last from 'it-last'
6
+ import { InvalidParametersError } from '../errors.js'
7
+ import type { FileCandidate, AddOptions, AddFileOptions } from '../index.js'
4
8
  import type { PutStore } from '../unixfs.js'
9
+ import type { ByteStream, DirectoryCandidate, ImportCandidateStream, ImportResult } from 'ipfs-unixfs-importer'
5
10
  import type { CID } from 'multiformats/cid'
6
11
 
7
12
  /**
8
13
  * Default importer settings match Filecoin
9
14
  */
10
- const defaultImporterSettings: ImporterOptions = {
15
+ const defaultImporterSettings: AddOptions = {
11
16
  cidVersion: 1,
12
17
  rawLeaves: true,
13
18
  layout: balanced({
14
19
  maxChildrenPerNode: 1024
15
20
  }),
16
21
  chunker: fixedSize({
17
- chunkSize: 1048576
22
+ chunkSize: 1_048_576
18
23
  })
19
24
  }
20
25
 
21
- export async function * addAll (source: ImportCandidateStream, blockstore: PutStore, options: Partial<ImporterOptions> = {}): AsyncGenerator<ImportResult, void, unknown> {
26
+ export async function * addAll (source: ImportCandidateStream, blockstore: PutStore, options: Partial<AddOptions> = {}): AsyncGenerator<ImportResult, void, unknown> {
22
27
  yield * importer(source, blockstore, {
23
28
  ...defaultImporterSettings,
24
29
  ...options
25
30
  })
26
31
  }
27
32
 
28
- export async function addBytes (bytes: Uint8Array, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
33
+ export async function addBytes (bytes: Uint8Array, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
29
34
  const { cid } = await importBytes(bytes, blockstore, {
30
35
  ...defaultImporterSettings,
31
36
  ...options
@@ -34,7 +39,7 @@ export async function addBytes (bytes: Uint8Array, blockstore: PutStore, options
34
39
  return cid
35
40
  }
36
41
 
37
- export async function addByteStream (bytes: ByteStream, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
42
+ export async function addByteStream (bytes: ByteStream, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
38
43
  const { cid } = await importByteStream(bytes, blockstore, {
39
44
  ...defaultImporterSettings,
40
45
  ...options
@@ -43,23 +48,48 @@ export async function addByteStream (bytes: ByteStream, blockstore: PutStore, op
43
48
  return cid
44
49
  }
45
50
 
46
- export async function addFile (file: FileCandidate, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
47
- const { cid } = await importFile(file, blockstore, {
51
+ export async function addFile (file: FileCandidate, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
52
+ if (file.path == null) {
53
+ throw new InvalidParametersError('path is required')
54
+ }
55
+
56
+ if (file.content == null) {
57
+ throw new InvalidParametersError('content is required')
58
+ }
59
+
60
+ const result = await last(addAll([file], blockstore, {
48
61
  ...defaultImporterSettings,
49
- ...options
50
- })
62
+ ...options,
63
+ wrapWithDirectory: true
64
+ }))
51
65
 
52
- return cid
66
+ if (result == null) {
67
+ throw new InvalidParametersError('Nothing imported')
68
+ }
69
+
70
+ return result.cid
53
71
  }
54
72
 
55
- export async function addDirectory (dir: Partial<DirectoryCandidate>, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
56
- const { cid } = await importDirectory({
73
+ export async function addDirectory (dir: Partial<DirectoryCandidate>, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
74
+ // @ts-expect-error field is not in the types
75
+ if (dir.content != null) {
76
+ throw new InvalidParametersError('Directories cannot have content, use addFile instead')
77
+ }
78
+
79
+ const ord = dir.path == null ? first : last
80
+
81
+ const result = await ord(addAll([{
57
82
  ...dir,
58
83
  path: dir.path ?? '-'
59
- }, blockstore, {
84
+ }], blockstore, {
60
85
  ...defaultImporterSettings,
61
- ...options
62
- })
86
+ ...options,
87
+ wrapWithDirectory: dir.path != null
88
+ }))
63
89
 
64
- return cid
90
+ if (result == null) {
91
+ throw new InvalidParametersError('Nothing imported')
92
+ }
93
+
94
+ return result.cid
65
95
  }
@@ -1,17 +1,21 @@
1
1
  import * as dagPb from '@ipld/dag-pb'
2
2
  import { logger } from '@libp2p/logger'
3
+ import { ScalableCuckooFilter } from '@libp2p/utils/filters'
3
4
  import { UnixFS } from 'ipfs-unixfs'
4
- import { exporter } from 'ipfs-unixfs-exporter'
5
+ import { exporter, type RawNode, type UnixFSDirectory, type UnixFSFile } from 'ipfs-unixfs-exporter'
5
6
  import mergeOpts from 'merge-options'
6
7
  import * as raw from 'multiformats/codecs/raw'
7
8
  import { InvalidPBNodeError, NotUnixFSError, UnknownError } from '../errors.js'
8
9
  import { resolve } from './utils/resolve.js'
9
- import type { StatOptions, UnixFSStats } from '../index.js'
10
+ import type { ExtendedStatOptions, ExtendedDirectoryStats, ExtendedFileStats, StatOptions, DirectoryStats, FileStats, RawStats, ExtendedRawStats } from '../index.js'
10
11
  import type { GetStore, HasStore } from '../unixfs.js'
11
- import type { AbortOptions } from '@libp2p/interface'
12
- import type { Mtime } from 'ipfs-unixfs'
12
+ import type { Filter } from '@libp2p/utils/filters'
13
13
  import type { CID } from 'multiformats/cid'
14
14
 
15
+ // https://github.com/ipfs/specs/blob/main/UNIXFS.md#metadata
16
+ const DEFAULT_DIR_MODE = 0x755
17
+ const DEFAULT_FILE_MODE = 0x644
18
+
15
19
  const mergeOptions = mergeOpts.bind({ ignoreUndefined: true })
16
20
  const log = logger('helia:unixfs:stat')
17
21
 
@@ -19,7 +23,9 @@ const defaultOptions: StatOptions = {
19
23
 
20
24
  }
21
25
 
22
- export async function stat (cid: CID, blockstore: GetStore & HasStore, options: Partial<StatOptions> = {}): Promise<UnixFSStats> {
26
+ export async function stat (cid: CID, blockstore: GetStore & HasStore, options?: StatOptions): Promise<FileStats | DirectoryStats | RawStats>
27
+ export async function stat (cid: CID, blockstore: GetStore & HasStore, options?: ExtendedStatOptions): Promise<ExtendedFileStats | ExtendedDirectoryStats | ExtendedRawStats>
28
+ export async function stat (cid: CID, blockstore: GetStore & HasStore, options: Partial<ExtendedStatOptions> = {}): Promise<any> {
23
29
  const opts: StatOptions = mergeOptions(defaultOptions, options)
24
30
  const resolved = await resolve(cid, options.path, blockstore, opts)
25
31
 
@@ -27,116 +33,186 @@ export async function stat (cid: CID, blockstore: GetStore & HasStore, options:
27
33
 
28
34
  const result = await exporter(resolved.cid, blockstore, opts)
29
35
 
30
- if (result.type !== 'file' && result.type !== 'directory' && result.type !== 'raw') {
31
- throw new NotUnixFSError()
36
+ if (result.type === 'raw') {
37
+ if (options.extended === true) {
38
+ return createExtendedRawStats(result)
39
+ }
40
+
41
+ return createRawStats(result)
42
+ } else if (result.type === 'file' || result.type === 'directory') {
43
+ if (options.extended === true) {
44
+ return createExtendedStats(result, blockstore, options.filter ?? new ScalableCuckooFilter({ filterSize: 1024 }), options)
45
+ }
46
+
47
+ return createStats(result)
32
48
  }
33
49
 
34
- let fileSize: bigint = 0n
35
- let dagSize: bigint = 0n
36
- let localFileSize: bigint = 0n
37
- let localDagSize: bigint = 0n
38
- let blocks: number = 0
39
- let mode: number | undefined
40
- let mtime: Mtime | undefined
41
- const type = result.type
42
- let unixfs: UnixFS | undefined
50
+ throw new NotUnixFSError()
51
+ }
43
52
 
44
- if (result.type === 'raw') {
45
- fileSize = BigInt(result.node.byteLength)
46
- dagSize = BigInt(result.node.byteLength)
47
- localFileSize = BigInt(result.node.byteLength)
48
- localDagSize = BigInt(result.node.byteLength)
49
- blocks = 1
53
+ function createStats (entry: UnixFSFile | UnixFSDirectory): FileStats | DirectoryStats {
54
+ return {
55
+ type: entry.type,
56
+ cid: entry.cid,
57
+ unixfs: entry.unixfs,
58
+ mode: entry.unixfs.mode ?? (entry.unixfs.isDirectory() ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE),
59
+ mtime: entry.unixfs.mtime,
60
+ size: entry.unixfs.fileSize()
50
61
  }
62
+ }
51
63
 
52
- if (result.type === 'directory') {
53
- fileSize = 0n
54
- dagSize = BigInt(result.unixfs.marshal().byteLength)
55
- localFileSize = 0n
56
- localDagSize = dagSize
57
- blocks = 1
58
- mode = result.unixfs.mode
59
- mtime = result.unixfs.mtime
60
- unixfs = result.unixfs
64
+ async function createExtendedStats (entry: UnixFSFile | UnixFSDirectory, blockstore: GetStore & HasStore, filter: Filter, options: StatOptions): Promise<ExtendedFileStats | ExtendedDirectoryStats> {
65
+ const stats = await inspectDag(entry.cid, blockstore, false, filter, options)
66
+
67
+ return {
68
+ type: entry.type,
69
+ cid: entry.cid,
70
+ unixfs: entry.unixfs,
71
+ size: entry.unixfs.isDirectory() ? stats.dirSize : entry.unixfs.fileSize(),
72
+ mode: entry.unixfs.mode ?? (entry.unixfs.isDirectory() ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE),
73
+ mtime: entry.unixfs.mtime,
74
+ localSize: stats.localSize,
75
+ dagSize: stats.dagSize,
76
+ deduplicatedDagSize: stats.deduplicatedDagSize,
77
+ blocks: stats.blocks,
78
+ uniqueBlocks: stats.uniqueBlocks
61
79
  }
80
+ }
62
81
 
63
- if (result.type === 'file') {
64
- const results = await inspectDag(resolved.cid, blockstore, opts)
65
-
66
- fileSize = result.unixfs.fileSize()
67
- dagSize = BigInt((result.node.Data?.byteLength ?? 0) + result.node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0))
68
- localFileSize = BigInt(results.localFileSize)
69
- localDagSize = BigInt(results.localDagSize)
70
- blocks = results.blocks
71
- mode = result.unixfs.mode
72
- mtime = result.unixfs.mtime
73
- unixfs = result.unixfs
82
+ function createRawStats (entry: RawNode): RawStats {
83
+ return {
84
+ type: entry.type,
85
+ cid: entry.cid,
86
+ unixfs: undefined,
87
+ mode: DEFAULT_FILE_MODE,
88
+ mtime: undefined,
89
+ size: BigInt(entry.node.byteLength)
74
90
  }
91
+ }
75
92
 
93
+ function createExtendedRawStats (entry: RawNode): ExtendedRawStats {
76
94
  return {
77
- cid: resolved.cid,
78
- mode,
79
- mtime,
80
- fileSize,
81
- dagSize,
82
- localFileSize,
83
- localDagSize,
84
- blocks,
85
- type,
86
- unixfs
95
+ type: entry.type,
96
+ cid: entry.cid,
97
+ unixfs: undefined,
98
+ mode: DEFAULT_FILE_MODE,
99
+ mtime: undefined,
100
+ size: BigInt(entry.node.byteLength),
101
+ localSize: BigInt(entry.node.byteLength),
102
+ dagSize: BigInt(entry.node.byteLength),
103
+ deduplicatedDagSize: BigInt(entry.node.byteLength),
104
+ blocks: 1n,
105
+ uniqueBlocks: 1n
87
106
  }
88
107
  }
89
108
 
90
109
  interface InspectDagResults {
91
- localFileSize: number
92
- localDagSize: number
93
- blocks: number
110
+ dirSize: bigint
111
+ localSize: bigint
112
+ dagSize: bigint
113
+ deduplicatedDagSize: bigint
114
+ blocks: bigint
115
+ uniqueBlocks: bigint
94
116
  }
95
117
 
96
- async function inspectDag (cid: CID, blockstore: GetStore & HasStore, options: AbortOptions): Promise<InspectDagResults> {
97
- const results = {
98
- localFileSize: 0,
99
- localDagSize: 0,
100
- blocks: 0
118
+ async function inspectDag (cid: CID, blockstore: GetStore & HasStore, isFile: boolean, filter: Filter, options: StatOptions): Promise<InspectDagResults> {
119
+ const results: InspectDagResults = {
120
+ dirSize: 0n,
121
+ localSize: 0n,
122
+ dagSize: 0n,
123
+ deduplicatedDagSize: 0n,
124
+ blocks: 0n,
125
+ uniqueBlocks: 0n
101
126
  }
102
127
 
103
- if (await blockstore.has(cid, options)) {
128
+ try {
129
+ const alreadyTraversed = filter.has(cid.bytes)
130
+ filter.add(cid.bytes)
131
+
104
132
  const block = await blockstore.get(cid, options)
105
133
  results.blocks++
106
- results.localDagSize += block.byteLength
134
+ results.dagSize += BigInt(block.byteLength)
135
+
136
+ if (!alreadyTraversed) {
137
+ results.uniqueBlocks++
138
+ results.deduplicatedDagSize += BigInt(block.byteLength)
139
+ }
107
140
 
108
141
  if (cid.code === raw.code) {
109
- results.localFileSize += block.byteLength
142
+ results.localSize += BigInt(block.byteLength)
143
+
144
+ if (isFile) {
145
+ results.dirSize += BigInt(block.byteLength)
146
+ }
110
147
  } else if (cid.code === dagPb.code) {
111
148
  const pbNode = dagPb.decode(block)
112
149
 
150
+ let unixfs: UnixFS | undefined
151
+
152
+ if (pbNode.Data != null) {
153
+ unixfs = UnixFS.unmarshal(pbNode.Data)
154
+ }
155
+
113
156
  if (pbNode.Links.length > 0) {
114
157
  // intermediate node
115
158
  for (const link of pbNode.Links) {
116
- const linkResult = await inspectDag(link.Hash, blockstore, options)
159
+ const linkResult = await inspectDag(link.Hash, blockstore, linkIsFile(link, unixfs), filter, options)
117
160
 
118
- results.localFileSize += linkResult.localFileSize
119
- results.localDagSize += linkResult.localDagSize
161
+ results.localSize += linkResult.localSize
162
+ results.dagSize += linkResult.dagSize
163
+ results.deduplicatedDagSize += linkResult.deduplicatedDagSize
120
164
  results.blocks += linkResult.blocks
165
+ results.uniqueBlocks += linkResult.uniqueBlocks
166
+ results.dirSize += linkResult.dirSize
167
+ }
168
+
169
+ // multi-block file node
170
+ if (isFile && unixfs != null) {
171
+ results.dirSize += unixfs.fileSize()
121
172
  }
122
173
  } else {
123
- // leaf node
124
- if (pbNode.Data == null) {
174
+ if (unixfs == null) {
125
175
  throw new InvalidPBNodeError(`PBNode ${cid.toString()} had no data`)
126
176
  }
127
177
 
128
- const unixfs = UnixFS.unmarshal(pbNode.Data)
129
-
130
- if (unixfs.data == null) {
131
- throw new InvalidPBNodeError(`UnixFS node ${cid.toString()} had no data`)
178
+ // multi-block file leaf node
179
+ if (unixfs.data != null) {
180
+ results.localSize += BigInt(unixfs.data.byteLength ?? 0)
132
181
  }
133
182
 
134
- results.localFileSize += unixfs.data.byteLength ?? 0
183
+ // single-block file node
184
+ if (isFile) {
185
+ results.dirSize += unixfs.fileSize()
186
+ }
135
187
  }
136
188
  } else {
137
189
  throw new UnknownError(`${cid.toString()} was neither DAG_PB nor RAW`)
138
190
  }
191
+ } catch (err: any) {
192
+ if (err.name !== 'NotFoundError' || options.offline !== true) {
193
+ throw err
194
+ }
139
195
  }
140
196
 
141
197
  return results
142
198
  }
199
+
200
+ function linkIsFile (link: dagPb.PBLink, parent?: UnixFS): boolean {
201
+ if (parent == null) {
202
+ return false
203
+ }
204
+
205
+ const name = link.Name
206
+
207
+ if (name == null) {
208
+ return false
209
+ }
210
+
211
+ if (parent.type === 'directory') {
212
+ return true
213
+ } else if (parent.type === 'hamt-sharded-directory' && name.length > 2) {
214
+ return true
215
+ }
216
+
217
+ return false
218
+ }