@helia/verified-fetch
Version:
A fetch-like API for obtaining verified & trustless IPFS content on the web
142 lines (115 loc) • 4.97 kB
text/typescript
import { BlockExporter, car, CIDPath, depthFirstWalker, naturalOrderWalker, SubgraphExporter, UnixFSExporter } from '@helia/car'
import { code as dagPbCode } from '@ipld/dag-pb'
import { createScalableCuckooFilter } from '@libp2p/utils'
import toBrowserReadableStream from 'it-to-browser-readablestream'
import { CONTENT_TYPE_CAR, MEDIA_TYPE_CAR } from '../utils/content-types.ts'
import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
import { entityBytesToOffsetAndLength } from '../utils/get-offset-and-length.ts'
import { badRequestResponse, notAcceptableResponse, okResponse } from '../utils/responses.js'
import { BasePlugin } from './plugin-base.js'
import type { PluginContext } from '../index.js'
import type { ExportCarOptions, UnixFSExporterOptions } from '@helia/car'
function getFilename (ipfsPath: string): string {
// convert context.ipfsPath to a filename. replace all / with _, replace prefix protocol with empty string
const filename = ipfsPath
.replace(/\/ipfs\//, '')
.replace(/\/ipns\//, '')
.replace(/\/+$/g, '')
.replace(/\//g, '_')
return `${filename}.car`
}
/**
* @see https://specs.ipfs.tech/http-gateways/trustless-gateway/#dag-scope-request-query-parameter
*/
type DagScope = 'all' | 'entity' | 'block'
function getDagScope ({ url }: PluginContext): DagScope | null {
const dagScope = url.searchParams.get('dag-scope')
if (dagScope === 'all' || dagScope === 'entity' || dagScope === 'block') {
return dagScope
}
// entity-bytes implies entity scope
if (url.searchParams.has('entity-bytes')) {
return 'entity'
}
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 {
readonly id = 'car-plugin'
canHandle ({ accept }: PluginContext): boolean {
return accept.some(header => header.contentType.mediaType === MEDIA_TYPE_CAR)
}
async handle (context: PluginContext): Promise<Response> {
const { options, url, accept, resource, blockstore, range, ipfsRoots, terminalElement, requestedMimeTypes } = context
if (range != null) {
return badRequestResponse(resource, new Error('Range requests are not supported for CAR files'))
}
const acceptCar = accept.filter(header => header.contentType.mediaType === MEDIA_TYPE_CAR).pop()
// we have already asserted that the CAR media type is present so this
// branch should never be hit
if (acceptCar == null) {
return badRequestResponse(resource, new Error('Could not find CAR media type in accept header'))
}
const order = acceptCar.options.order === 'dfs' ? 'dfs' : 'unk'
const duplicates = acceptCar.options.dups !== 'n'
// TODO: `@ipld/car` only supports CARv1
if (acceptCar.options.version === '2' || url.searchParams.get('car-version') === '2') {
return notAcceptableResponse(resource, requestedMimeTypes, [
CONTENT_TYPE_CAR
])
}
const helia = this.pluginOptions.helia
const c = car({
blockstore,
getCodec: helia.getCodec,
logger: helia.logger
})
const carExportOptions: ExportCarOptions = {
...options,
includeTraversalBlocks: true
}
if (!duplicates) {
carExportOptions.blockFilter = createScalableCuckooFilter(1024)
}
if (ipfsRoots.length > 1) {
carExportOptions.traversal = new CIDPath(ipfsRoots)
}
const dagScope = getDagScope(context)
const target = terminalElement.cid
if (dagScope === 'block') {
carExportOptions.exporter = new BlockExporter()
} else if (dagScope === 'entity') {
// if its unixFS, we need to enumerate a directory, or get all/some blocks
// for the entity, otherwise, use blockExporter
if (target.code === dagPbCode) {
const options: UnixFSExporterOptions = {
listingOnly: true
}
const slice = entityBytesToOffsetAndLength(terminalElement.size, url.searchParams.get('entity-bytes'))
options.offset = slice.offset
options.length = slice.length
carExportOptions.exporter = new UnixFSExporter(options)
} else {
carExportOptions.exporter = new BlockExporter()
}
} else {
carExportOptions.exporter = new SubgraphExporter({
walker: order === 'dfs' ? depthFirstWalker() : naturalOrderWalker()
})
}
const stream = toBrowserReadableStream(c.export(target, carExportOptions))
return okResponse(resource, stream, {
headers: {
'content-type': `${MEDIA_TYPE_CAR}; version=1; order=${order}; dups=${duplicates ? 'y' : 'n'}`,
'content-disposition': `attachment; ${
getContentDispositionFilename(url.searchParams.get('filename') ?? getFilename(`/ipfs/${url.hostname}${url.pathname}`))
}`,
'x-content-type-options': 'nosniff',
'accept-ranges': 'none'
}
})
}
}