This repository has been archived by the owner on Feb 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
91a84e4
commit 700765b
Showing
42 changed files
with
1,418 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+104 Bytes
packages/interface-ipfs-core/test/fixtures/car/combined_naked_roots_genesis_and_128.car
Binary file not shown.
Binary file added
BIN
+35.3 KB
packages/interface-ipfs-core/test/fixtures/car/lotus_devnet_genesis.car
Binary file not shown.
Binary file added
BIN
+35.3 KB
packages/interface-ipfs-core/test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car
Binary file not shown.
Binary file added
BIN
+469 KB
packages/interface-ipfs-core/test/fixtures/car/lotus_testnet_export_128.car
Binary file not shown.
Binary file added
BIN
+1.39 MB
packages/interface-ipfs-core/test/fixtures/car/lotus_testnet_export_256_multiroot.car
Binary file not shown.
Oops, something went wrong.