Skip to content

Commit

Permalink
feat: support downloading car files from @helia/verified-fetch (#441)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
achingbrain and SgtPooki authored Feb 22, 2024
1 parent 54c4383 commit 703980c
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 29 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion src/utils/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
})
}
12 changes: 10 additions & 2 deletions src/verified-fetch.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<Response> {
return notSupportedResponse('vnd.ipld.car support is not implemented')
private async handleCar ({ cid, options }: FetchHandlerFunctionArg): Promise<Response> {
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
}

/**
Expand Down
27 changes: 1 addition & 26 deletions test/accept-header.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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())
})
})
69 changes: 69 additions & 0 deletions test/car.spec.ts
Original file line number Diff line number Diff line change
@@ -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"')
})
})

0 comments on commit 703980c

Please sign in to comment.