Skip to content

Commit

Permalink
feat: implement block order signaling (#25)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alanshaw committed May 19, 2023
1 parent b0760cc commit 1862165
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 24 deletions.
167 changes: 153 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@web3-storage/handlebars": "^1.0.0",
"bytes": "^3.1.2",
"chardet": "^1.5.0",
"dagula": "^6.0.2",
"dagula": "^7.0.0",
"magic-bytes.js": "^1.0.12",
"mrmime": "^1.0.1",
"multiformats": "^11.0.1",
Expand Down
58 changes: 49 additions & 9 deletions src/handlers/car.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import { HttpError } from '../util/errors.js'
/**
* @typedef {import('../bindings').IpfsUrlContext & import('../bindings').DagulaContext & { timeoutController?: import('../bindings').TimeoutControllerContext['timeoutController'] }} CarHandlerContext
* @typedef {import('multiformats').CID} CID
* @typedef {{ version: 1|2, order: import('dagula').BlockOrder, dups: boolean }} CarParams
*/

/** @type {CarParams} */
const DefaultCarParams = { version: 1, order: 'unk', dups: true }

/** @type {import('../bindings').Handler<CarHandlerContext>} */
export async function handleCar (request, env, ctx) {
const { dataCid, path, timeoutController: controller, dagula, searchParams } = ctx
if (!dataCid) throw new Error('missing IPFS path')
if (path == null) throw new Error('missing URL path')
if (!dagula) throw new Error('missing dagula instance')

const carScope = getCarScope(searchParams)
const dagScope = getDagScope(searchParams)
const { version, order, dups } = getAcceptParams(request.headers)

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

/** @param {URLSearchParams} searchParams */
function getCarScope (searchParams) {
const carScope = searchParams.get('car-scope') ?? 'all'
if (carScope === 'all' || carScope === 'file' || carScope === 'block') {
return carScope
/**
* @param {URLSearchParams} searchParams
* @returns {import('dagula').DagScope}
*/
function getDagScope (searchParams) {
const scope = searchParams.get('dag-scope') ?? 'all'
if (scope === 'all' || scope === 'entity' || scope === 'block') {
return scope
}
throw new HttpError(`unsupported dag-scope: ${scope}`, { status: 400 })
}

/**
* @param {Headers} headers
* @returns {CarParams}
*/
function getAcceptParams (headers) {
const accept = headers.get('accept')
if (!accept) return DefaultCarParams

const types = accept.split(',').map(s => s.trim())
const carType = types.find(t => t.startsWith('application/vnd.ipld.car'))
if (!carType) return DefaultCarParams

const paramPairs = carType.split(';').slice(1).map(s => s.trim())
const { version, order, dups } = Object.fromEntries(paramPairs.map(p => p.split('=').map(s => s.trim())))

// only CARv1
if (version != null && version !== '1') {
throw new HttpError(`unsupported accept parameter: version=${version}`, { status: 400 })
}
throw new HttpError(`unsupported car-scope: ${carScope}`, { status: 400 })
// only yes duplicates
if (dups && dups !== 'y') {
throw new HttpError(`unsupported accept parameter: dups=${dups}`, { status: 400 })
}
// only dfs or unk ordering
if (order && order !== 'dfs' && order !== 'unk') {
throw new HttpError(`unsupported accept parameter: order=${order}`, { status: 400 })
}

return { version: 1, order, dups: true }
}
Loading

0 comments on commit 1862165

Please sign in to comment.