-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(middleware): add R2 interceptor
- Loading branch information
1 parent
53c7e28
commit ea1e6b8
Showing
10 changed files
with
152 additions
and
58 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* eslint-env browser */ | ||
/* global FixedLengthStream */ | ||
// @ts-expect-error no types | ||
import httpRangeParse from 'http-range-parse' | ||
|
||
/** | ||
* Convert a HTTP Range header to a range object. | ||
* @param {string} value | ||
* @returns {import("@cloudflare/workers-types").R2Range} | ||
*/ | ||
export function parseRange (value) { | ||
const result = httpRangeParse(value) | ||
if (result.ranges) throw new Error('Multipart ranges not supported') | ||
const { unit, first, last, suffix } = result | ||
if (unit !== 'bytes') throw new Error(`Unsupported range unit: ${unit}`) | ||
return suffix != null | ||
? { suffix } | ||
: { offset: first, length: last != null ? last - first + 1 : undefined } | ||
} | ||
|
||
/** | ||
* | ||
* @param {import("@cloudflare/workers-types").R2ObjectBody} obj | ||
* @param {import("@cloudflare/workers-types").R2Range | undefined} range | ||
* @param {Headers} [headers] | ||
* @returns | ||
*/ | ||
export const toResponse = (obj, range, headers) => { | ||
const status = range ? 206 : 200 | ||
headers = headers || new Headers({}) | ||
let contentLength = obj.size | ||
if (range) { | ||
let first, last | ||
if ('suffix' in range) { | ||
first = obj.size - range.suffix | ||
last = obj.size - 1 | ||
} else { | ||
first = range.offset || 0 | ||
last = range.length != null ? first + range.length - 1 : obj.size - 1 | ||
} | ||
headers.set('Content-Range', `bytes ${first}-${last}/${obj.size}`) | ||
contentLength = last - first + 1 | ||
} | ||
headers.set('Content-Length', contentLength.toString()) | ||
|
||
// @ts-expect-error ReadableStream types incompatible | ||
return new Response(obj.body.pipeThrough(new FixedLengthStream(contentLength)), { status, headers }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { withSimpleSpan } from '@web3-storage/blob-fetcher/tracing/tracing' | ||
import { parseRange, toResponse } from './utils.js' | ||
/** | ||
* @import { | ||
* Middleware, | ||
* Context as MiddlewareContext | ||
* } from '@web3-storage/gateway-lib' | ||
* @import { | ||
* CarParkFetchContext, | ||
* CarParkFetchEnvironment | ||
* } from './withCarParkFetch.types.js' | ||
*/ | ||
|
||
/** | ||
* Adds {@link CarParkFetchContext.fetch} to the context. This version of fetch | ||
* will pull directly from R2 CARPARK when present | ||
* | ||
* @type {Middleware<CarParkFetchContext, MiddlewareContext, CarParkFetchEnvironment>} | ||
*/ | ||
export function withCarParkFetch (handler) { | ||
return async (request, env, ctx) => { | ||
// if carpark public bucket is not set, just use default | ||
if (!env.CARPARK_PUBLIC_BUCKET_URL) { | ||
return handler(request, env, { ...ctx, fetch: globalThis.fetch }) | ||
} | ||
/** | ||
* | ||
* @param {globalThis.RequestInfo | URL} input | ||
* @param {globalThis.RequestInit} [init] | ||
* @returns {Promise<globalThis.Response>} | ||
*/ | ||
const fetch = async (input, init) => { | ||
const urlString = input instanceof Request ? input.url : input instanceof URL ? input.toString() : input | ||
// check whether request is going to CARPARK | ||
if (env.CARPARK_PUBLIC_BUCKET_URL && urlString.startsWith(env.CARPARK_PUBLIC_BUCKET_URL)) { | ||
// extract carpark key from request | ||
let key = urlString.replace(env.CARPARK_PUBLIC_BUCKET_URL, '') | ||
key = key[0] === '/' ? key.slice(1) : key | ||
// extract headers from request | ||
const headers = input instanceof Request ? input.headers : init?.headers || {} | ||
// extract range header | ||
const rangeHeader = (new Headers(headers)).get('Range') | ||
|
||
// extract range if present from range header | ||
/** @type {import('@cloudflare/workers-types').R2GetOptions} */ | ||
|
||
/** @type {import('@cloudflare/workers-types').R2Range|undefined} */ | ||
let range | ||
if (rangeHeader) { | ||
try { | ||
range = parseRange(request.headers.get('range') ?? '') | ||
} catch (err) { | ||
return globalThis.fetch(input, init) | ||
} | ||
} | ||
// fetch directly from carpark | ||
const resp = await withSimpleSpan('carPark.get', env.CARPARK.get, env.CARPARK)(key, { range }) | ||
|
||
// return a fetch response object from the CARPARK response | ||
return resp == null ? new Response(null, { status: 404 }) : toResponse(resp, range) | ||
} | ||
return globalThis.fetch(input, init) | ||
} | ||
return handler(request, env, { ...ctx, fetch }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { | ||
Environment as MiddlewareEnvironment, | ||
Context as MiddlewareContext, | ||
} from '@web3-storage/gateway-lib' | ||
import { R2Bucket } from '@cloudflare/workers-types' | ||
|
||
export interface CarParkFetchEnvironment extends MiddlewareEnvironment { | ||
CARPARK: R2Bucket | ||
CARPARK_PUBLIC_BUCKET_URL?: string | ||
} | ||
|
||
export interface CarParkFetchContext extends MiddlewareContext { | ||
fetch: typeof globalThis.fetch | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters