Spaces:
Sleeping
Sleeping
File size: 9,806 Bytes
245db06 a406c08 245db06 a406c08 245db06 a406c08 c4c36f8 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 a406c08 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 787ee13 245db06 a406c08 787ee13 a406c08 787ee13 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
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
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
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.")
@app.get("/redirector-status", include_in_schema=False) # 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) |