Serve images from R2 via files.iddaai.com
Deploy Iddaai Backend / build-and-deploy (push) Successful in 54s
Deploy Iddaai Backend / build-and-deploy (push) Successful in 54s
Add IMAGE_BASE_URL to the deploy env so the backend builds image URLs against the Cloudflare image-proxy Worker instead of mackolik, and check in the Worker source (already live on files.iddaai.com). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* iddaai image proxy — Cloudflare Worker in front of an R2 bucket.
|
||||
*
|
||||
* Request flow for GET /teams/<id>, /competitions/<id>, /areas/<id>:
|
||||
* 1. Cloudflare edge cache
|
||||
* 2. R2 bucket (permanent mirror)
|
||||
* 3. Upstream (file.mackolikfeeds.com) → stored in R2 on the way out
|
||||
*
|
||||
* The bucket fills itself lazily; once an image lands in R2 it is served
|
||||
* from there forever, even if the upstream removes it.
|
||||
*
|
||||
* NOTE: deployed manually via the Cloudflare dashboard on 2026-06-10
|
||||
* (worker name: iddaai-image-proxy, domain: files.iddaai.com). Keep this
|
||||
* file in sync with the dashboard copy, or deploy from here with
|
||||
* `npm run deploy`.
|
||||
*/
|
||||
export interface Env {
|
||||
BUCKET: R2Bucket;
|
||||
}
|
||||
|
||||
const UPSTREAM_BASE = "https://file.mackolikfeeds.com";
|
||||
const VALID_KEY = /^(teams|competitions|areas)\/[A-Za-z0-9_-]{1,64}$/;
|
||||
|
||||
// Browsers revalidate daily; the edge keeps hits for a week. Misses are
|
||||
// cached briefly so a missing logo doesn't hammer the upstream.
|
||||
const HIT_CACHE_CONTROL = "public, max-age=86400, s-maxage=604800";
|
||||
const MISS_CACHE_CONTROL = "public, max-age=3600";
|
||||
|
||||
function imageResponse(
|
||||
body: BodyInit | null,
|
||||
contentType: string | undefined,
|
||||
etag?: string,
|
||||
): Response {
|
||||
const headers = new Headers({
|
||||
"Content-Type": contentType ?? "image/png",
|
||||
"Cache-Control": HIT_CACHE_CONTROL,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
});
|
||||
if (etag) headers.set("ETag", etag);
|
||||
return new Response(body, { headers });
|
||||
}
|
||||
|
||||
function notFound(): Response {
|
||||
return new Response("Not found", {
|
||||
status: 404,
|
||||
headers: { "Cache-Control": MISS_CACHE_CONTROL },
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext,
|
||||
): Promise<Response> {
|
||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const key = new URL(request.url).pathname.slice(1);
|
||||
if (!VALID_KEY.test(key)) return notFound();
|
||||
|
||||
// The Cache API only stores GET entries; use a GET key for HEAD too.
|
||||
const cache = caches.default;
|
||||
const cacheKey = new Request(new URL(request.url).toString());
|
||||
const cached = await cache.match(cacheKey);
|
||||
if (cached) {
|
||||
return request.method === "HEAD"
|
||||
? new Response(null, cached)
|
||||
: cached;
|
||||
}
|
||||
|
||||
// 2. Permanent mirror in R2
|
||||
const object = await env.BUCKET.get(key);
|
||||
if (object) {
|
||||
const response = imageResponse(
|
||||
object.body,
|
||||
object.httpMetadata?.contentType,
|
||||
object.httpEtag,
|
||||
);
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return request.method === "HEAD"
|
||||
? new Response(null, response)
|
||||
: response;
|
||||
}
|
||||
|
||||
// 3. Upstream fetch + mirror into R2 (images are small, buffer them)
|
||||
const upstream = await fetch(`${UPSTREAM_BASE}/${key}`, {
|
||||
cf: { cacheTtl: 0 },
|
||||
});
|
||||
if (!upstream.ok) {
|
||||
const response = notFound();
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return response;
|
||||
}
|
||||
|
||||
const contentType =
|
||||
upstream.headers.get("Content-Type") ?? "image/png";
|
||||
const buffer = await upstream.arrayBuffer();
|
||||
|
||||
ctx.waitUntil(
|
||||
env.BUCKET.put(key, buffer, {
|
||||
httpMetadata: { contentType },
|
||||
}),
|
||||
);
|
||||
|
||||
const response = imageResponse(buffer, contentType);
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return request.method === "HEAD"
|
||||
? new Response(null, response)
|
||||
: response;
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
Reference in New Issue
Block a user