Skip to content

Commit

Permalink
feat: support downloading car files from @helia/verified-fetch
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.
  • Loading branch information
achingbrain committed Feb 20, 2024
1 parent 0ad29ea commit 9a1849c
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 26 deletions.
1 change: 1 addition & 0 deletions packages/verified-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,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
25 changes: 24 additions & 1 deletion packages/verified-fetch/src/verified-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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 { CarWriter } from '@ipld/car'
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 @@ -157,7 +160,27 @@ export class VerifiedFetch {
* 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')
const c = car(this.helia)
const { writer, out } = CarWriter.create(cid)

const stream = toBrowserReadableStream<Uint8Array>(async function * () {
yield * out
}())

// write the DAG behind `cid` into the writer
c.export(cid, writer, options)
.catch(err => {
this.log.error('could not write car', err)
stream.cancel(err)
.catch(err => {
this.log.error('could not cancel stream after car export error', err)
})

Check warning on line 177 in packages/verified-fetch/src/verified-fetch.ts

View check run for this annotation

Codecov / codecov/patch

packages/verified-fetch/src/verified-fetch.ts#L173-L177

Added lines #L173 - L177 were not covered by tests
})

const response = okResponse(stream)
response.headers.set('content-type', 'application/vnd.ipld.car; version=1')

return response
}

/**
Expand Down
25 changes: 0 additions & 25 deletions packages/verified-fetch/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 @@ -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 packages/verified-fetch/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 specify 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 9a1849c

Please sign in to comment.