UNPKG

ipfs-core

Version:

JavaScript implementation of the IPFS specification

301 lines (256 loc) 7.93 kB
import * as dagPB from '@ipld/dag-pb' import { notFoundError } from 'datastore-core/errors' import { toCidAndPath } from 'ipfs-core-utils/to-cid-and-path' import { CID } from 'multiformats/cid' import { TimeoutController } from 'timeout-abort-controller' import { anySignal } from 'any-signal' const ERR_NOT_FOUND = notFoundError().code export const Format = { default: '<dst>', edges: '<src> -> <dst>' } /** * @typedef {object} Node * @property {string} [name] * @property {CID} cid * * @typedef {object} TraversalResult * @property {Node} parent * @property {Node} node * @property {boolean} isDuplicate * * @typedef {import('ipfs-core-types/src/utils').AbortOptions} AbortOptions */ /** * @param {object} config * @param {import('ipfs-repo').IPFSRepo} config.repo * @param {import('ipfs-core-utils/multicodecs').Multicodecs} config.codecs * @param {import('ipfs-core-types/src/root').API<{}>["resolve"]} config.resolve * @param {import('../../types').Preload} config.preload */ export function createRefs ({ repo, codecs, resolve, preload }) { /** * @type {import('ipfs-core-types/src/refs').API<{}>["refs"]} */ async function * refs (ipfsPath, options = {}) { if (options.maxDepth === 0) { return } if (options.edges && options.format && options.format !== Format.default) { throw new Error('Cannot set edges to true and also specify format') } options.format = options.edges ? Format.edges : options.format if (typeof options.maxDepth !== 'number') { options.maxDepth = options.recursive ? Infinity : 1 } if (options.timeout) { const controller = new TimeoutController(options.timeout) const signals = [controller.signal] if (options.signal) { signals.push(options.signal) } options.signal = anySignal(signals) } /** @type {(string|CID)[]} */ const rawPaths = Array.isArray(ipfsPath) ? ipfsPath : [ipfsPath] const paths = rawPaths.map(p => getFullPath(preload, p, options)) for (const path of paths) { try { yield * refsStream(resolve, repo, codecs, path, options) } catch (/** @type {any} */ err) { yield { ref: '', err: err.message } } } } return refs } /** * @param {import('../../types').Preload} preload * @param {string | CID} ipfsPath * @param {import('ipfs-core-types/src/refs').RefsOptions} options */ function getFullPath (preload, ipfsPath, options) { const { cid, path } = toCidAndPath(ipfsPath) if (options.preload !== false) { preload(cid) } return `/ipfs/${cid}${path || ''}` } /** * Get a stream of refs at the given path * * @param {import('ipfs-core-types/src/root').API<{}>["resolve"]} resolve * @param {import('ipfs-repo').IPFSRepo} repo * @param {import('ipfs-core-utils/multicodecs').Multicodecs} codecs * @param {string} path * @param {import('ipfs-core-types/src/refs').RefsOptions} options */ async function * refsStream (resolve, repo, codecs, path, options) { // Resolve to the target CID of the path const resPath = await resolve(path, options) const { cid } = toCidAndPath(resPath) const maxDepth = options.maxDepth != null ? options.maxDepth : Infinity const unique = options.unique || false // Traverse the DAG, converting it into a stream for await (const obj of objectStream(repo, codecs, cid, maxDepth, unique, options)) { // Root object will not have a parent if (!obj.parent) { continue } // Filter out duplicates (isDuplicate flag is only set if options.unique is set) if (obj.isDuplicate) { continue } // Format the links // Clients expect refs to be in the format { ref: <ref> } yield { ref: formatLink(obj.parent.cid, obj.node.cid, obj.node.name, options.format) } } } /** * Get formatted link * * @param {CID} srcCid * @param {CID} dstCid * @param {string} [linkName] * @param {string} [format] */ function formatLink (srcCid, dstCid, linkName = '', format = Format.default) { let out = format.replace(/<src>/g, srcCid.toString()) out = out.replace(/<dst>/g, dstCid.toString()) out = out.replace(/<linkname>/g, linkName) return out } /** * Do a depth first search of the DAG, starting from the given root cid * * @param {import('ipfs-repo').IPFSRepo} repo * @param {import('ipfs-core-utils/multicodecs').Multicodecs} codecs * @param {CID} rootCid * @param {number} maxDepth * @param {boolean} uniqueOnly * @param {AbortOptions} options */ async function * objectStream (repo, codecs, rootCid, maxDepth, uniqueOnly, options) { // eslint-disable-line require-await const seen = new Set() /** * @param {Node} parent * @param {number} depth * @returns {AsyncGenerator<TraversalResult, void, undefined>} */ async function * traverseLevel (parent, depth) { const nextLevelDepth = depth + 1 // Check the depth if (nextLevelDepth > maxDepth) { return } // Get this object's links try { // Look at each link, parent and the new depth for await (const link of getLinks(repo, codecs, parent.cid, options)) { yield { parent: parent, node: link, isDuplicate: uniqueOnly && seen.has(link.cid.toString()) } if (uniqueOnly) { seen.add(link.cid.toString()) } yield * traverseLevel(link, nextLevelDepth) } } catch (/** @type {any} */ err) { if (err.code === ERR_NOT_FOUND) { err.message = `Could not find object with CID: ${parent.cid}` } throw err } } yield * traverseLevel({ cid: rootCid }, 0) } /** * Fetch a node and then get all its links * * @param {import('ipfs-repo').IPFSRepo} repo * @param {import('ipfs-core-utils/multicodecs').Multicodecs} codecs * @param {CID} cid * @param {AbortOptions} options * @returns {AsyncGenerator<{ name: string, cid: CID }, void, undefined>} */ async function * getLinks (repo, codecs, cid, options) { const block = await repo.blocks.get(cid, options) const codec = await codecs.getCodec(cid.code) const value = codec.decode(block) const isDagPb = cid.code === dagPB.code /** @type {Array<string|number>} */ const base = [] for (const [name, cid] of links(value, base)) { // special case for dag-pb - use the name of the link // instead of the path within the object if (isDagPb) { const match = name.match(/^Links\/(\d+)\/Hash$/) if (match) { const index = Number(match[1]) if (index < value.Links.length) { yield { name: value.Links[index].Name, cid } continue } } } yield { name, cid } } } /** * @param {*} source * @param {Array<string|number>} base * @returns {Iterable<[string, CID]>} */ const links = function * (source, base) { if (source == null) { return } if (source instanceof Uint8Array) { return } for (const [key, value] of Object.entries(source)) { const path = [...base, key] if (value != null && typeof value === 'object') { if (Array.isArray(value)) { for (const [index, element] of value.entries()) { const elementPath = [...path, index] const cid = CID.asCID(element) // eslint-disable-next-line max-depth if (cid) { yield [elementPath.join('/'), cid] } else if (typeof element === 'object') { yield * links(element, elementPath) } } } else { const cid = CID.asCID(value) if (cid) { yield [path.join('/'), cid] } else { yield * links(value, path) } } } } // ts requires a @returns annotation when a function is recursive, // eslint requires a return when you use a @returns annotation. return [] }