Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 700765b

Browse files
rvaggachingbrain
andauthored
feat: implement dag import/export (#3728)
Adds `ipfs.dag.import` and `ipfs.dag.export` commands to import/export CAR files, e.g. single-file archives that contain blocks and root CIDs. Supersedes #2953 Fixes #2745 Co-authored-by: achingbrain <alex@achingbrain.net>
1 parent 91a84e4 commit 700765b

File tree

42 files changed

+1418
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1418
-80
lines changed

docs/core-api/DAG.md

+83-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,46 @@ _Explore the DAG API through interactive coding challenges in our ProtoSchool tu
2727
- _[P2P data links with content addressing](https://proto.school/#/basics/) (beginner)_
2828
- _[Blogging on the Decentralized Web](https://proto.school/#/blog/) (intermediate)_
2929

30+
## `ipfs.dag.export(cid, [options])`
31+
32+
> Returns a stream of Uint8Arrays that make up a [CAR file][]
33+
34+
Exports a CAR for the entire DAG available from the given root CID. The CAR will have a single
35+
root and IPFS will attempt to fetch and bundle all blocks that are linked within the connected
36+
DAG.
37+
38+
### Parameters
39+
40+
| Name | Type | Description |
41+
| ---- | ---- | ----------- |
42+
| cid | [CID][] | The root CID of the DAG we wish to export |
43+
44+
### Options
45+
46+
An optional object which may have the following keys:
47+
48+
| Name | Type | Default | Description |
49+
| ---- | ---- | ------- | ----------- |
50+
| timeout | `Number` | `undefined` | A timeout in ms |
51+
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
52+
53+
### Returns
54+
55+
| Type | Description |
56+
| -------- | -------- |
57+
| `AsyncIterable<Uint8Array>` | A stream containing the car file bytes |
58+
59+
### Example
60+
61+
```JavaScript
62+
const { Readable } = require('stream')
63+
64+
const out = await ipfs.dag.export(cid)
65+
66+
Readable.from(out).pipe(fs.createWriteStream('example.car'))
67+
```
68+
69+
A great source of [examples][] can be found in the tests for this API.
3070
## `ipfs.dag.put(dagNode, [options])`
3171

3272
> Store an IPLD format node
@@ -146,6 +186,48 @@ await getAndLog(cid, '/c/ca/1')
146186

147187
A great source of [examples][] can be found in the tests for this API.
148188

189+
## `ipfs.dag.import(source, [options])`
190+
191+
> Adds one or more [CAR file][]s full of blocks to the repo for this node
192+
193+
Import all blocks from one or more CARs and optionally recursively pin the roots identified
194+
within the CARs.
195+
196+
### Parameters
197+
198+
| Name | Type | Description |
199+
| ---- | ---- | ----------- |
200+
| sources | `AsyncIterable<Uint8Array>` | `AsyncIterable<AsyncIterable<Uint8Array>>` | One or more [CAR file][] streams |
201+
202+
### Options
203+
204+
An optional object which may have the following keys:
205+
206+
| Name | Type | Default | Description |
207+
| ---- | ---- | ------- | ----------- |
208+
| pinRoots | `boolean` | `true` | Whether to recursively pin each root to the blockstore |
209+
| timeout | `Number` | `undefined` | A timeout in ms |
210+
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
211+
212+
### Returns
213+
214+
| Type | Description |
215+
| -------- | -------- |
216+
| `AsyncIterable<{ cid: CID, pinErrorMsg?: string }>` | A stream containing the result of importing the car file(s) |
217+
218+
### Example
219+
220+
```JavaScript
221+
const fs = require('fs')
222+
223+
for await (const result of ipfs.dag.import(fs.createReadStream('./path/to/archive.car'))) {
224+
console.info(result)
225+
// Qmfoo
226+
}
227+
```
228+
229+
A great source of [examples][] can be found in the tests for this API.
230+
149231
## `ipfs.dag.tree(cid, [options])`
150232

151233
> Enumerate all the entries in a graph
@@ -262,7 +344,7 @@ console.log(result)
262344

263345
A great source of [examples][] can be found in the tests for this API.
264346

265-
266347
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/dag
267348
[cid]: https://www.npmjs.com/package/cids
268349
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
350+
[CAR file]: https://ipld.io/specs/transport/car/

examples/custom-ipfs-repo/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"@ipld/dag-cbor": "^6.0.5",
1414
"@ipld/dag-pb": "^2.1.3",
1515
"blockstore-datastore-adapter": "^1.0.0",
16-
"datastore-fs": "^5.0.1",
16+
"datastore-fs": "^5.0.2",
1717
"ipfs": "^0.55.4",
18-
"ipfs-repo": "^11.0.0",
18+
"ipfs-repo": "^11.0.1",
1919
"it-all": "^1.0.4",
2020
"multiformats": "^9.4.1"
2121
},

packages/interface-ipfs-core/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
]
3737
},
3838
"dependencies": {
39+
"@ipld/car": "^3.1.6",
3940
"@ipld/dag-cbor": "^6.0.5",
4041
"@ipld/dag-pb": "^2.1.3",
4142
"abort-controller": "^3.0.0",
@@ -56,7 +57,8 @@
5657
"it-first": "^1.0.4",
5758
"it-last": "^1.0.4",
5859
"it-map": "^1.0.4",
59-
"it-pushable": "^1.4.0",
60+
"it-pushable": "^1.4.2",
61+
"it-to-buffer": "^2.0.0",
6062
"libp2p-crypto": "^0.19.6",
6163
"libp2p-websockets": "^0.16.1",
6264
"multiaddr": "^10.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const all = require('it-all')
5+
const { getDescribe, getIt, expect } = require('../utils/mocha')
6+
const { CarReader } = require('@ipld/car')
7+
const uint8ArrayFromString = require('uint8arrays/from-string')
8+
const dagPb = require('@ipld/dag-pb')
9+
const dagCbor = require('@ipld/dag-cbor')
10+
const loadFixture = require('aegir/utils/fixtures')
11+
const toBuffer = require('it-to-buffer')
12+
13+
/** @typedef { import("ipfsd-ctl/src/factory") } Factory */
14+
/**
15+
* @param {Factory} common
16+
* @param {Object} options
17+
*/
18+
module.exports = (common, options) => {
19+
const describe = getDescribe(options)
20+
const it = getIt(options)
21+
22+
describe('.dag.export', () => {
23+
let ipfs
24+
before(async () => {
25+
ipfs = (await common.spawn()).api
26+
})
27+
28+
after(() => common.clean())
29+
30+
it('should export a car file', async () => {
31+
const child = dagPb.encode({
32+
Data: uint8ArrayFromString('block-' + Math.random()),
33+
Links: []
34+
})
35+
const childCid = await ipfs.block.put(child, {
36+
format: 'dag-pb',
37+
version: 0
38+
})
39+
const parent = dagPb.encode({
40+
Links: [{
41+
Hash: childCid,
42+
Tsize: child.length,
43+
Name: ''
44+
}]
45+
})
46+
const parentCid = await ipfs.block.put(parent, {
47+
format: 'dag-pb',
48+
version: 0
49+
})
50+
const grandParent = dagCbor.encode({
51+
parent: parentCid
52+
})
53+
const grandParentCid = await await ipfs.block.put(grandParent, {
54+
format: 'dag-cbor',
55+
version: 1
56+
})
57+
58+
const expectedCids = [
59+
grandParentCid,
60+
parentCid,
61+
childCid
62+
]
63+
64+
const reader = await CarReader.fromIterable(ipfs.dag.export(grandParentCid))
65+
const cids = await all(reader.cids())
66+
67+
expect(cids).to.deep.equal(expectedCids)
68+
})
69+
70+
it('export of shuffled devnet export identical to canonical original', async function () {
71+
this.timeout(360000)
72+
73+
const input = loadFixture('test/fixtures/car/lotus_devnet_genesis.car', 'interface-ipfs-core')
74+
const result = await all(ipfs.dag.import(async function * () { yield input }()))
75+
const exported = await toBuffer(ipfs.dag.export(result[0].root.cid))
76+
77+
expect(exported).to.equalBytes(input)
78+
})
79+
80+
it('export of shuffled testnet export identical to canonical original', async function () {
81+
this.timeout(360000)
82+
83+
const input = loadFixture('test/fixtures/car/lotus_testnet_export_128.car', 'interface-ipfs-core')
84+
const result = await all(ipfs.dag.import(async function * () { yield input }()))
85+
const exported = await toBuffer(ipfs.dag.export(result[0].root.cid))
86+
87+
expect(exported).to.equalBytes(input)
88+
})
89+
})
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const all = require('it-all')
5+
const drain = require('it-drain')
6+
const { CID } = require('multiformats/cid')
7+
const { sha256 } = require('multiformats/hashes/sha2')
8+
const { getDescribe, getIt, expect } = require('../utils/mocha')
9+
const { CarWriter, CarReader } = require('@ipld/car')
10+
const raw = require('multiformats/codecs/raw')
11+
const uint8ArrayFromString = require('uint8arrays/from-string')
12+
const loadFixture = require('aegir/utils/fixtures')
13+
14+
/**
15+
*
16+
* @param {number} num
17+
*/
18+
async function createBlocks (num) {
19+
const blocks = []
20+
21+
for (let i = 0; i < num; i++) {
22+
const bytes = uint8ArrayFromString('block-' + Math.random())
23+
const digest = await sha256.digest(raw.encode(bytes))
24+
const cid = CID.create(1, raw.code, digest)
25+
26+
blocks.push({ bytes, cid })
27+
}
28+
29+
return blocks
30+
}
31+
32+
/**
33+
* @param {{ cid: CID, bytes: Uint8Array }[]} blocks
34+
* @returns {AsyncIterable<Uint8Array>}
35+
*/
36+
async function createCar (blocks) {
37+
const rootBlock = blocks[0]
38+
const { writer, out } = await CarWriter.create([rootBlock.cid])
39+
40+
writer.put(rootBlock)
41+
.then(async () => {
42+
for (const block of blocks.slice(1)) {
43+
writer.put(block)
44+
}
45+
46+
await writer.close()
47+
})
48+
49+
return out
50+
}
51+
52+
/** @typedef { import("ipfsd-ctl/src/factory") } Factory */
53+
/**
54+
* @param {Factory} common
55+
* @param {Object} options
56+
*/
57+
module.exports = (common, options) => {
58+
const describe = getDescribe(options)
59+
const it = getIt(options)
60+
61+
describe('.dag.import', () => {
62+
let ipfs
63+
before(async () => {
64+
ipfs = (await common.spawn()).api
65+
})
66+
67+
after(() => common.clean())
68+
69+
it('should import a car file', async () => {
70+
const blocks = await createBlocks(5)
71+
const car = await createCar(blocks)
72+
73+
const result = await all(ipfs.dag.import(car))
74+
expect(result).to.have.lengthOf(1)
75+
expect(result).to.have.nested.deep.property('[0].root.cid', blocks[0].cid)
76+
77+
for (const { cid } of blocks) {
78+
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
79+
}
80+
81+
await expect(all(ipfs.pin.ls({ paths: blocks[0].cid }))).to.eventually.have.lengthOf(1)
82+
.and.have.nested.property('[0].type', 'recursive')
83+
})
84+
85+
it('should import a car file without pinning the roots', async () => {
86+
const blocks = await createBlocks(5)
87+
const car = await createCar(blocks)
88+
89+
await all(ipfs.dag.import(car, {
90+
pinRoots: false
91+
}))
92+
93+
await expect(all(ipfs.pin.ls({ paths: blocks[0].cid }))).to.eventually.be.rejectedWith(/is not pinned/)
94+
})
95+
96+
it('should import multiple car files', async () => {
97+
const blocks1 = await createBlocks(5)
98+
const car1 = await createCar(blocks1)
99+
100+
const blocks2 = await createBlocks(5)
101+
const car2 = await createCar(blocks2)
102+
103+
const result = await all(ipfs.dag.import([car1, car2]))
104+
expect(result).to.have.lengthOf(2)
105+
expect(result).to.deep.include({ root: { cid: blocks1[0].cid, pinErrorMsg: '' } })
106+
expect(result).to.deep.include({ root: { cid: blocks2[0].cid, pinErrorMsg: '' } })
107+
108+
for (const { cid } of blocks1) {
109+
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
110+
}
111+
112+
for (const { cid } of blocks2) {
113+
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
114+
}
115+
})
116+
117+
it('should import car with roots but no blocks', async () => {
118+
const input = loadFixture('test/fixtures/car/combined_naked_roots_genesis_and_128.car', 'interface-ipfs-core')
119+
const reader = await CarReader.fromBytes(input)
120+
const cids = await reader.getRoots()
121+
122+
expect(cids).to.have.lengthOf(2)
123+
124+
// naked roots car does not contain blocks
125+
const result1 = await all(ipfs.dag.import(async function * () { yield input }()))
126+
expect(result1).to.deep.include({ root: { cid: cids[0], pinErrorMsg: 'blockstore: block not found' } })
127+
expect(result1).to.deep.include({ root: { cid: cids[1], pinErrorMsg: 'blockstore: block not found' } })
128+
129+
await drain(ipfs.dag.import(async function * () { yield loadFixture('test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car', 'interface-ipfs-core') }()))
130+
131+
// have some of the blocks now, should be able to pin one root
132+
const result2 = await all(ipfs.dag.import(async function * () { yield input }()))
133+
expect(result2).to.deep.include({ root: { cid: cids[0], pinErrorMsg: '' } })
134+
expect(result2).to.deep.include({ root: { cid: cids[1], pinErrorMsg: 'blockstore: block not found' } })
135+
136+
await drain(ipfs.dag.import(async function * () { yield loadFixture('test/fixtures/car/lotus_testnet_export_128.car', 'interface-ipfs-core') }()))
137+
138+
// have all of the blocks now, should be able to pin both
139+
const result3 = await all(ipfs.dag.import(async function * () { yield input }()))
140+
expect(result3).to.deep.include({ root: { cid: cids[0], pinErrorMsg: '' } })
141+
expect(result3).to.deep.include({ root: { cid: cids[1], pinErrorMsg: '' } })
142+
})
143+
144+
it('should import lotus devnet genesis shuffled nulroot', async () => {
145+
const input = loadFixture('test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car', 'interface-ipfs-core')
146+
const reader = await CarReader.fromBytes(input)
147+
const cids = await reader.getRoots()
148+
149+
expect(cids).to.have.lengthOf(1)
150+
expect(cids[0].toString()).to.equal('bafkqaaa')
151+
152+
const result = await all(ipfs.dag.import(async function * () { yield input }()))
153+
expect(result).to.have.nested.deep.property('[0].root.cid', cids[0])
154+
})
155+
})
156+
}

packages/interface-ipfs-core/src/dag/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
const { createSuite } = require('../utils/suite')
33

44
const tests = {
5+
export: require('./export'),
56
get: require('./get'),
67
put: require('./put'),
8+
import: require('./import'),
79
resolve: require('./resolve')
810
}
911

Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)