Spaces:
Sleeping
Sleeping
import os | |
import httpx # Library HTTP client modern, async-friendly | |
import asyncio | |
from fastapi import FastAPI, Request, HTTPException | |
from fastapi.responses import StreamingResponse, RedirectResponse, PlainTextResponse | |
from fastapi.middleware.cors import CORSMiddleware | |
from dotenv import load_dotenv # Untuk membaca .env jika perlu (opsional) | |
import logging | |
import time | |
# --- Konfigurasi Logging --- | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# --- Konfigurasi Aplikasi --- | |
load_dotenv() # Muat variabel dari .env jika ada (untuk pengembangan lokal) | |
# URL ke file backend_url.txt RAW di GitHub | |
# Ganti YOUR_GITHUB_USERNAME dan YOUR_REPO_NAME | |
GITHUB_RAW_URL_FILE = os.getenv( | |
"GITHUB_RAW_URL_FILE", | |
"https://raw.githubusercontent.com/BagaSept26/education-lab/refs/heads/main/backend_url.txt" | |
) | |
# Token GitHub PAT jika repo Anda private (tidak direkomendasikan untuk file URL ini) | |
# Jika repo publik, token tidak diperlukan untuk membaca file raw. | |
# GITHUB_TOKEN = os.getenv("GITHUB_PAT_FOR_REDIRECTOR") # Simpan di Secrets HF Space | |
# Interval cache untuk URL backend (dalam detik) | |
BACKEND_URL_CACHE_TTL = int(os.getenv("BACKEND_URL_CACHE_TTL", "60")) # Cache 60 detik | |
# --- State Aplikasi --- | |
app = FastAPI(title="Edication Lab Redirector", version="0.1.0") | |
current_backend_url = None | |
last_url_fetch_time = 0 | |
url_fetch_lock = asyncio.Lock() | |
# --- Konfigurasi CORS untuk HF Space --- | |
# Izinkan request dari Vercel frontend Anda | |
# Ganti dengan URL Vercel Anda yang sebenarnya atau gunakan wildcard yang lebih spesifik | |
origins = [ | |
"http://localhost:5173", # Untuk dev lokal frontend | |
"https://education-lab-bagaseptian.vercel.app", # GANTI INI | |
"https://*.vercel.app", # Lebih umum | |
"https://bagaseptian-edu_lab.hf.space" # URL space ini sendiri jika perlu | |
] | |
app.add_middleware( | |
CORSMiddleware, | |
allow_origins=origins, | |
allow_credentials=True, | |
allow_methods=["*"], | |
allow_headers=["*"], | |
) | |
async def get_latest_backend_url_from_github(): | |
"""Mengambil URL backend terbaru dari file di GitHub.""" | |
global current_backend_url, last_url_fetch_time | |
async with url_fetch_lock: # Pastikan hanya satu coroutine yang fetch pada satu waktu | |
# Cek apakah cache masih valid | |
if current_backend_url and (time.time() - last_url_fetch_time) < BACKEND_URL_CACHE_TTL: | |
logger.info(f"Menggunakan URL backend dari cache: {current_backend_url}") | |
return current_backend_url | |
logger.info(f"Mengambil URL backend terbaru dari GitHub: {GITHUB_RAW_URL_FILE}") | |
headers = {} | |
# if GITHUB_TOKEN: # Jika file di repo private | |
# headers["Authorization"] = f"token {GITHUB_TOKEN}" | |
try: | |
async with httpx.AsyncClient(timeout=10.0) as client: | |
response = await client.get(GITHUB_RAW_URL_FILE, headers=headers) | |
response.raise_for_status() # Akan raise error untuk status 4xx/5xx | |
new_url = response.text.strip() | |
if new_url and (new_url.startswith("http://") or new_url.startswith("https://")): | |
if new_url != current_backend_url: | |
logger.info(f"URL backend baru ditemukan: {new_url}") | |
current_backend_url = new_url | |
else: | |
logger.info(f"URL backend tidak berubah: {new_url}") | |
last_url_fetch_time = time.time() | |
return current_backend_url | |
else: | |
logger.error(f"Format URL tidak valid dari GitHub: '{new_url}'") | |
# Jangan update cache jika URL tidak valid, tetap gunakan yang lama (jika ada) | |
return current_backend_url # Atau None jika belum pernah ada yg valid | |
except httpx.HTTPStatusError as e: | |
logger.error(f"HTTP error saat mengambil URL dari GitHub: {e.response.status_code} - {e.response.text}") | |
except httpx.RequestError as e: | |
logger.error(f"Request error saat mengambil URL dari GitHub: {e}") | |
except Exception as e: | |
logger.error(f"Error tidak terduga saat mengambil URL: {e}") | |
# Jika fetch gagal, kembalikan URL lama yang valid (jika ada) untuk sementara waktu | |
# atau jika URL lama sudah terlalu lama, mungkin lebih baik return None | |
if current_backend_url and (time.time() - last_url_fetch_time) < (BACKEND_URL_CACHE_TTL * 5): # Toleransi 5x TTL | |
logger.warning("Gagal mengambil URL baru, menggunakan URL lama dari cache.") | |
return current_backend_url | |
else: # Jika URL lama juga sudah terlalu basi atau tidak ada | |
current_backend_url = None # Reset jika gagal fetch terlalu lama | |
last_url_fetch_time = 0 | |
return None | |
async def proxy_to_backend(request: Request, path: str): | |
""" | |
Menerima semua request dan mem-proxy-nya ke backend Colab. | |
""" | |
backend_url = await get_latest_backend_url_from_github() | |
if not backend_url: | |
logger.error("Tidak ada URL backend yang valid tersedia.") | |
raise HTTPException(status_code=503, detail="Layanan backend sementara tidak tersedia. URL tidak ditemukan.") | |
target_url_path = f"{backend_url}/{path}" | |
if request.url.query: | |
target_url_path += f"?{request.url.query}" | |
logger.info(f"Meneruskan request {request.method} ke: {target_url_path}") | |
# Baca body request jika ada | |
body_bytes = await request.body() | |
headers = dict(request.headers) | |
# Hapus atau modifikasi header tertentu jika perlu (misal, 'host') | |
headers.pop("host", None) | |
headers.pop("Host", None) | |
# Tambahkan header yang mungkin dibutuhkan oleh backend Anda | |
# headers["X-Forwarded-For"] = request.client.host | |
async with httpx.AsyncClient(timeout=300.0) as client: # Timeout panjang untuk model AI | |
try: | |
# Kirim request ke backend | |
rp = await client.request( | |
method=request.method, | |
url=target_url_path, | |
headers=headers, | |
content=body_bytes, | |
# Untuk mengizinkan redirect jika backend mengembalikan 3xx | |
# follow_redirects=False # Kita akan handle redirect secara manual jika perlu | |
) | |
# Buat respons streaming agar efisien | |
# Beberapa header tidak boleh di-proxy langsung | |
excluded_headers = [ | |
"content-encoding", "Content-Encoding", # Biarkan httpx/FastAPI handle de/compression | |
"transfer-encoding", "Transfer-Encoding", | |
"connection", "Connection" | |
] | |
response_headers = { | |
k: v for k, v in rp.headers.items() if k.lower() not in excluded_headers | |
} | |
# Pastikan CORS header dari proxy juga benar | |
response_headers["Access-Control-Allow-Origin"] = request.headers.get("origin") or "*" # Atau lebih spesifik | |
response_headers["Access-Control-Allow-Credentials"] = "true" | |
# Jika backend mengembalikan redirect, kita bisa meneruskannya | |
if 300 <= rp.status_code < 400 and "location" in rp.headers: | |
return RedirectResponse(url=rp.headers["location"], status_code=rp.status_code, headers=response_headers) | |
return StreamingResponse( | |
rp.aiter_bytes(), # Stream content dari backend | |
status_code=rp.status_code, | |
media_type=rp.headers.get("content-type"), | |
headers=response_headers | |
) | |
except httpx.HTTPStatusError as e: | |
logger.error(f"HTTP error dari backend: {e.response.status_code} - {e.response.text}") | |
# Meneruskan status error dari backend | |
return PlainTextResponse(content=e.response.text, status_code=e.response.status_code) | |
except httpx.ConnectError as e: | |
logger.error(f"Tidak dapat terhubung ke backend: {backend_url} - {e}") | |
raise HTTPException(status_code=504, detail=f"Gateway Timeout: Tidak dapat terhubung ke layanan backend Colab di {backend_url}.") | |
except httpx.TimeoutException as e: | |
logger.error(f"Timeout saat menghubungi backend: {backend_url} - {e}") | |
raise HTTPException(status_code=504, detail=f"Gateway Timeout: Waktu habis saat menghubungi layanan backend Colab.") | |
except Exception as e: | |
logger.error(f"Error tidak terduga saat proxying: {e}") | |
raise HTTPException(status_code=500, detail="Internal server error pada proxy.") | |
# Endpoint untuk cek status redirector | |
async def status(): | |
latest_url = await get_latest_backend_url_from_github() # Ambil URL terbaru | |
return { | |
"status": "Redirector Aktif", | |
"pesan": "Proxy bot untuk Edication Lab Backend.", | |
"target_github_url_file": GITHUB_RAW_URL_FILE, | |
"current_resolved_backend_url": latest_url if latest_url else "Belum ada URL backend yang valid ditemukan.", | |
"cache_ttl_seconds": BACKEND_URL_CACHE_TTL, | |
"last_url_fetch_attempt_timestamp": last_url_fetch_time if last_url_fetch_time > 0 else "Belum pernah fetch" | |
} | |
# Jika dijalankan secara lokal (python app.py) | |
# if __name__ == "__main__": | |
# import uvicorn | |
# logger.info("Menjalankan redirector secara lokal di port 8001...") | |
# # Ganti nilai di GITHUB_RAW_URL_FILE dengan URL raw GitHub Anda yang benar sebelum menjalankan lokal | |
# # Contoh: GITHUB_RAW_URL_FILE = "https://raw.githubusercontent.com/NAMA_USER_ANDA/edication-lab/main/backend_url.txt" | |
# uvicorn.run(app, host="0.0.0.0", port=8001) |