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:
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.wrangler/
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["@cloudflare/workers-types"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user