Skip to content

Commit 1862165

Browse files
author
Alan Shaw
authored
feat: implement block order signaling (#25)
This PR updates dagula and implements block order signaling via HTTP Accept header as specified in ipfs/specs#412. It also renames the query param `car-scope` to `dag-scope` and changes the scope value `file` to `entity` as these were updated in ipfs/specs#402.
1 parent b0760cc commit 1862165

File tree

4 files changed

+303
-24
lines changed

4 files changed

+303
-24
lines changed

package-lock.json

+153-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"@web3-storage/handlebars": "^1.0.0",
6565
"bytes": "^3.1.2",
6666
"chardet": "^1.5.0",
67-
"dagula": "^6.0.2",
67+
"dagula": "^7.0.0",
6868
"magic-bytes.js": "^1.0.12",
6969
"mrmime": "^1.0.1",
7070
"multiformats": "^11.0.1",

src/handlers/car.js

+49-9
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@ import { HttpError } from '../util/errors.js'
66
/**
77
* @typedef {import('../bindings').IpfsUrlContext & import('../bindings').DagulaContext & { timeoutController?: import('../bindings').TimeoutControllerContext['timeoutController'] }} CarHandlerContext
88
* @typedef {import('multiformats').CID} CID
9+
* @typedef {{ version: 1|2, order: import('dagula').BlockOrder, dups: boolean }} CarParams
910
*/
1011

12+
/** @type {CarParams} */
13+
const DefaultCarParams = { version: 1, order: 'unk', dups: true }
14+
1115
/** @type {import('../bindings').Handler<CarHandlerContext>} */
1216
export async function handleCar (request, env, ctx) {
1317
const { dataCid, path, timeoutController: controller, dagula, searchParams } = ctx
1418
if (!dataCid) throw new Error('missing IPFS path')
1519
if (path == null) throw new Error('missing URL path')
1620
if (!dagula) throw new Error('missing dagula instance')
1721

18-
const carScope = getCarScope(searchParams)
22+
const dagScope = getDagScope(searchParams)
23+
const { version, order, dups } = getAcceptParams(request.headers)
1924

2025
// Use root CID for etag even tho we may resolve a different root for the terminus of the path
2126
// as etags are only relevant per path. If the caller has an etag for this path already, and
@@ -34,7 +39,8 @@ export async function handleCar (request, env, ctx) {
3439
const { writer, out } = CarWriter.create(dataCid)
3540
;(async () => {
3641
try {
37-
for await (const block of dagula.getPath(`${dataCid}${path}`, { carScope, signal: controller?.signal })) {
42+
for await (const block of dagula.getPath(`${dataCid}${path}`, { dagScope, order, signal: controller?.signal })) {
43+
// @ts-expect-error
3844
await writer.put(block)
3945
}
4046
} catch (/** @type {any} */ err) {
@@ -52,7 +58,7 @@ export async function handleCar (request, env, ctx) {
5258
const headers = {
5359
// Make it clear we don't support range-requests over a car stream
5460
'Accept-Ranges': 'none',
55-
'Content-Type': 'application/vnd.ipld.car; version=1',
61+
'Content-Type': `application/vnd.ipld.car; version=${version}; order=${order}; dups=${dups ? 'y' : 'n'}`,
5662
'X-Content-Type-Options': 'nosniff',
5763
Etag: etag,
5864
'Cache-Control': 'public, max-age=29030400, immutable',
@@ -62,11 +68,45 @@ export async function handleCar (request, env, ctx) {
6268
return new Response(toReadableStream(out), { headers })
6369
}
6470

65-
/** @param {URLSearchParams} searchParams */
66-
function getCarScope (searchParams) {
67-
const carScope = searchParams.get('car-scope') ?? 'all'
68-
if (carScope === 'all' || carScope === 'file' || carScope === 'block') {
69-
return carScope
71+
/**
72+
* @param {URLSearchParams} searchParams
73+
* @returns {import('dagula').DagScope}
74+
*/
75+
function getDagScope (searchParams) {
76+
const scope = searchParams.get('dag-scope') ?? 'all'
77+
if (scope === 'all' || scope === 'entity' || scope === 'block') {
78+
return scope
79+
}
80+
throw new HttpError(`unsupported dag-scope: ${scope}`, { status: 400 })
81+
}
82+
83+
/**
84+
* @param {Headers} headers
85+
* @returns {CarParams}
86+
*/
87+
function getAcceptParams (headers) {
88+
const accept = headers.get('accept')
89+
if (!accept) return DefaultCarParams
90+
91+
const types = accept.split(',').map(s => s.trim())
92+
const carType = types.find(t => t.startsWith('application/vnd.ipld.car'))
93+
if (!carType) return DefaultCarParams
94+
95+
const paramPairs = carType.split(';').slice(1).map(s => s.trim())
96+
const { version, order, dups } = Object.fromEntries(paramPairs.map(p => p.split('=').map(s => s.trim())))
97+
98+
// only CARv1
99+
if (version != null && version !== '1') {
100+
throw new HttpError(`unsupported accept parameter: version=${version}`, { status: 400 })
70101
}
71-
throw new HttpError(`unsupported car-scope: ${carScope}`, { status: 400 })
102+
// only yes duplicates
103+
if (dups && dups !== 'y') {
104+
throw new HttpError(`unsupported accept parameter: dups=${dups}`, { status: 400 })
105+
}
106+
// only dfs or unk ordering
107+
if (order && order !== 'dfs' && order !== 'unk') {
108+
throw new HttpError(`unsupported accept parameter: order=${order}`, { status: 400 })
109+
}
110+
111+
return { version: 1, order, dups: true }
72112
}

0 commit comments

Comments
 (0)