Serve images from R2 via files.iddaai.com
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:
2026-06-10 14:24:38 +03:00
parent e0fbde2fde
commit 9a8f9941b6
7 changed files with 206 additions and 0 deletions
+1
View File
@@ -46,6 +46,7 @@ jobs:
-e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \ -e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \
-e JWT_SECRET='${{ secrets.JWT_SECRET }}' \ -e JWT_SECRET='${{ secrets.JWT_SECRET }}' \
-e JWT_ACCESS_EXPIRATION='1d' \ -e JWT_ACCESS_EXPIRATION='1d' \
-e IMAGE_BASE_URL='https://files.iddaai.com' \
iddaai-be:latest /bin/sh -c "npx prisma migrate deploy && node dist/src/main.js" iddaai-be:latest /bin/sh -c "npx prisma migrate deploy && node dist/src/main.js"
- name: Saglik Kontrolu - name: Saglik Kontrolu
+1
View File
@@ -0,0 +1 @@
.wrangler/
+51
View File
@@ -0,0 +1,51 @@
# iddaai image proxy (Cloudflare Worker + R2)
Takım / lig / ülke görsellerini R2'den servis eden lazy-fill proxy.
İstek akışı: **edge cache → R2 → upstream (mackolik) → R2'ye yaz**.
Bucket kendi kendine dolar; bir görsel R2'ye girdikten sonra upstream
kaldırsa bile kalıcıdır.
## Canlı kurulum (2026-06-10)
- Worker: `iddaai-image-proxy` (dashboard üzerinden deploy edildi)
- Bucket: `iddaai-images` (binding adı: `BUCKET`)
- Domain: `https://files.iddaai.com`
- BE env: `IMAGE_BASE_URL=https://files.iddaai.com` (.gitea/workflows/deploy.yml)
Dashboard'daki kod ile `src/index.ts` aynı mantıktır (dashboard'da JS,
burada TS). Kod değişikliği gerekirse ikisini senkron tut veya
`npm run deploy` ile buradan deploy et.
## URL şeması (upstream ile birebir aynı)
```
GET /teams/<teamId> → takım logosu
GET /competitions/<leagueId> → lig logosu
GET /areas/<countryId> → ülke bayrağı
```
## CLI ile geliştirme / deploy
```bash
cd workers/image-proxy
npm install
npm run dev # lokal test (miniflare, lokal R2 simülasyonu)
npm run typecheck
npx wrangler login # ilk seferde
npm run deploy
```
## Bucket'ı önceden doldurma (opsiyonel)
```bash
../../scripts/warm-image-cache.sh https://files.iddaai.com
```
Prod sunucuda çalışır; DB'deki tüm takım/lig/ülke ID'lerini Worker
üzerinden bir kez ister, Worker her birini R2'ye yazar (~20K istek,
~200 MB). Çalıştırılmasa da bucket trafikle kendi kendine dolar.
## Maliyet
R2 free tier: 10 GB depolama + sınırsız egress. Workers free tier:
100K istek/gün. Bu ölçekte aylık maliyet: 0.
+15
View File
@@ -0,0 +1,15 @@
{
"name": "iddaai-image-proxy",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260601.0",
"typescript": "^5.6.0",
"wrangler": "^4.0.0"
}
}
+113
View File
@@ -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>;
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"types": ["@cloudflare/workers-types"]
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
name = "iddaai-image-proxy"
main = "src/index.ts"
compatibility_date = "2026-06-01"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "iddaai-images"
# Custom domain (configured in the Cloudflare dashboard on 2026-06-10;
# kept here so CLI deploys stay in sync).
[[routes]]
pattern = "files.iddaai.com"
custom_domain = true