cdn

Log | Files | Refs | Activity

root / app / main.py

main.py (9019B)


      1 import os
      2 import shutil
      3 import uuid
      4 import mimetypes
      5 import io
      6 import zipfile
      7 import httpx
      8 from pathlib import Path
      9 from fastapi import FastAPI, Request, File, UploadFile, HTTPException
     10 from fastapi.responses import StreamingResponse, HTMLResponse, PlainTextResponse, RedirectResponse, FileResponse
     11 from fastapi.templating import Jinja2Templates
     12 
     13 # Added format_size to the import list
     14 from .storage import is_file, list_directory, list_files_recursive, upload_temp_file, repo_stats, HF_REPO_ID, format_size
     15 
     16 app = FastAPI()
     17 
     18 templates = Jinja2Templates(directory="app/templates")
     19 
     20 STATIC_DIR = Path(__file__).parent / "static"
     21 
     22 _NO_STORE_FILES = {"manifest.json", "sw.js"}
     23 
     24 @app.get("/static/{filename}")
     25 async def static_no_cache_root(filename: str):
     26     if filename not in _NO_STORE_FILES:
     27         raise HTTPException(status_code=404)
     28     file_path = STATIC_DIR / filename
     29     if not file_path.is_file():
     30         raise HTTPException(status_code=404)
     31     return FileResponse(
     32         file_path,
     33         headers={"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"},
     34     )
     35 
     36 @app.get("/static/icons/{filename}")
     37 async def static_icons(filename: str):
     38     file_path = STATIC_DIR / "icons" / filename
     39     if not file_path.is_file():
     40         raise HTTPException(status_code=404)
     41     return FileResponse(
     42         file_path,
     43         headers={"Cache-Control": "public, max-age=86400"},
     44     )
     45 
     46 @app.get("/favicon.ico")
     47 async def favicon():
     48     return FileResponse(
     49         STATIC_DIR / "favicon.ico",
     50         headers={"Cache-Control": "public, max-age=86400"},
     51     )
     52 
     53 CDN_BASE_URL = os.getenv("CDN_BASE_URL", "https://cdn-zt7p.onrender.com")
     54 RAW_DOMAIN = os.getenv("RAW_DOMAIN", "raw.cdn.amit.is-a.dev")
     55 RAW_BASE_URL = os.getenv("RAW_BASE_URL", f"https://{RAW_DOMAIN}")
     56 RAW_PREFIX = "raw/"
     57 
     58 def render_context(extra: dict) -> dict:
     59     stats = repo_stats()
     60     ctx = dict(extra)
     61     ctx["repo_file_count"] = stats["file_count"] if stats else None
     62     ctx["repo_size_str"] = stats["size_str"] if stats else None
     63     return ctx
     64 
     65 @app.exception_handler(404)
     66 async def not_found_handler(request: Request, exc: HTTPException):
     67     if request.url.hostname == RAW_DOMAIN:
     68         return PlainTextResponse("404: File Not Found", status_code=404)
     69     return templates.TemplateResponse(request, "index.html", render_context({"page": "404"}), status_code=404)
     70 
     71 async def stream_raw(path: str):
     72     hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}"
     73     client = httpx.AsyncClient(follow_redirects=True)
     74     req = client.build_request("GET", hf_url)
     75     r = await client.send(req, stream=True)
     76     
     77     if r.status_code != 200:
     78         await client.aclose()
     79         raise HTTPException(status_code=404, detail="File not found")
     80     
     81     headers = {
     82         "Access-Control-Allow-Origin": "*",
     83         "Cache-Control": "public, max-age=31536000",
     84         "X-Content-Type-Options": "nosniff"
     85     }
     86     
     87     # Safely proxy critical headers (like Content-Encoding for gzip handling)
     88     for h in ["Content-Type", "Content-Encoding", "Content-Length", "Etag", "Accept-Ranges"]:
     89         if h in r.headers:
     90             headers[h] = r.headers[h]
     91     
     92     # Explicit override for text files in case HF serves as octet-stream
     93     filename = path.split("/")[-1].lower()
     94     if filename.endswith(".md") or filename.endswith(".txt"):
     95         if headers.get("Content-Type", "application/octet-stream") == "application/octet-stream":
     96             headers["Content-Type"] = "text/plain; charset=utf-8"
     97 
     98     async def stream_generator():
     99         try:
    100             # Using aiter_raw guarantees we don't accidentally decompress the stream if it's encoded
    101             async for chunk in r.aiter_raw():
    102                 yield chunk
    103         finally:
    104             await client.aclose()
    105             
    106     return StreamingResponse(stream_generator(), headers=headers)
    107 
    108 @app.get("/api/download/{path:path}")
    109 async def download_file(path: str):
    110     hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}"
    111     client = httpx.AsyncClient(follow_redirects=True)
    112     req = client.build_request("GET", hf_url)
    113     r = await client.send(req, stream=True)
    114     
    115     if r.status_code != 200:
    116         await client.aclose()
    117         raise HTTPException(status_code=404, detail="Not found")
    118     
    119     filename = path.split("/")[-1]
    120     headers = {
    121         "Content-Disposition": f'attachment; filename="{filename}"',
    122         "Content-Type": r.headers.get("Content-Type", "application/octet-stream")
    123     }
    124     
    125     # Forward length and encoding to allow browser progress bars and raw file integrity
    126     for h in ["Content-Length", "Content-Encoding", "Etag"]:
    127         if h in r.headers:
    128             headers[h] = r.headers[h]
    129     
    130     async def stream_generator():
    131         try:
    132             async for chunk in r.aiter_raw():
    133                 yield chunk
    134         finally:
    135             await client.aclose()
    136             
    137     return StreamingResponse(stream_generator(), headers=headers)
    138 
    139 @app.post("/api/upload")
    140 async def handle_upload(files: list[UploadFile] = File(...)):
    141     results = []
    142     for file in files:
    143         temp_path = f"/tmp/{uuid.uuid4()}-{file.filename}"
    144         with open(temp_path, "wb") as f:
    145             shutil.copyfileobj(file.file, f)
    146 
    147         try:
    148             hf_path = upload_temp_file(temp_path, file.filename)
    149         finally:
    150             if os.path.exists(temp_path):
    151                 os.remove(temp_path)
    152 
    153         results.append({
    154             "filename": file.filename,
    155             "cdn_url": f"{CDN_BASE_URL}/{hf_path}",
    156             "raw_url": f"{RAW_BASE_URL}/{hf_path}"
    157         })
    158 
    159     return {"files": results}
    160 
    161 @app.get("/api/zip-stats/{path:path}")
    162 async def zip_stats(path: str):
    163     clean_path = path.strip("/")
    164     try:
    165         files = list_files_recursive(clean_path)
    166     except ValueError as e:
    167         raise HTTPException(status_code=413, detail=str(e))
    168 
    169     if not files:
    170         raise HTTPException(status_code=404, detail="Folder is empty or not found")
    171 
    172     total_size = sum(f["size"] for f in files)
    173     return {
    174         "file_count": len(files),
    175         "total_size": total_size,
    176         "size_str": format_size(total_size)
    177     }
    178 
    179 @app.get("/api/download-zip/{path:path}")
    180 async def download_zip(path: str):
    181     clean_path = path.strip("/")
    182 
    183     try:
    184         files = list_files_recursive(clean_path)
    185     except ValueError as e:
    186         raise HTTPException(status_code=413, detail=str(e))
    187 
    188     if not files:
    189         raise HTTPException(status_code=404, detail="Folder is empty or not found")
    190 
    191     prefix_len = len(clean_path.rstrip("/")) + 1 if clean_path else 0
    192 
    193     buffer = io.BytesIO()
    194     async with httpx.AsyncClient(follow_redirects=True) as client:
    195         with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
    196             for f in files:
    197                 hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{f['path']}"
    198                 r = await client.get(hf_url)
    199                 if r.status_code != 200:
    200                     continue
    201                 arcname = f["path"][prefix_len:] if prefix_len else f["path"]
    202                 zf.writestr(arcname, r.content)
    203 
    204     buffer.seek(0)
    205     zip_filename = (clean_path.rstrip("/").split("/")[-1] if clean_path else HF_REPO_ID.split("/")[-1]) + ".zip"
    206 
    207     return StreamingResponse(
    208         buffer,
    209         media_type="application/zip",
    210         headers={"Content-Disposition": f'attachment; filename="{zip_filename}"'}
    211     )
    212 
    213 @app.api_route("/ping", methods=["GET", "HEAD"], response_class=PlainTextResponse)
    214 async def ping():
    215     return "Server is awake!"
    216 
    217 @app.api_route("/{path:path}", methods=["GET", "HEAD"])
    218 async def serve(request: Request, path: str):
    219     clean_path = path.strip("/")
    220 
    221     if request.url.hostname == RAW_DOMAIN:
    222         if not clean_path:
    223             return HTMLResponse("Specify a file path.", status_code=200)
    224         return await stream_raw(clean_path)
    225 
    226     if clean_path == RAW_PREFIX.rstrip("/") or clean_path.startswith(RAW_PREFIX):
    227         raw_path = clean_path[len(RAW_PREFIX):]
    228         if not raw_path:
    229             return HTMLResponse("/raw/ — specify a file path after this prefix.", status_code=200)
    230         return RedirectResponse(f"{RAW_BASE_URL}/{raw_path}")
    231 
    232     if clean_path == "upload":
    233         return templates.TemplateResponse(request, "index.html", render_context({"page": "upload"}))
    234     
    235     if clean_path and await is_file(clean_path):
    236         filename = clean_path.split("/")[-1]
    237         return templates.TemplateResponse(request, "index.html", render_context({
    238             "page": "file", 
    239             "path": clean_path,
    240             "filename": filename,
    241             "raw_base_url": RAW_BASE_URL
    242         }))
    243     
    244     items = list_directory(clean_path)
    245     if clean_path and not items:
    246         raise HTTPException(status_code=404, detail="Not Found")
    247 
    248     return templates.TemplateResponse(request, "index.html", render_context({
    249         "page": "listing",
    250         "path": clean_path,
    251         "items": items,
    252         "raw_base_url": RAW_BASE_URL
    253     }))
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror