From a04e041bb86c42e04d259af80742f8ce6672ee11 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 22 Feb 2024 10:16:47 +0000 Subject: [PATCH] feat: support downloading car files from @helia/verified-fetch (#441) Adds support for the `application/vnd.ipld.car` accept header to allow downloading CAR files of DAGs. --------- Co-authored-by: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> --- packages/verified-fetch/package.json | 2 + .../verified-fetch/src/utils/responses.ts | 2 +- packages/verified-fetch/src/verified-fetch.ts | 12 +++- .../verified-fetch/test/accept-header.spec.ts | 27 +------- packages/verified-fetch/test/car.spec.ts | 69 +++++++++++++++++++ 5 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 packages/verified-fetch/test/car.spec.ts diff --git a/packages/verified-fetch/package.json b/packages/verified-fetch/package.json index fda1a6809..669964acf 100644 --- a/packages/verified-fetch/package.json +++ b/packages/verified-fetch/package.json @@ -141,6 +141,7 @@ "release": "aegir release" }, "dependencies": { + "@helia/car": "^3.0.0", "@helia/block-brokers": "^2.0.1", "@helia/http": "^1.0.1", "@helia/interface": "^4.0.0", @@ -155,6 +156,7 @@ "cborg": "^4.0.9", "hashlru": "^2.3.0", "ipfs-unixfs-exporter": "^13.5.0", + "it-to-browser-readablestream": "^2.0.6", "multiformats": "^13.1.0", "progress-events": "^1.0.0" }, diff --git a/packages/verified-fetch/src/utils/responses.ts b/packages/verified-fetch/src/utils/responses.ts index 11d906ae8..4b1afa784 100644 --- a/packages/verified-fetch/src/utils/responses.ts +++ b/packages/verified-fetch/src/utils/responses.ts @@ -17,6 +17,6 @@ export function notSupportedResponse (body?: BodyInit | null): Response { export function notAcceptableResponse (body?: BodyInit | null): Response { return new Response(body, { status: 406, - statusText: '406 Not Acceptable' + statusText: 'Not Acceptable' }) } diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 7d9a48014..8af88976a 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -1,9 +1,11 @@ +import { car } from '@helia/car' import { ipns as heliaIpns, type IPNS } from '@helia/ipns' import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs' import * as ipldDagCbor from '@ipld/dag-cbor' import * as ipldDagJson from '@ipld/dag-json' import { code as dagPbCode } from '@ipld/dag-pb' +import toBrowserReadableStream from 'it-to-browser-readablestream' import { code as jsonCode } from 'multiformats/codecs/json' import { code as rawCode } from 'multiformats/codecs/raw' import { identity } from 'multiformats/hashes/identity' @@ -134,8 +136,14 @@ export class VerifiedFetch { * Accepts a `CID` and returns a `Response` with a body stream that is a CAR * of the `DAG` referenced by the `CID`. */ - private async handleCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise { - return notSupportedResponse('vnd.ipld.car support is not implemented') + private async handleCar ({ cid, options }: FetchHandlerFunctionArg): Promise { + const c = car(this.helia) + const stream = toBrowserReadableStream(c.stream(cid, options)) + + const response = okResponse(stream) + response.headers.set('content-type', 'application/vnd.ipld.car; version=1') + + return response } /** diff --git a/packages/verified-fetch/test/accept-header.spec.ts b/packages/verified-fetch/test/accept-header.spec.ts index fa50e7e81..a71d9882a 100644 --- a/packages/verified-fetch/test/accept-header.spec.ts +++ b/packages/verified-fetch/test/accept-header.spec.ts @@ -1,4 +1,3 @@ -import { car } from '@helia/car' import { dagCbor } from '@helia/dag-cbor' import { dagJson } from '@helia/dag-json' import { ipns } from '@helia/ipns' @@ -10,7 +9,6 @@ import { expect } from 'aegir/chai' import { marshal } from 'ipns' import { VerifiedFetch } from '../src/verified-fetch.js' import { createHelia } from './fixtures/create-offline-helia.js' -import { memoryCarWriter } from './fixtures/memory-car.js' import type { Helia } from '@helia/interface' describe('accept header', () => { @@ -153,7 +151,7 @@ describe('accept header', () => { } }) expect(resp.status).to.equal(406) - expect(resp.statusText).to.equal('406 Not Acceptable') + expect(resp.statusText).to.equal('Not Acceptable') }) it('should support wildcards', async () => { @@ -248,27 +246,4 @@ describe('accept header', () => { expect(buf).to.equalBytes(marshal(record)) }) - - it.skip('should support fetching a CAR file', async () => { - const obj = { - hello: 'world' - } - const c = dagCbor(helia) - const cid = await c.add(obj) - - const ca = car(helia) - const writer = memoryCarWriter(cid) - await ca.export(cid, writer) - - const resp = await verifiedFetch.fetch(cid, { - headers: { - accept: 'application/vnd.ipld.car' - } - }) - expect(resp.status).to.equal(200) - expect(resp.headers.get('content-type')).to.equal('application/vnd.ipld.car; version=1') - const buf = await resp.arrayBuffer() - - expect(buf).to.equalBytes(await writer.bytes()) - }) }) diff --git a/packages/verified-fetch/test/car.spec.ts b/packages/verified-fetch/test/car.spec.ts new file mode 100644 index 000000000..01937dbf0 --- /dev/null +++ b/packages/verified-fetch/test/car.spec.ts @@ -0,0 +1,69 @@ +import { car } from '@helia/car' +import { dagCbor } from '@helia/dag-cbor' +import { stop } from '@libp2p/interface' +import { expect } from 'aegir/chai' +import { VerifiedFetch } from '../src/verified-fetch.js' +import { createHelia } from './fixtures/create-offline-helia.js' +import { memoryCarWriter } from './fixtures/memory-car.js' +import type { Helia } from '@helia/interface' + +describe('car files', () => { + let helia: Helia + let verifiedFetch: VerifiedFetch + + beforeEach(async () => { + helia = await createHelia() + verifiedFetch = new VerifiedFetch({ + helia + }) + }) + + afterEach(async () => { + await stop(helia, verifiedFetch) + }) + + it('should support fetching a CAR file', async () => { + const obj = { + hello: 'world' + } + const c = dagCbor(helia) + const cid = await c.add(obj) + + const ca = car(helia) + const writer = memoryCarWriter(cid) + await ca.export(cid, writer) + + const resp = await verifiedFetch.fetch(cid, { + headers: { + accept: 'application/vnd.ipld.car' + } + }) + expect(resp.status).to.equal(200) + expect(resp.headers.get('content-type')).to.equal('application/vnd.ipld.car; version=1') + expect(resp.headers.get('content-disposition')).to.equal(`attachment; filename="${cid.toString()}.car"`) + const buf = new Uint8Array(await resp.arrayBuffer()) + + expect(buf).to.equalBytes(await writer.bytes()) + }) + + it('should support specifying a filename for a CAR file', async () => { + const obj = { + hello: 'world' + } + const c = dagCbor(helia) + const cid = await c.add(obj) + + const ca = car(helia) + const writer = memoryCarWriter(cid) + await ca.export(cid, writer) + + const resp = await verifiedFetch.fetch(`ipfs://${cid}?filename=foo.bar`, { + headers: { + accept: 'application/vnd.ipld.car' + } + }) + expect(resp.status).to.equal(200) + expect(resp.headers.get('content-type')).to.equal('application/vnd.ipld.car; version=1') + expect(resp.headers.get('content-disposition')).to.equal('attachment; filename="foo.bar"') + }) +})