|
| 1 | +'use strict' |
| 2 | + |
| 3 | +const { Readable } = require('stream') |
| 4 | +const { default: parseDuration } = require('parse-duration') |
| 5 | +const LegacyCID = require('cids') |
| 6 | +const { CID } = require('multiformats/cid') |
| 7 | +const Block = require('multiformats/block') |
| 8 | +const { base58 } = require('multiformats/bases/base58') |
| 9 | +const { CarWriter } = require('@ipld/car/writer') |
| 10 | +const codecs = [ |
| 11 | + require('@ipld/dag-pb'), |
| 12 | + require('@ipld/dag-cbor'), |
| 13 | + require('@ipld/dag-json') |
| 14 | +].reduce((codecs, codec) => { |
| 15 | + codecs[codec.code] = codec |
| 16 | + return codecs |
| 17 | +}, []) |
| 18 | + |
| 19 | +// blocks that we're OK with not inspecting for links |
| 20 | +const NO_LINKS_CODECS = [ |
| 21 | + 0x51, // CBOR |
| 22 | + 0x55, // raw |
| 23 | + 0x0200 // JSON |
| 24 | +] |
| 25 | + |
| 26 | +/** |
| 27 | + * @typedef {import('ipfs-core-types').IPFS} IPFS |
| 28 | + * @typedef {import('@ipld/car/api').BlockWriter} BlockWriter |
| 29 | + */ |
| 30 | + |
| 31 | +module.exports = { |
| 32 | + command: 'export <root cid>', |
| 33 | + |
| 34 | + describe: 'Streams the DAG beginning at the given root CID as a .car stream on stdout.', |
| 35 | + |
| 36 | + builder: { |
| 37 | + timeout: { |
| 38 | + type: 'string', |
| 39 | + coerce: parseDuration |
| 40 | + } |
| 41 | + }, |
| 42 | + |
| 43 | + /** |
| 44 | + * @param {object} argv |
| 45 | + * @param {import('../../types').Context} argv.ctx |
| 46 | + * @param {string} argv.rootcid |
| 47 | + * @param {number} argv.timeout |
| 48 | + */ |
| 49 | + async handler ({ ctx: { ipfs, print }, rootcid, timeout }) { |
| 50 | + const options = { timeout } |
| 51 | + const cid = CID.parse(rootcid) |
| 52 | + |
| 53 | + const { writer, out } = await CarWriter.create([cid]) |
| 54 | + Readable.from(out).pipe(process.stdout) |
| 55 | + |
| 56 | + const complete = await traverseWrite(ipfs, options, cid, writer) |
| 57 | + writer.close() |
| 58 | + |
| 59 | + if (!complete) { |
| 60 | + print('cannot decode links in all blocks, DAG may be incomplete') |
| 61 | + } |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * @param {IPFS} ipfs |
| 67 | + * @param {{ timeout?: number}} options |
| 68 | + * @param {CID} cid |
| 69 | + * @returns {Promise<{cid:CID, bytes:Uint8Array, links:CID[]|null}>} |
| 70 | + */ |
| 71 | +const getBlock = async (ipfs, options, cid) => { |
| 72 | + cid = CID.asCID(cid) |
| 73 | + const result = await ipfs.block.get(new LegacyCID(cid.bytes), options) |
| 74 | + const bytes = result.data |
| 75 | + let links = null |
| 76 | + const codec = codecs[result.cid.code] |
| 77 | + if (codec) { |
| 78 | + const block = Block.createUnsafe({ bytes: result.data, cid: CID.asCID(result.cid), codec }) |
| 79 | + links = [...block.links()].map((l) => l[1]) |
| 80 | + } else if (NO_LINKS_CODECS.includes(result.cid.code)) { |
| 81 | + // these blocks are known not to contain any IPLD links |
| 82 | + links = [] |
| 83 | + } // else we may have a block with links that we can't decode |
| 84 | + return { cid, bytes, links } |
| 85 | +} |
| 86 | + |
| 87 | +/** |
| 88 | + * @param {IPFS} ipfs |
| 89 | + * @param {{ timeout?: number}} options |
| 90 | + * @param {CID} cid |
| 91 | + * @param {BlockWriter} writer |
| 92 | + * @param {Set<string>} seen |
| 93 | + * @returns {boolean} complete DAG |
| 94 | + */ |
| 95 | +async function traverseWrite (ipfs, options, cid, writer, seen = new Set()) { |
| 96 | + const b58Cid = cid.toString(base58) |
| 97 | + if (seen.has(b58Cid)) { |
| 98 | + return true |
| 99 | + } |
| 100 | + const block = await getBlock(ipfs, options, cid) |
| 101 | + await writer.put(block) |
| 102 | + seen.add(b58Cid) |
| 103 | + if (block.links === null) { |
| 104 | + return false // potentially incomplete DAG |
| 105 | + } |
| 106 | + |
| 107 | + // recursive traversal of all links |
| 108 | + let complete = true |
| 109 | + for (const link of block.links) { |
| 110 | + if (!await traverseWrite(ipfs, options, link, writer, seen)) { |
| 111 | + complete = false |
| 112 | + } |
| 113 | + } |
| 114 | + return complete |
| 115 | +} |
0 commit comments