11/* eslint-env mocha */
22
33import { unixfs } from '@helia/unixfs'
4+ import * as dagPb from '@ipld/dag-pb'
5+ import { multiaddr } from '@multiformats/multiaddr'
46import { expect } from 'aegir/chai'
57import { fixedSize } from 'ipfs-unixfs-importer/chunker'
68import { balanced } from 'ipfs-unixfs-importer/layout'
9+ import drain from 'it-drain'
10+ import last from 'it-last'
711import { CID } from 'multiformats/cid'
12+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
813import { createHeliaNode } from './fixtures/create-helia.js'
914import { createKuboNode } from './fixtures/create-kubo.js'
1015import type { AddOptions , UnixFS } from '@helia/unixfs'
1116import type { HeliaLibp2p } from 'helia'
12- import type { ByteStream } from 'ipfs-unixfs-importer'
17+ import type { ByteStream , ImportCandidateStream } from 'ipfs-unixfs-importer'
1318import type { KuboNode } from 'ipfsd-ctl'
1419import type { AddOptions as KuboAddOptions } from 'kubo-rpc-client'
1520
@@ -24,12 +29,32 @@ describe('@helia/unixfs - files', () => {
2429 return cid
2530 }
2631
32+ async function importDirectoryToHelia ( data : ImportCandidateStream , opts ?: Partial < AddOptions > ) : Promise < CID > {
33+ const result = await last ( unixFs . addAll ( data , opts ) )
34+
35+ if ( result == null ) {
36+ throw new Error ( 'Nothing imported' )
37+ }
38+
39+ return CID . parse ( result . cid . toString ( ) )
40+ }
41+
2742 async function importToKubo ( data : ByteStream , opts ?: KuboAddOptions ) : Promise < CID > {
2843 const result = await kubo . api . add ( data , opts )
2944
3045 return CID . parse ( result . cid . toString ( ) )
3146 }
3247
48+ async function importDirectoryToKubo ( data : ImportCandidateStream , opts ?: KuboAddOptions ) : Promise < CID > {
49+ const result = await last ( kubo . api . addAll ( data , opts ) )
50+
51+ if ( result == null ) {
52+ throw new Error ( 'Nothing imported' )
53+ }
54+
55+ return CID . parse ( result . cid . toString ( ) )
56+ }
57+
3358 async function expectSameCid ( data : ( ) => ByteStream , heliaOpts : Partial < AddOptions > = { } , kuboOpts : KuboAddOptions = { } ) : Promise < void > {
3459 const heliaCid = await importToHelia ( data ( ) , {
3560 // these are the default kubo options
@@ -85,4 +110,103 @@ describe('@helia/unixfs - files', () => {
85110
86111 await expectSameCid ( candidate )
87112 } )
113+
114+ it ( 'should return the same directory stats' , async ( ) => {
115+ const candidates = [ {
116+ path : '/foo1.txt' ,
117+ content : uint8ArrayFromString ( 'Hello World!' )
118+ } , {
119+ path : '/foo2.txt' ,
120+ content : uint8ArrayFromString ( 'Hello World!' )
121+ } ]
122+
123+ const heliaCid = await importDirectoryToHelia ( candidates , {
124+ wrapWithDirectory : true
125+ } )
126+ const kuboCid = await importDirectoryToKubo ( candidates , {
127+ cidVersion : 1 ,
128+ chunker : `size-${ 1024 * 1024 } ` ,
129+ rawLeaves : true ,
130+ wrapWithDirectory : true
131+ } )
132+
133+ expect ( heliaCid . toString ( ) ) . to . equal ( kuboCid . toString ( ) )
134+
135+ const heliaStat = await unixFs . stat ( heliaCid , {
136+ extended : true
137+ } )
138+ const kuboStat = await kubo . api . files . stat ( `/ipfs/${ kuboCid } ` , {
139+ withLocal : true
140+ } )
141+
142+ expect ( heliaStat . dagSize . toString ( ) ) . to . equal ( kuboStat . cumulativeSize . toString ( ) )
143+ expect ( heliaStat . dagSize . toString ( ) ) . to . equal ( kuboStat . sizeLocal ?. toString ( ) )
144+
145+ // +1 because kubo doesn't count the root directory block
146+ expect ( heliaStat . blocks . toString ( ) ) . to . equal ( ( kuboStat . blocks + 1 ) . toString ( ) )
147+ } )
148+
149+ it ( 'fetches missing blocks during stat' , async ( ) => {
150+ const chunkSize = 1024 * 1024
151+ const size = chunkSize * 10
152+
153+ const candidate = ( ) : ByteStream => ( async function * ( ) {
154+ for ( let i = 0 ; i < size ; i += chunkSize ) {
155+ yield new Uint8Array ( new Array ( chunkSize ) . fill ( 0 ) . map ( ( val , index ) => {
156+ return Math . floor ( Math . random ( ) * 256 )
157+ } ) )
158+ }
159+ } ( ) )
160+
161+ const largeFileCid = await importToKubo ( candidate ( ) )
162+ const info = await kubo . info ( )
163+
164+ await helia . libp2p . dial ( info . multiaddrs . map ( ma => multiaddr ( ma ) ) )
165+
166+ // pull all blocks from kubo
167+ await drain ( unixFs . cat ( largeFileCid ) )
168+
169+ // check the root block
170+ const block = await helia . blockstore . get ( largeFileCid )
171+ const node = dagPb . decode ( block )
172+
173+ expect ( node . Links ) . to . have . lengthOf ( 40 )
174+
175+ const stats = await unixFs . stat ( largeFileCid , {
176+ extended : true
177+ } )
178+
179+ expect ( stats . unixfs ?. fileSize ( ) ) . to . equal ( 10485760n )
180+ expect ( stats . blocks ) . to . equal ( 41n )
181+ expect ( stats . dagSize ) . to . equal ( 10488250n )
182+ expect ( stats . localSize ) . to . equal ( 10485760n )
183+
184+ // remove one of the blocks so we now have an incomplete DAG
185+ await helia . blockstore . delete ( node . Links [ 0 ] . Hash )
186+
187+ // block count and local file/dag sizes should be smaller
188+ const updatedStats = await unixFs . stat ( largeFileCid , {
189+ extended : true ,
190+ offline : true
191+ } )
192+
193+ expect ( updatedStats . unixfs ?. fileSize ( ) ) . to . equal ( 10485760n )
194+ expect ( updatedStats . blocks ) . to . equal ( 40n )
195+ expect ( updatedStats . dagSize ) . to . equal ( 10226092n )
196+ expect ( updatedStats . localSize ) . to . equal ( 10223616n )
197+
198+ await new Promise < void > ( ( resolve ) => {
199+ setTimeout ( ( ) => {
200+ resolve ( )
201+ } , 1_000 )
202+ } )
203+
204+ // block count and local file/dag sizes should be smaller
205+ const finalStats = await unixFs . stat ( largeFileCid , {
206+ extended : true
207+ } )
208+
209+ // should have fetched missing block from Kubo
210+ expect ( finalStats ) . to . deep . equal ( stats , 'did not fetch missing block' )
211+ } )
88212} )
0 commit comments