fix cloudflare r2 adapter range requests

This commit is contained in:
dswbx
2025-07-02 14:07:26 +02:00
parent 4e10b36d0d
commit d1378c6c51
5 changed files with 31 additions and 14 deletions

View File

@@ -33,6 +33,7 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
keepAliveSeconds?: number; keepAliveSeconds?: number;
forceHttps?: boolean; forceHttps?: boolean;
manifest?: string; manifest?: string;
registerMedia?: boolean | ((env: Env) => void);
}; };
export type Context<Env = CloudflareEnv> = { export type Context<Env = CloudflareEnv> = {

View File

@@ -93,8 +93,12 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>, config: CloudflareBkndConfig<Env>,
args?: CfMakeConfigArgs<Env>, args?: CfMakeConfigArgs<Env>,
) { ) {
if (!media_registered) { if (!media_registered && config.registerMedia !== false) {
if (typeof config.registerMedia === "function") {
config.registerMedia(args?.env as any);
} else {
registerMedia(args?.env as any); registerMedia(args?.env as any);
}
media_registered = true; media_registered = true;
} }

View File

@@ -13,6 +13,8 @@ export {
type BindingMap, type BindingMap,
} from "./bindings"; } from "./bindings";
export { constants } from "./config"; export { constants } from "./config";
export { StorageR2Adapter } from "./storage/StorageR2Adapter";
export { registries } from "bknd";
// for compatibility with old code // for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>( export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(

View File

@@ -1,5 +1,6 @@
import { registries } from "bknd"; import { registries } from "bknd";
import { isDebug } from "bknd/core"; import { isDebug } from "bknd/core";
// @ts-ignore
import { StringEnum } from "bknd/utils"; import { StringEnum } from "bknd/utils";
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media"; import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
import { getBindings } from "../bindings"; import { getBindings } from "../bindings";
@@ -63,46 +64,49 @@ export class StorageR2Adapter extends StorageAdapter {
async putObject(key: string, body: FileBody) { async putObject(key: string, body: FileBody) {
try { try {
const res = await this.bucket.put(key, body); const res = await this.bucket.put(this.getKey(key), body);
return res?.etag; return res?.etag;
} catch (e) { } catch (e) {
return undefined; return undefined;
} }
} }
async listObjects( async listObjects(prefix = ""): Promise<{ key: string; last_modified: Date; size: number }[]> {
prefix?: string, const list = await this.bucket.list({ limit: 50, prefix: this.getKey(prefix) });
): Promise<{ key: string; last_modified: Date; size: number }[]> {
const list = await this.bucket.list({ limit: 50 });
return list.objects.map((item) => ({ return list.objects.map((item) => ({
key: item.key, key: item.key.replace(this.getKey(""), ""),
size: item.size, size: item.size,
last_modified: item.uploaded, last_modified: item.uploaded,
})); }));
} }
private async headObject(key: string): Promise<R2Object | null> { private async headObject(key: string): Promise<R2Object | null> {
return await this.bucket.head(key); return await this.bucket.head(this.getKey(key));
} }
async objectExists(key: string): Promise<boolean> { async objectExists(key: string): Promise<boolean> {
return (await this.headObject(key)) !== null; return (await this.headObject(key)) !== null;
} }
async getObject(key: string, headers: Headers): Promise<Response> { async getObject(_key: string, headers: Headers): Promise<Response> {
let object: R2ObjectBody | null; let object: R2ObjectBody | null;
const key = this.getKey(_key);
const responseHeaders = new Headers({ const responseHeaders = new Headers({
"Accept-Ranges": "bytes", "Accept-Ranges": "bytes",
"Content-Type": guess(key), "Content-Type": guess(key),
}); });
const range = headers.has("range");
//console.log("getObject:headers", headersToObject(headers)); //console.log("getObject:headers", headersToObject(headers));
if (headers.has("range")) { if (range) {
const options = isDebug() const options = isDebug()
? {} // miniflare doesn't support range requests ? {} // miniflare doesn't support range requests
: { : {
range: headers, range: headers,
onlyIf: headers, onlyIf: headers,
}; };
object = (await this.bucket.get(key, options)) as R2ObjectBody; object = (await this.bucket.get(key, options)) as R2ObjectBody;
if (!object) { if (!object) {
@@ -130,13 +134,14 @@ export class StorageR2Adapter extends StorageAdapter {
responseHeaders.set("Last-Modified", object.uploaded.toUTCString()); responseHeaders.set("Last-Modified", object.uploaded.toUTCString());
return new Response(object.body, { return new Response(object.body, {
status: object.range ? 206 : 200, status: range ? 206 : 200,
headers: responseHeaders, headers: responseHeaders,
}); });
} }
private writeHttpMetadata(headers: Headers, object: R2Object | R2ObjectBody): void { private writeHttpMetadata(headers: Headers, object: R2Object | R2ObjectBody): void {
let metadata = object.httpMetadata; let metadata = object.httpMetadata;
if (!metadata || Object.keys(metadata).length === 0) { if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare) // guessing is especially required for dev environment (miniflare)
metadata = { metadata = {
@@ -163,13 +168,17 @@ export class StorageR2Adapter extends StorageAdapter {
} }
async deleteObject(key: string): Promise<void> { async deleteObject(key: string): Promise<void> {
await this.bucket.delete(key); await this.bucket.delete(this.getKey(key));
} }
getObjectUrl(key: string): string { getObjectUrl(key: string): string {
throw new Error("Method getObjectUrl not implemented."); throw new Error("Method getObjectUrl not implemented.");
} }
protected getKey(key: string) {
return key;
}
toJSON(secrets?: boolean) { toJSON(secrets?: boolean) {
return { return {
type: this.getName(), type: this.getName(),

View File

@@ -124,7 +124,8 @@ const Icons = {
}; };
const AdapterIcon = ({ type }: { type: string }) => { const AdapterIcon = ({ type }: { type: string }) => {
const Icon = Icons[type]; // find icon whose name starts with type
const Icon = Object.entries(Icons).find(([key]) => type.startsWith(key))?.[1];
if (!Icon) return null; if (!Icon) return null;
return <Icon />; return <Icon />;
}; };