cheekeong2025 commited on
Commit
35bbbf1
·
verified ·
1 Parent(s): 9c712b3

Update loader.py

Browse files
Files changed (1) hide show
  1. loader.py +72 -34
loader.py CHANGED
@@ -1,7 +1,8 @@
1
- # loader.py — Public proxy for PRIVATE Static Space using huggingface.co/resolve
2
  import os
3
  import mimetypes
4
  from urllib.parse import urljoin
 
5
 
6
  from fastapi import FastAPI, Request
7
  from fastapi.responses import Response, HTMLResponse, PlainTextResponse
@@ -10,15 +11,14 @@ import httpx
10
  app = FastAPI()
11
 
12
  # ==== Config from Secrets ====
13
- SPACE_ID = os.getenv("PRIVATE_SPACE_ID") # e.g. "cheekeong2025/iip-grading"
14
- HF_TOKEN = os.getenv("HF_TOKEN") # must have read access
15
- REVISION = os.getenv("REVISION", "main") # optional: pin a branch/tag/commit
16
  # =============================
17
 
18
  if not SPACE_ID:
19
  raise RuntimeError(
20
- "PRIVATE_SPACE_ID is not set. Go to Settings → Repository secrets and add "
21
- "PRIVATE_SPACE_ID like 'cheekeong2025/iip-grading'."
22
  )
23
  if not HF_TOKEN:
24
  raise RuntimeError(
@@ -28,19 +28,59 @@ if not HF_TOKEN:
28
  BASE_RESOLVE = f"https://huggingface.co/spaces/{SPACE_ID}/resolve/{REVISION}/"
29
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"}
30
 
31
- def build_resolve_url(path: str) -> str:
32
- # Ensure trailing slash join is correct
33
- return urljoin(BASE_RESOLVE, path.lstrip("/"))
34
 
35
  def with_query(url: str, request: Request) -> str:
36
  q = str(request.url.query or "")
37
  return f"{url}?{q}" if q else url
38
 
39
- def guess_mime(path: str, fallback: str = "application/octet-stream") -> str:
 
 
 
 
40
  if path.endswith("/") or path == "":
41
  return "text/html; charset=utf-8"
42
  mime, _ = mimetypes.guess_type(path)
43
- return mime or fallback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  @app.get("/health")
46
  async def health():
@@ -48,39 +88,37 @@ async def health():
48
 
49
  @app.get("/")
50
  async def root(request: Request):
51
- # Serve index.html explicitly (avoid upstream redirects)
52
- target = with_query(build_resolve_url("index.html"), request)
 
 
 
 
53
  try:
54
  async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
55
- r = await client.get(target, headers=HEADERS)
 
 
56
  except Exception as e:
57
- return PlainTextResponse(f"Error fetching index.html: {e}", status_code=500)
58
-
59
- # If index.html is missing, try repo root as a fallback (rare)
60
- if r.status_code == 404:
61
- try:
62
- fallback = with_query(build_resolve_url(""), request)
63
- async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
64
- r = await client.get(fallback, headers=HEADERS)
65
- except Exception as e:
66
- return PlainTextResponse(f"Error fetching root: {e}", status_code=500)
67
 
68
  ctype = r.headers.get("content-type", "text/html; charset=utf-8")
69
- return HTMLResponse(r.text, status_code=r.status_code, media_type=ctype)
 
 
 
70
 
71
  @app.get("/{path:path}")
72
  async def proxy(path: str, request: Request):
73
- # Proxy any other static asset (CSS, JS, images, fonts, etc.)
74
- # Map directories to index.html for SPA-style paths
75
- resolved_path = path if "." in path.split("/")[-1] else (path.rstrip("/") + "/index.html")
76
- target = with_query(build_resolve_url(resolved_path), request)
77
-
78
  try:
79
  async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
80
- r = await client.get(target, headers=HEADERS)
81
  except Exception as e:
82
- return PlainTextResponse(f"Error fetching {resolved_path}: {e}", status_code=500)
83
 
84
- # Prefer upstream header; otherwise guess from file extension
85
- ctype = r.headers.get("content-type") or guess_mime(resolved_path)
86
  return Response(content=r.content, media_type=ctype, status_code=r.status_code)
 
1
+ # loader.py — robust public proxy for PRIVATE Static Space using huggingface.co/resolve
2
  import os
3
  import mimetypes
4
  from urllib.parse import urljoin
5
+ from typing import Optional
6
 
7
  from fastapi import FastAPI, Request
8
  from fastapi.responses import Response, HTMLResponse, PlainTextResponse
 
11
  app = FastAPI()
12
 
13
  # ==== Config from Secrets ====
14
+ SPACE_ID = os.getenv("PRIVATE_SPACE_ID") # e.g. "cheekeong2025/iip-grading"
15
+ HF_TOKEN = os.getenv("HF_TOKEN") # must have read access
16
+ REVISION = os.getenv("REVISION", "main") # optional: pin branch/tag/commit
17
  # =============================
18
 
19
  if not SPACE_ID:
20
  raise RuntimeError(
21
+ "PRIVATE_SPACE_ID is not set. Add it in Settings → Repository secrets (e.g. 'cheekeong2025/iip-grading')."
 
22
  )
23
  if not HF_TOKEN:
24
  raise RuntimeError(
 
28
  BASE_RESOLVE = f"https://huggingface.co/spaces/{SPACE_ID}/resolve/{REVISION}/"
29
  HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"}
30
 
31
+ def build(url_base: str, path: str) -> str:
32
+ return urljoin(url_base, path.lstrip("/"))
 
33
 
34
  def with_query(url: str, request: Request) -> str:
35
  q = str(request.url.query or "")
36
  return f"{url}?{q}" if q else url
37
 
38
+ def looks_like_file(path: str) -> bool:
39
+ # crude but effective: treat last segment with a dot as a file
40
+ return "." in path.split("/")[-1]
41
+
42
+ def guess_mime(path: str, default: str = "application/octet-stream") -> str:
43
  if path.endswith("/") or path == "":
44
  return "text/html; charset=utf-8"
45
  mime, _ = mimetypes.guess_type(path)
46
+ return mime or default
47
+
48
+ async def fetch(client: httpx.AsyncClient, path: str, request: Request) -> httpx.Response:
49
+ """
50
+ Try multiple resolve locations:
51
+ 1) path
52
+ 2) static/path
53
+ 3) if directory: path/index.html
54
+ 4) if directory: static/path/index.html
55
+ """
56
+ # 1) as-is
57
+ url = with_query(build(BASE_RESOLVE, path), request)
58
+ r = await client.get(url, headers=HEADERS)
59
+ if r.status_code == 200:
60
+ return r
61
+
62
+ # 2) static/<path>
63
+ url_static = with_query(build(BASE_RESOLVE, f"static/{path.lstrip('/')}"), request)
64
+ r2 = await client.get(url_static, headers=HEADERS)
65
+ if r2.status_code == 200:
66
+ return r2
67
+
68
+ # If it doesn't look like a file, try directory index.html in both places
69
+ if not looks_like_file(path):
70
+ # 3) path/index.html
71
+ url_dir = with_query(build(BASE_RESOLVE, path.rstrip("/") + "/index.html"), request)
72
+ r3 = await client.get(url_dir, headers=HEADERS)
73
+ if r3.status_code == 200:
74
+ return r3
75
+
76
+ # 4) static/path/index.html
77
+ url_dir_static = with_query(build(BASE_RESOLVE, f"static/{path.rstrip('/')}/index.html"), request)
78
+ r4 = await client.get(url_dir_static, headers=HEADERS)
79
+ if r4.status_code == 200:
80
+ return r4
81
+
82
+ # Return the "best" (most recent) response even if 404, so caller can show message
83
+ return r2 if r2.status_code != 404 else r
84
 
85
  @app.get("/health")
86
  async def health():
 
88
 
89
  @app.get("/")
90
  async def root(request: Request):
91
+ """
92
+ Serve index.html robustly:
93
+ - Try /index.html
94
+ - Try /static/index.html
95
+ - As a last resort, try repo root (may 404)
96
+ """
97
  try:
98
  async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
99
+ r = await fetch(client, "index.html", request)
100
+ if r.status_code == 404:
101
+ r = await fetch(client, "", request) # fallback
102
  except Exception as e:
103
+ return PlainTextResponse(f"Error fetching root: {e}", status_code=500)
 
 
 
 
 
 
 
 
 
104
 
105
  ctype = r.headers.get("content-type", "text/html; charset=utf-8")
106
+ # If HTML, return text; else raw bytes
107
+ if "text/html" in ctype:
108
+ return HTMLResponse(r.text, status_code=r.status_code, media_type=ctype)
109
+ return Response(content=r.content, status_code=r.status_code, media_type=ctype)
110
 
111
  @app.get("/{path:path}")
112
  async def proxy(path: str, request: Request):
113
+ """
114
+ Proxy all assets (CSS, JS, images, fonts, SPA routes).
115
+ Tries root, then static/, and directory index.html fallbacks.
116
+ """
 
117
  try:
118
  async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client:
119
+ r = await fetch(client, path, request)
120
  except Exception as e:
121
+ return PlainTextResponse(f"Error fetching {path}: {e}", status_code=500)
122
 
123
+ ctype = r.headers.get("content-type") or guess_mime(path)
 
124
  return Response(content=r.content, media_type=ctype, status_code=r.status_code)