Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@
"author": "Alan Shaw",
"license": "Apache-2.0 OR MIT",
"dependencies": {
"@gct256/hexdump": "^0.1.2",
"@httpland/range-parser": "^1.2.0",
"@ipld/car": "^5.2.6",
"@ipld/dag-cbor": "^9.2.5",
"@ipld/dag-json": "^10.2.5",
"@web3-storage/handlebars": "^1.0.0",
"bytes": "^3.1.2",
"chardet": "^2.0.0",
Expand All @@ -79,13 +82,12 @@
"uint8arrays": "^5.0.1"
},
"devDependencies": {
"@ipld/dag-cbor": "^9.0.8",
"@ipld/dag-pb": "^4.0.8",
"@types/bytes": "^3.1.4",
"ipfs-unixfs": "^11.1.2",
"ipfs-unixfs-exporter": "^13.3.0",
"standard": "^17.1.0",
"typescript": "^5.3.3"
"typescript": "^5.9.2"
},
"publishConfig": {
"access": "public"
Expand Down
2 changes: 1 addition & 1 deletion src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
import type { BlockService, DagService, UnixfsService } from 'dagula'
import type { TimeoutController } from 'timeout-abort-controller'

export {}
export { TimeoutController }

export interface Environment {
DEBUG?: string
Expand Down
127 changes: 126 additions & 1 deletion src/handlers/block.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
/* eslint-env browser */
/**
* @import { Block } from 'dagula'
* @import { BlockDecoder } from 'multiformats'
* @import { IpfsUrlContext, BlockContext, UnixfsContext, TimeoutController } from '../bindings.js'
*/
import * as dagJSON from '@ipld/dag-json'
import * as dagCBOR from '@ipld/dag-cbor'
import './templates/bundle.cjs'
import { MultipartByteRange } from 'multipart-byte-range'
import { hexdump } from '@gct256/hexdump'
import { CID } from 'multiformats/cid'
import { decodeRangeHeader, resolveRange } from '../util/range.js'
import { HttpError } from '../util/errors.js'
import { getTemplate, registerHelper } from '../util/handlebars.js'

/**
* @typedef {import('../bindings.js').IpfsUrlContext & import('../bindings.js').BlockContext & import('../bindings.js').UnixfsContext & { timeoutController?: import('../bindings.js').TimeoutControllerContext['timeoutController'] }} BlockHandlerContext
* @typedef {IpfsUrlContext & BlockContext & UnixfsContext & { timeoutController?: TimeoutController }} BlockHandlerContext
* @typedef {IpfsUrlContext & { gatewayDomain?: string, block: Block }} BlockHtmlHandlerContext
*/

/** @type {import('../bindings.js').Handler<BlockHandlerContext>} */
Expand Down Expand Up @@ -125,3 +137,116 @@ const handleMultipartRange = async (blocks, cid, ranges, options) => {
})
return new Response(source, { status: 206, headers: { ...options?.headers, ...source.headers } })
}

/** @type {Record<number, BlockDecoder<number, unknown>>} */
const codecs = {
[dagCBOR.code]: dagCBOR,
[dagJSON.code]: dagJSON
}

/** @type {Record<number, string>} */
const codecNames = {
[dagCBOR.code]: 'dag-cbor',
[dagJSON.code]: 'dag-json'
}

/** @param {unknown} obj */
const isIpldScalar = obj => {
switch (typeof obj) {
case 'string':
return true
case 'boolean':
return true
case 'number':
return true
case 'object': {
if (obj == null) {
return true
}
if (obj instanceof Uint8Array) { // IPLD bytes
return true
}
if (obj instanceof CID) {
return true
}
break
}
}
return false
}
registerHelper('isIpldScalar', isIpldScalar)

/** @param {unknown} obj */
const isIpldList = obj => Array.isArray(obj)
registerHelper('isIpldList', isIpldList)

/** @param {unknown} obj */
const isIpldLink = obj => obj instanceof CID
registerHelper('isIpldLink', isIpldLink)

/** @param {unknown} obj */
const isIpldMap = obj => {
return obj != null && typeof obj === 'object' && !isIpldScalar(obj) && !isIpldList(obj)
}
registerHelper('isIpldMap', isIpldMap)

/** @param {unknown} obj */
const isIpldBytes = obj => obj instanceof Uint8Array
registerHelper('isIpldBytes', isIpldBytes)

registerHelper('formatIpldScalar', (/** @type {unknown} */ obj) => {
switch (typeof obj) {
case 'string':
return obj
case 'boolean':
return obj ? 'true' : 'false'
case 'number':
return obj.toString()
case 'object': {
if (obj == null) {
return 'NULL'
}
if (obj instanceof Uint8Array) {
return hexdump(obj).join('\n')
}
break
}
}
return 'UNKNOWN'
})

/** @type {import('../bindings.js').Handler<BlockHtmlHandlerContext>} */
export async function handleBlockHtml (request, env, ctx) {
const { dataCid, path, block } = ctx
if (!dataCid) throw new Error('missing data CID')
if (path == null) throw new Error('missing URL pathname')
if (!block) throw new Error('missing block')

const codec = codecs[block.cid.code]
if (!codec) {
throw new HttpError('unsupported entry type', { status: 501 })
}

const headers = {
'Content-Type': 'text/html'
}

if (request.method === 'HEAD') {
return new Response(null, { headers })
}
if (request.method !== 'GET') {
throw new HttpError('method not allowed', { status: 405 })
}

const isSubdomain = new URL(request.url).hostname.includes('.ipfs.')
const html = getTemplate('block')({
gatewayDomain: ctx.gatewayDomain || 'storacha.link',
path: isSubdomain ? ctx.path : `${ctx.dataCid}/${ctx.path}`,
cid: block.cid,
codecName: codecNames[block.cid.code] ?? 'UNKNOWN',
codecHex: `0x${block.cid.code.toString(16)}`,
value: codec.decode(block.bytes)
})

return new Response(html)
}
Loading