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

feat: implement dag import/export #3728

Merged
merged 20 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0bb4104
feat: implement `ipfs dag export <root>`
rvagg Jun 28, 2021
de2ab6e
fix: types, make tests pass, don't allow partial DAG
rvagg Jun 28, 2021
cf772c9
feat: implement `ipfs dag import [path...]`
rvagg Jun 28, 2021
d0eeaf1
chore: migrate `dag export` from ipfs-cli to ipfs-core
rvagg Jun 30, 2021
8d30a68
feat: `dag/export` for http-{client,server}
rvagg Jul 1, 2021
ff6c992
feat: `dag import` in core, cli, http client and http server
rvagg Jul 2, 2021
e863378
Merge remote-tracking branch 'origin/master' into rvagg/import-export
achingbrain Jul 19, 2021
133e39e
chore: fix linting and types
achingbrain Jul 19, 2021
3a0eb1e
chore: remove unused deps
achingbrain Jul 20, 2021
58f41bf
chore: add tests, back out block count until common approach is agree…
achingbrain Jul 20, 2021
95c79d0
chore: skip dag import/export tests for message port client
achingbrain Jul 20, 2021
d1c0bc3
chore: add docs
achingbrain Jul 20, 2021
be3cb5c
chore: add fixture car files and start porting sharness test cases
achingbrain Jul 23, 2021
64e8c4a
chore: fix tests
achingbrain Jul 24, 2021
08f5a2e
chore: add more interface tests and unit tests for cli and http api
achingbrain Jul 26, 2021
1614c8f
chore: fix failing test
achingbrain Jul 26, 2021
668d4b3
chore: remove dag-cbor from codecs we do not follow links for as we d…
achingbrain Jul 26, 2021
f844ad4
chore: output order is not guarenteed
achingbrain Jul 26, 2021
dad6878
chore: fix tests
achingbrain Jul 26, 2021
7ba22a2
chore: revert streaming response refactor
achingbrain Jul 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion docs/core-api/DAG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ _Explore the DAG API through interactive coding challenges in our ProtoSchool tu
- _[P2P data links with content addressing](https://proto.school/#/basics/) (beginner)_
- _[Blogging on the Decentralized Web](https://proto.school/#/blog/) (intermediate)_

## `ipfs.dag.export(cid, [options])`

> Returns a stream of Uint8Arrays that make up a [CAR file][]

Exports a CAR for the entire DAG available from the given root CID. The CAR will have a single
root and IPFS will attempt to fetch and bundle all blocks that are linked within the connected
DAG.

### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The root CID of the DAG we wish to export |

### Options

An optional object which may have the following keys:

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |

### Returns

| Type | Description |
| -------- | -------- |
| `AsyncIterable<Uint8Array>` | A stream containing the car file bytes |

### Example

```JavaScript
const { Readable } = require('stream')

const out = await ipfs.dag.export(cid)

Readable.from(out).pipe(fs.createWriteStream('example.car'))
```

A great source of [examples][] can be found in the tests for this API.
## `ipfs.dag.put(dagNode, [options])`

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

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

## `ipfs.dag.import(source, [options])`

> Adds one or more [CAR file][]s full of blocks to the repo for this node

Import all blocks from one or more CARs and optionally recursively pin the roots identified
within the CARs.

### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| sources | `AsyncIterable<Uint8Array>` | `AsyncIterable<AsyncIterable<Uint8Array>>` | One or more [CAR file][] streams |

### Options

An optional object which may have the following keys:

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| pinRoots | `boolean` | `true` | Whether to recursively pin each root to the blockstore |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |

### Returns

| Type | Description |
| -------- | -------- |
| `AsyncIterable<{ cid: CID, pinErrorMsg?: string }>` | A stream containing the result of importing the car file(s) |

### Example

```JavaScript
const fs = require('fs')

for await (const result of ipfs.dag.import(fs.createReadStream('./path/to/archive.car'))) {
console.info(result)
// Qmfoo
}
```

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

## `ipfs.dag.tree(cid, [options])`

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

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


[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/dag
[cid]: https://www.npmjs.com/package/cids
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[CAR file]: https://ipld.io/specs/transport/car/
4 changes: 2 additions & 2 deletions examples/custom-ipfs-repo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"@ipld/dag-cbor": "^6.0.5",
"@ipld/dag-pb": "^2.1.3",
"blockstore-datastore-adapter": "^1.0.0",
"datastore-fs": "^5.0.1",
"datastore-fs": "^5.0.2",
"ipfs": "^0.55.4",
"ipfs-repo": "^11.0.0",
"ipfs-repo": "^11.0.1",
"it-all": "^1.0.4",
"multiformats": "^9.4.1"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/interface-ipfs-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
]
},
"dependencies": {
"@ipld/car": "^3.1.6",
"@ipld/dag-cbor": "^6.0.5",
"@ipld/dag-pb": "^2.1.3",
"abort-controller": "^3.0.0",
Expand All @@ -56,7 +57,8 @@
"it-first": "^1.0.4",
"it-last": "^1.0.4",
"it-map": "^1.0.4",
"it-pushable": "^1.4.0",
"it-pushable": "^1.4.2",
"it-to-buffer": "^2.0.0",
"libp2p-crypto": "^0.19.6",
"libp2p-websockets": "^0.16.1",
"multiaddr": "^10.0.0",
Expand Down
90 changes: 90 additions & 0 deletions packages/interface-ipfs-core/src/dag/export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-env mocha */
'use strict'

const all = require('it-all')
const { getDescribe, getIt, expect } = require('../utils/mocha')
const { CarReader } = require('@ipld/car')
const uint8ArrayFromString = require('uint8arrays/from-string')
const dagPb = require('@ipld/dag-pb')
const dagCbor = require('@ipld/dag-cbor')
const loadFixture = require('aegir/utils/fixtures')
const toBuffer = require('it-to-buffer')

/** @typedef { import("ipfsd-ctl/src/factory") } Factory */
/**
* @param {Factory} common
* @param {Object} options
*/
module.exports = (common, options) => {
const describe = getDescribe(options)
const it = getIt(options)

describe('.dag.export', () => {
let ipfs
before(async () => {
ipfs = (await common.spawn()).api
})

after(() => common.clean())

it('should export a car file', async () => {
const child = dagPb.encode({
Data: uint8ArrayFromString('block-' + Math.random()),
Links: []
})
const childCid = await ipfs.block.put(child, {
format: 'dag-pb',
version: 0
})
const parent = dagPb.encode({
Links: [{
Hash: childCid,
Tsize: child.length,
Name: ''
}]
})
const parentCid = await ipfs.block.put(parent, {
format: 'dag-pb',
version: 0
})
const grandParent = dagCbor.encode({
parent: parentCid
})
const grandParentCid = await await ipfs.block.put(grandParent, {
format: 'dag-cbor',
version: 1
})

const expectedCids = [
grandParentCid,
parentCid,
childCid
]

const reader = await CarReader.fromIterable(ipfs.dag.export(grandParentCid))
const cids = await all(reader.cids())

expect(cids).to.deep.equal(expectedCids)
})

it('export of shuffled devnet export identical to canonical original', async function () {
this.timeout(360000)

const input = loadFixture('test/fixtures/car/lotus_devnet_genesis.car', 'interface-ipfs-core')
const result = await all(ipfs.dag.import(async function * () { yield input }()))
const exported = await toBuffer(ipfs.dag.export(result[0].root.cid))

expect(exported).to.equalBytes(input)
})

it('export of shuffled testnet export identical to canonical original', async function () {
this.timeout(360000)

const input = loadFixture('test/fixtures/car/lotus_testnet_export_128.car', 'interface-ipfs-core')
const result = await all(ipfs.dag.import(async function * () { yield input }()))
const exported = await toBuffer(ipfs.dag.export(result[0].root.cid))

expect(exported).to.equalBytes(input)
})
})
}
156 changes: 156 additions & 0 deletions packages/interface-ipfs-core/src/dag/import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* eslint-env mocha */
'use strict'

const all = require('it-all')
const drain = require('it-drain')
const { CID } = require('multiformats/cid')
const { sha256 } = require('multiformats/hashes/sha2')
const { getDescribe, getIt, expect } = require('../utils/mocha')
const { CarWriter, CarReader } = require('@ipld/car')
const raw = require('multiformats/codecs/raw')
const uint8ArrayFromString = require('uint8arrays/from-string')
const loadFixture = require('aegir/utils/fixtures')

/**
*
* @param {number} num
*/
async function createBlocks (num) {
const blocks = []

for (let i = 0; i < num; i++) {
const bytes = uint8ArrayFromString('block-' + Math.random())
const digest = await sha256.digest(raw.encode(bytes))
const cid = CID.create(1, raw.code, digest)

blocks.push({ bytes, cid })
}

return blocks
}

/**
* @param {{ cid: CID, bytes: Uint8Array }[]} blocks
* @returns {AsyncIterable<Uint8Array>}
*/
async function createCar (blocks) {
const rootBlock = blocks[0]
const { writer, out } = await CarWriter.create([rootBlock.cid])

writer.put(rootBlock)
.then(async () => {
for (const block of blocks.slice(1)) {
writer.put(block)
}

await writer.close()
})

return out
}

/** @typedef { import("ipfsd-ctl/src/factory") } Factory */
/**
* @param {Factory} common
* @param {Object} options
*/
module.exports = (common, options) => {
const describe = getDescribe(options)
const it = getIt(options)

describe('.dag.import', () => {
let ipfs
before(async () => {
ipfs = (await common.spawn()).api
})

after(() => common.clean())

it('should import a car file', async () => {
const blocks = await createBlocks(5)
const car = await createCar(blocks)

const result = await all(ipfs.dag.import(car))
expect(result).to.have.lengthOf(1)
expect(result).to.have.nested.deep.property('[0].root.cid', blocks[0].cid)

for (const { cid } of blocks) {
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
}

await expect(all(ipfs.pin.ls({ paths: blocks[0].cid }))).to.eventually.have.lengthOf(1)
.and.have.nested.property('[0].type', 'recursive')
})

it('should import a car file without pinning the roots', async () => {
const blocks = await createBlocks(5)
const car = await createCar(blocks)

await all(ipfs.dag.import(car, {
pinRoots: false
}))

await expect(all(ipfs.pin.ls({ paths: blocks[0].cid }))).to.eventually.be.rejectedWith(/is not pinned/)
})

it('should import multiple car files', async () => {
const blocks1 = await createBlocks(5)
const car1 = await createCar(blocks1)

const blocks2 = await createBlocks(5)
const car2 = await createCar(blocks2)

const result = await all(ipfs.dag.import([car1, car2]))
expect(result).to.have.lengthOf(2)
expect(result).to.deep.include({ root: { cid: blocks1[0].cid, pinErrorMsg: '' } })
expect(result).to.deep.include({ root: { cid: blocks2[0].cid, pinErrorMsg: '' } })

for (const { cid } of blocks1) {
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
}

for (const { cid } of blocks2) {
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
}
})

it('should import car with roots but no blocks', async () => {
const input = loadFixture('test/fixtures/car/combined_naked_roots_genesis_and_128.car', 'interface-ipfs-core')
const reader = await CarReader.fromBytes(input)
const cids = await reader.getRoots()

expect(cids).to.have.lengthOf(2)

// naked roots car does not contain blocks
const result1 = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result1).to.deep.include({ root: { cid: cids[0], pinErrorMsg: 'blockstore: block not found' } })
expect(result1).to.deep.include({ root: { cid: cids[1], pinErrorMsg: 'blockstore: block not found' } })

await drain(ipfs.dag.import(async function * () { yield loadFixture('test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car', 'interface-ipfs-core') }()))

// have some of the blocks now, should be able to pin one root
const result2 = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result2).to.deep.include({ root: { cid: cids[0], pinErrorMsg: '' } })
expect(result2).to.deep.include({ root: { cid: cids[1], pinErrorMsg: 'blockstore: block not found' } })

await drain(ipfs.dag.import(async function * () { yield loadFixture('test/fixtures/car/lotus_testnet_export_128.car', 'interface-ipfs-core') }()))

// have all of the blocks now, should be able to pin both
const result3 = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result3).to.deep.include({ root: { cid: cids[0], pinErrorMsg: '' } })
expect(result3).to.deep.include({ root: { cid: cids[1], pinErrorMsg: '' } })
})

it('should import lotus devnet genesis shuffled nulroot', async () => {
const input = loadFixture('test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car', 'interface-ipfs-core')
const reader = await CarReader.fromBytes(input)
const cids = await reader.getRoots()

expect(cids).to.have.lengthOf(1)
expect(cids[0].toString()).to.equal('bafkqaaa')

const result = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result).to.have.nested.deep.property('[0].root.cid', cids[0])
})
})
}
2 changes: 2 additions & 0 deletions packages/interface-ipfs-core/src/dag/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
const { createSuite } = require('../utils/suite')

const tests = {
export: require('./export'),
get: require('./get'),
put: require('./put'),
import: require('./import'),
resolve: require('./resolve')
}

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading