From 9a8f9941b6a4e2cc945c1c0e73c4965e2d61d691 Mon Sep 17 00:00:00 2001 From: Fahri Can Date: Wed, 10 Jun 2026 14:24:38 +0300 Subject: [PATCH] Serve images from R2 via files.iddaai.com 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 --- .gitea/workflows/deploy.yml | 1 + workers/image-proxy/.gitignore | 1 + workers/image-proxy/README.md | 51 ++++++++++++++ workers/image-proxy/package.json | 15 ++++ workers/image-proxy/src/index.ts | 113 ++++++++++++++++++++++++++++++ workers/image-proxy/tsconfig.json | 12 ++++ workers/image-proxy/wrangler.toml | 13 ++++ 7 files changed, 206 insertions(+) create mode 100644 workers/image-proxy/.gitignore create mode 100644 workers/image-proxy/README.md create mode 100644 workers/image-proxy/package.json create mode 100644 workers/image-proxy/src/index.ts create mode 100644 workers/image-proxy/tsconfig.json create mode 100644 workers/image-proxy/wrangler.toml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 6e7aa99..7ee5be3 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -46,6 +46,7 @@ jobs: -e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \ -e JWT_SECRET='${{ secrets.JWT_SECRET }}' \ -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" - name: Saglik Kontrolu diff --git a/workers/image-proxy/.gitignore b/workers/image-proxy/.gitignore new file mode 100644 index 0000000..b75a0fa --- /dev/null +++ b/workers/image-proxy/.gitignore @@ -0,0 +1 @@ +.wrangler/ diff --git a/workers/image-proxy/README.md b/workers/image-proxy/README.md new file mode 100644 index 0000000..9ac774f --- /dev/null +++ b/workers/image-proxy/README.md @@ -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/ → takım logosu +GET /competitions/ → lig logosu +GET /areas/ → ü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. diff --git a/workers/image-proxy/package.json b/workers/image-proxy/package.json new file mode 100644 index 0000000..641ab98 --- /dev/null +++ b/workers/image-proxy/package.json @@ -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" + } +} diff --git a/workers/image-proxy/src/index.ts b/workers/image-proxy/src/index.ts new file mode 100644 index 0000000..ef80df4 --- /dev/null +++ b/workers/image-proxy/src/index.ts @@ -0,0 +1,113 @@ +/** + * iddaai image proxy — Cloudflare Worker in front of an R2 bucket. + * + * Request flow for GET /teams/, /competitions/, /areas/: + * 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 { + 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; diff --git a/workers/image-proxy/tsconfig.json b/workers/image-proxy/tsconfig.json new file mode 100644 index 0000000..8f186d5 --- /dev/null +++ b/workers/image-proxy/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ES2022", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/workers/image-proxy/wrangler.toml b/workers/image-proxy/wrangler.toml new file mode 100644 index 0000000..74ebc49 --- /dev/null +++ b/workers/image-proxy/wrangler.toml @@ -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