|
| 1 | +'use strict' |
| 2 | + |
| 3 | +const fs = require('fs') |
| 4 | +const { CarBlockIterator } = require('@ipld/car/iterator') |
| 5 | +const Block = require('ipld-block') |
| 6 | +const LegacyCID = require('cids') |
| 7 | +const { default: parseDuration } = require('parse-duration') |
| 8 | +const { cidToString } = require('ipfs-core-utils/src/cid') |
| 9 | + |
| 10 | +/** |
| 11 | + * @typedef {import('ipfs-core-types').IPFS} IPFS |
| 12 | + * @typedef {import('multiformats/cid').CID} CID |
| 13 | + * @typedef {[CID, boolean][]} RootsStatus |
| 14 | + */ |
| 15 | + |
| 16 | +module.exports = { |
| 17 | + command: 'import [path...]', |
| 18 | + |
| 19 | + describe: 'Import the contents of one or more CARs from files or stdin', |
| 20 | + |
| 21 | + builder: { |
| 22 | + 'pin-roots': { |
| 23 | + type: 'boolean', |
| 24 | + default: true, |
| 25 | + describe: 'Pin optional roots listed in the CAR headers after importing.' |
| 26 | + }, |
| 27 | + timeout: { |
| 28 | + type: 'string', |
| 29 | + coerce: parseDuration |
| 30 | + } |
| 31 | + }, |
| 32 | + |
| 33 | + /** |
| 34 | + * @param {object} argv |
| 35 | + * @param {import('../../types').Context} argv.ctx |
| 36 | + * @param {string[]} argv.path |
| 37 | + * @param {boolean} argv.pinRoots |
| 38 | + * @param {number} argv.timeout |
| 39 | + */ |
| 40 | + async handler ({ ctx: { ipfs, print, getStdin }, path, pinRoots, timeout }) { |
| 41 | + let count = 0 |
| 42 | + /** @type {RootsStatus} */ |
| 43 | + let pinRootStatus = [] |
| 44 | + if (path) { // files |
| 45 | + for await (const file of path) { |
| 46 | + print(`importing from ${file}...`) |
| 47 | + const { rootStatus, blockCount } = await importCar(ipfs, fs.createReadStream(file), timeout) |
| 48 | + pinRootStatus = pinRootStatus.concat(rootStatus) |
| 49 | + count += blockCount |
| 50 | + } |
| 51 | + } else { // stdin |
| 52 | + print('importing CAR from stdin...') |
| 53 | + const { rootStatus, blockCount } = await importCar(ipfs, getStdin(), timeout) |
| 54 | + pinRootStatus = pinRootStatus.concat(rootStatus) |
| 55 | + count += blockCount |
| 56 | + } |
| 57 | + |
| 58 | + print(`imported ${count} blocks`) |
| 59 | + |
| 60 | + if (pinRoots) { |
| 61 | + for (const [cid, status] of pinRootStatus) { |
| 62 | + if (!status) { |
| 63 | + print(`got malformed CAR, not pinning nonexistent root ${cid.toString()}`) |
| 64 | + } |
| 65 | + } |
| 66 | + const pinCids = pinRootStatus |
| 67 | + .filter(([_, status]) => status) |
| 68 | + .map(([cid]) => ({ cid: new LegacyCID(cid.bytes) })) |
| 69 | + for await (const cid of ipfs.pin.addAll(pinCids)) { |
| 70 | + print(`pinned root ${cidToString(cid)}`) |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +/** |
| 77 | + * @param {IPFS} ipfs |
| 78 | + * @param {AsyncIterable<Uint8Array>} inStream |
| 79 | + * @param {number} timeout |
| 80 | + * @returns {Promise<{rootStatus: RootsStatus, blockCount: number}>} |
| 81 | + */ |
| 82 | +async function importCar (ipfs, inStream, timeout) { |
| 83 | + const reader = await CarBlockIterator.fromIterable(inStream) |
| 84 | + // keep track of whether the root(s) exist within the CAR or not for later reporting & pinning |
| 85 | + /** @type {RootsStatus} */ |
| 86 | + const rootStatus = (await reader.getRoots()).map((/** @type {CID} */ root) => [root, false]) |
| 87 | + let blockCount = 0 |
| 88 | + for await (const { cid, bytes } of reader) { |
| 89 | + rootStatus.forEach((rootStatus) => { |
| 90 | + if (!rootStatus[1] && cid.equals(rootStatus[0])) { |
| 91 | + rootStatus[1] = true // the root points to a CID in the CAR |
| 92 | + } |
| 93 | + }) |
| 94 | + const block = new Block(bytes, new LegacyCID(cid.bytes)) |
| 95 | + await ipfs.block.put(block, { timeout }) |
| 96 | + blockCount++ |
| 97 | + } |
| 98 | + return { rootStatus, blockCount } |
| 99 | +} |
0 commit comments