Spaces:
Sleeping
Sleeping
Commit
·
245db06
1
Parent(s):
4cbb66f
initial setup for redirector
Browse files- README.md +15 -1
- app.py +170 -0
- requirements.txt +4 -0
README.md
CHANGED
@@ -4,9 +4,23 @@ emoji: 🚀
|
|
4 |
colorFrom: yellow
|
5 |
colorTo: red
|
6 |
sdk: docker
|
|
|
|
|
7 |
pinned: false
|
8 |
license: apache-2.0
|
9 |
short_description: lab for education
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
colorFrom: yellow
|
5 |
colorTo: red
|
6 |
sdk: docker
|
7 |
+
sdk_version: 3.9
|
8 |
+
app_file: app.py
|
9 |
pinned: false
|
10 |
license: apache-2.0
|
11 |
short_description: lab for education
|
12 |
---
|
13 |
|
14 |
+
# Education Lab Redirector Bot
|
15 |
+
|
16 |
+
Bot ini berfungsi sebagai proxy/redirector untuk backend Edication Lab yang berjalan di Google Colab.
|
17 |
+
Tujuannya adalah menyediakan URL statis yang dapat diakses oleh frontend, sementara URL backend Colab (dari ngrok) dapat berubah.
|
18 |
+
|
19 |
+
## Bagaimana Cara Kerjanya?
|
20 |
+
1. Frontend mengirim request ke URL publik Space Hugging Face ini.
|
21 |
+
2. Bot ini mengambil URL backend Colab terbaru dari file `backend_url.txt` yang ada di repositori GitHub [BagaSept26/education-lab](https://github.com/BagaSept26/education-lab/blob/main/backend_url.txt).
|
22 |
+
3. Bot kemudian meneruskan request tersebut ke URL backend Colab yang aktif.
|
23 |
+
|
24 |
+
## Endpoint Status
|
25 |
+
Anda dapat memeriksa status redirector dan URL backend yang saat ini digunakan di:
|
26 |
+
[/redirector-status](/redirector-status)
|
app.py
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import httpx #library HTTP client modern, async-friendly
|
3 |
+
import asyncio
|
4 |
+
from fastapi import FastAPI, Request, HTTPException
|
5 |
+
from fastapi.responses import StreamingResponse, RedirectResponse, PlainTextResponse
|
6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
7 |
+
from dotenv import load_dotenv #.env
|
8 |
+
import logging
|
9 |
+
import time
|
10 |
+
|
11 |
+
# --- Konfigurasi logging ---
|
12 |
+
logging.basisConfig(level=logging.INFO)
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
# --- konfigurasi aplikasi ---
|
16 |
+
load_dotenv()
|
17 |
+
|
18 |
+
#url backend RAW github
|
19 |
+
GITHUB_URL_FILE = os.getenv(
|
20 |
+
"GITHUB_RAW_URL_FILE",
|
21 |
+
"https://raw.githubusercontent.com/BagaSept26/education-lab/refs/heads/main/backend_url.txt"
|
22 |
+
)
|
23 |
+
|
24 |
+
#interval cache untuk URL backend
|
25 |
+
BACKEND_URL_CACHE_TTL = int(os.getenv("BACK_END_URL_CACHE_TTL", "60"))
|
26 |
+
|
27 |
+
# --- State Aplikasi ---
|
28 |
+
app = FastAPI(title="Education Lab Redirector", version="0.1.0")
|
29 |
+
current_backend_url = None
|
30 |
+
last_url_fetch_time = 0
|
31 |
+
url_fetch_lock = asyncio.Lock()
|
32 |
+
|
33 |
+
#CORS
|
34 |
+
origins = [
|
35 |
+
"http://localhost:5173", #dev lokal
|
36 |
+
"https://education-lab-bagaseptian.vercel.app" #vercel pribadi
|
37 |
+
"https://*/vercel.app" #umum
|
38 |
+
"https://huggingface.co/spaces/bagaseptian/edu_lab" #hf space
|
39 |
+
]
|
40 |
+
app.add_middleware(
|
41 |
+
CORSMiddleware,
|
42 |
+
allow_origins=origins,
|
43 |
+
allow_credentials=True,
|
44 |
+
allow_methods=["*"],
|
45 |
+
allow_headers=["*"],
|
46 |
+
)
|
47 |
+
|
48 |
+
async def get_latest_backend_url_from_github():
|
49 |
+
"""Mengambil URL backend terbaru dari file gihub"""
|
50 |
+
global current_backend_url, last_url_fetch_time
|
51 |
+
|
52 |
+
async with url_fetch_lock:
|
53 |
+
if current_backend_url and (time.time() - last_url_fetch_time) < BACKEND_URL_CACHE_TTL:
|
54 |
+
logger.info(f"Menggunakan URL Backend dari cache: {current_backend_url}")
|
55 |
+
return current_backend_url
|
56 |
+
|
57 |
+
logger.info(f"Mengambil URL backend terbaru dari Github: {GITHUB_URL_FILE}")
|
58 |
+
headers = {}
|
59 |
+
try:
|
60 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
61 |
+
response = await client.get(GITHUB_RAW_URL_FILE, headers=headers)
|
62 |
+
response.raise_for_status()
|
63 |
+
|
64 |
+
new_url = response.text.strip()
|
65 |
+
if new_url and (new_url.startswith("http://") or new_url.startswith("https;//")):
|
66 |
+
if new_url != current_backend_url:
|
67 |
+
logger.info(f"URL backend baru ditemukan: {new_url}")
|
68 |
+
current_backend_url = new_url
|
69 |
+
else:
|
70 |
+
logger.info(f"URL backend tidak berubah: {new_url}")
|
71 |
+
last_url_fetch_time = time.time()
|
72 |
+
return current_backend_url
|
73 |
+
else:
|
74 |
+
logger.error(f"Format URL tidak valid dari Github: '{new_url}'")
|
75 |
+
return current_backend_url
|
76 |
+
except httpx.HTTPStatusError as e:
|
77 |
+
logger.error(f"HTTP error saat mengambil URL dari Github: {e.response.status_code} - {e.response.text}")
|
78 |
+
except httpx.RequestError as e:
|
79 |
+
logger.error(f"Request error saat mengambil URL dari Github: {e}")
|
80 |
+
except Exception as e:
|
81 |
+
logger.error(f"Error tidak terduga saat mengambil URL: {e}")
|
82 |
+
|
83 |
+
if current_backend_url and (time.time() - last_url_fetch_time) < (BACKEND_URL_CACHE_TTL * 5):
|
84 |
+
logger.warning("Gagal megambil URL baru, menggunakan URL lama dari cache.")
|
85 |
+
return current_backend_url
|
86 |
+
else:
|
87 |
+
current_backend_url = None
|
88 |
+
last_url_fetch_time = 0
|
89 |
+
return None
|
90 |
+
|
91 |
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
92 |
+
async def proxy_to_backend(request: Request, path: str):
|
93 |
+
"""
|
94 |
+
Menerima semua request dan memroxy ke backend Colab.
|
95 |
+
"""
|
96 |
+
backend_url = await get_latest_backend_url_from_github()
|
97 |
+
|
98 |
+
if not backend_url:
|
99 |
+
logger.error("Tidak ada URL backend yang valid tersedia.")
|
100 |
+
raise HTTPException(status_code=503, detail="Layanan backend sementara tidak tersedia. URL tidak ditemukan.")
|
101 |
+
|
102 |
+
target_url_path = f"{backend_url}/{path}"
|
103 |
+
if request.url.query:
|
104 |
+
target_url_path += f"?{request.url.querry}"
|
105 |
+
|
106 |
+
logger.info(f"Meneruskan request {request.method} ke: {target_url_path}")
|
107 |
+
|
108 |
+
#body req
|
109 |
+
body_bytes = await request.body()
|
110 |
+
headers = dict(request.headers)
|
111 |
+
|
112 |
+
headers.pop("host", None)
|
113 |
+
headers.pop("Host", None)
|
114 |
+
|
115 |
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
116 |
+
try:
|
117 |
+
rp = await client.request(
|
118 |
+
method=request.method,
|
119 |
+
url=target_url_path,
|
120 |
+
headers=headers,
|
121 |
+
content=body_bytes,
|
122 |
+
)
|
123 |
+
excluded_headers = [
|
124 |
+
"content-encoding", "Content-Encoding","transfer-encoding", "Transfer-Encoding","connection", "Connection"
|
125 |
+
]
|
126 |
+
responses_headers = {
|
127 |
+
k: v for k, v in rp.headers.items() if k.lower() not in excluded_headers
|
128 |
+
}
|
129 |
+
responses_headers["Access-Control-Allow-Origin"] = request.headers.get("origin") or "*"
|
130 |
+
responses_headers["Access-Control-Allow-Credentials"] = "true"
|
131 |
+
|
132 |
+
if 300 <= rp.status_code < 400 and "location" in rp.headers:
|
133 |
+
return RedirectResponse(url=rp.headers["location"],status_code=rp.status_code, headers=response_headers)
|
134 |
+
|
135 |
+
return StreamingResponse(
|
136 |
+
rp.aiter_bytes(),
|
137 |
+
status_code=rp.status_code,
|
138 |
+
media_type=rp.headers.get("content-type"),
|
139 |
+
headers=responses_headers
|
140 |
+
)
|
141 |
+
except httpx.HTTPStatusError as e:
|
142 |
+
logger.error(f"HTTP error dari backend: {e.response.status_code} - {e.response.text}")
|
143 |
+
return PlainTextResponse(content=e.response.text,status_code=e.response.status_code)
|
144 |
+
except httpx.ConnectErros as e:
|
145 |
+
logger.error(f"tidak dapat terhubung ke backend: {backend_url} - {e}")
|
146 |
+
raise HTTPException(status_code=504, detail =f"Gateway Timeout: tidak dapat terhubung ke layanan Colab di {backend_url}.")
|
147 |
+
except httpx.TimeoutException as e:
|
148 |
+
logger.error(f"timeout saat mengubungi backend: {backend_url} - {e}")
|
149 |
+
raise HTTPException(status_code=504, detail=f"Gateway Timeout: Waktu habis saat menghubungi layanan backend Colab.")
|
150 |
+
except Exception as e:
|
151 |
+
logger.error(f"Error tidak terduga saat proxy: {e}")
|
152 |
+
raise HTTPException(status_code=500, detail="Internal server error pada proxy.")
|
153 |
+
|
154 |
+
@app.get("/redirector-status", include_in_schema=False)
|
155 |
+
async def status():
|
156 |
+
latest_url = await get_latest_backend_url_from_github()
|
157 |
+
return{
|
158 |
+
"status": "Redirector Aktif",
|
159 |
+
"pesan": "Proxy bot untuk Education Lab Backend",
|
160 |
+
"target_github_url_file": GITHUB_RAW_URL_FILE,
|
161 |
+
"current_resolved_backend_url": latest_url if latest_url else "Belum ada URL backend yang valid ditemukan.",
|
162 |
+
"cache_ttl_seconds": BACKEND_URL_CACHE_TTL,
|
163 |
+
"last_url_fetch_attempt_timestamp": last_url_fetch_time if last_url_fetch_time > 0 else "Belum pernah fetch"
|
164 |
+
}
|
165 |
+
|
166 |
+
if __name__ == "__main__":
|
167 |
+
import uvicorn
|
168 |
+
logger.info("Menjalankan redirector lokal di port 8001...")
|
169 |
+
|
170 |
+
uvicorn.run(app, host="0.0.0.0", port=8001)
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi>=0.100.0
|
2 |
+
uvicorn[standard]>=0.20.0
|
3 |
+
httpx>=0.24.0
|
4 |
+
python.dotenv>=.0.20.0
|