UNPKG

@helia/verified-fetch

Version:

A fetch-like API for obtaining verified & trustless IPFS content on the web

96 lines 4.34 kB
import { BlockExporter, car, CIDPath, SubgraphExporter, UnixFSExporter } from '@helia/car'; import { CarWriter } from '@ipld/car'; import { code as dagPbCode } from '@ipld/dag-pb'; import toBrowserReadableStream from 'it-to-browser-readablestream'; import { okRangeResponse } from '../utils/responses.js'; import { BasePlugin } from './plugin-base.js'; function getFilename({ cid, ipfsPath, query }) { if (query.filename != null) { return query.filename; } // convert context.ipfsPath to a filename. replace all / with _, replace prefix protocol with empty string const filename = ipfsPath.replace(/\/ipfs\//, '').replace(/\/ipns\//, '').replace(/\//g, '_'); return `${filename}.car`; } function getDagScope({ query }) { const dagScope = query['dag-scope']; if (dagScope === 'all' || dagScope === 'entity' || dagScope === 'block') { return dagScope; } return 'all'; } /** * Accepts a `CID` and returns a `Response` with a body stream that is a CAR * of the `DAG` referenced by the `CID`. */ export class CarPlugin extends BasePlugin { id = 'car-plugin'; canHandle(context) { this.log('checking if we can handle %c with accept %s', context.cid, context.accept); if (context.byteRangeContext == null) { return false; } if (context.pathDetails == null) { return false; } return context.accept?.startsWith('application/vnd.ipld.car') === true || context.query.format === 'car'; // application/vnd.ipld.car } async handle(context) { const { options, pathDetails, cid } = context; if (pathDetails == null) { throw new Error('attempted to handle request for car with no path details'); } const { getBlockstore, helia } = this.pluginOptions; context.reqFormat = 'car'; context.query.download = true; context.query.filename = getFilename(context); const blockstore = getBlockstore(cid, context.resource, options?.session ?? true, options); const c = car({ blockstore, getCodec: helia.getCodec, logger: helia.logger }); const ipfsRootsWithoutDagRoot = pathDetails.ipfsRoots.filter(pathCid => !pathCid.equals(cid)); const carExportOptions = { ...options }; if (ipfsRootsWithoutDagRoot.length > 0) { carExportOptions.traversal = new CIDPath(ipfsRootsWithoutDagRoot); } const dagScope = getDagScope(context); // root should be the terminal element if it exists, otherwise the root cid.. because of this, we can't use the @helia/car stream() method. const root = pathDetails.terminalElement.cid ?? cid; if (dagScope === 'block') { carExportOptions.exporter = new BlockExporter(); } else if (dagScope === 'entity') { // if its unixFS, we need to enumerate a directory, or get all blocks for the entity, otherwise, use blockExporter if (root.code === dagPbCode) { carExportOptions.exporter = new UnixFSExporter(); } else { carExportOptions.exporter = new BlockExporter(); } } else { carExportOptions.exporter = new SubgraphExporter(); } const { writer, out } = CarWriter.create(root); const iter = async function* () { for await (const buf of out) { yield buf; } }; // the root passed to export should be the root CID of the DAG, not the terminal element. c.export(cid, writer, carExportOptions) .catch((err) => { this.log.error('error exporting car - %e', err); }); // export will close the writer when it's done, no finally needed. context.byteRangeContext.setBody(toBrowserReadableStream(iter())); const response = okRangeResponse(context.resource, context.byteRangeContext.getBody('application/vnd.ipld.car; version=1'), { byteRangeContext: context.byteRangeContext, log: this.log }); response.headers.set('content-type', context.byteRangeContext.getContentType() ?? 'application/vnd.ipld.car; version=1'); return response; } } //# sourceMappingURL=plugin-handle-car.js.map