cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
commit 34b4fc1fa2321c61fbb8393b5485e0a582e02e66
parent 39ec30ed89e102a3f9a8c2b5ea37b646695db99a
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Wed, 16 Sep 2026 22:58:27 +0530

Refactor HTTP exception handling and response formatting
Diffstat:
Mapp/main.py | 657+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 528 insertions(+), 129 deletions(-)

diff --git a/app/main.py b/app/main.py @@ -10,7 +10,6 @@ import tempfile import asyncio import socket import ipaddress -import random from urllib.parse import urlparse, unquote import httpx from collections import deque @@ -44,6 +43,7 @@ templates = Jinja2Templates(directory="app/templates") STATIC_DIR = Path(__file__).parent / "static" _NO_STORE_FILES = {"manifest.json", "sw.js"} + @app.get("/static/{filename}") async def static_no_cache_root(filename: str): if filename not in _NO_STORE_FILES: @@ -56,6 +56,7 @@ async def static_no_cache_root(filename: str): headers={"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"}, ) + @app.get("/static/icons/{filename}") async def static_icons(filename: str): file_path = STATIC_DIR / "icons" / filename @@ -66,6 +67,7 @@ async def static_icons(filename: str): headers={"Cache-Control": "public, max-age=86400"}, ) + @app.get("/favicon.ico") async def favicon(): return FileResponse( @@ -73,11 +75,13 @@ async def favicon(): headers={"Cache-Control": "public, max-age=86400"}, ) + CDN_BASE_URL = os.getenv("CDN_BASE_URL", "https://cdn.amit.is-a.dev") RAW_DOMAIN = os.getenv("RAW_DOMAIN", "raw.cdn.amit.is-a.dev") RAW_BASE_URL = os.getenv("RAW_BASE_URL", f"https://{RAW_DOMAIN}") RAW_PREFIX = "raw/" + async def render_context(extra: dict) -> dict: stats = await repo_stats() ctx = dict(extra) @@ -87,6 +91,7 @@ async def render_context(extra: dict) -> dict: ctx.setdefault("raw_base_url", RAW_BASE_URL) return ctx + @app.exception_handler(404) async def not_found_handler(request: Request, exc: HTTPException): if request.url.hostname == RAW_DOMAIN: @@ -94,6 +99,7 @@ async def not_found_handler(request: Request, exc: HTTPException): ctx = await render_context({"page": "404"}) return templates.TemplateResponse(request, "index.html", ctx, status_code=404) + async def _proxy_stream(client: httpx.AsyncClient, r: httpx.Response): try: async for chunk in r.aiter_raw(): @@ -101,6 +107,7 @@ async def _proxy_stream(client: httpx.AsyncClient, r: httpx.Response): finally: await client.aclose() + async def stream_raw(path: str, request: Request): hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}" client = httpx.AsyncClient(follow_redirects=True) @@ -110,28 +117,33 @@ async def stream_raw(path: str, request: Request): req_headers["Range"] = range_header req = client.build_request("GET", hf_url, headers=req_headers) r = await client.send(req, stream=True) - + if r.status_code not in (200, 206): await client.aclose() raise HTTPException(status_code=404, detail="File not found") - + headers = { "Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=31536000", "X-Content-Type-Options": "nosniff", "Accept-Ranges": "bytes", } - + for h in ["Content-Type", "Content-Encoding", "Content-Length", "Etag", "Content-Range"]: if h in r.headers: headers[h] = r.headers[h] - + filename = path.split("/")[-1].lower() if filename.endswith(".md") or filename.endswith(".txt"): if headers.get("Content-Type", "application/octet-stream") == "application/octet-stream": headers["Content-Type"] = "text/plain; charset=utf-8" - - return StreamingResponse(_proxy_stream(client, r), status_code=r.status_code, headers=headers) + + return StreamingResponse( + _proxy_stream(client, r), + status_code=r.status_code, + headers=headers, + ) + @app.get("/api/get/{path:path}") async def download_file(path: str, request: Request): @@ -143,26 +155,33 @@ async def download_file(path: str, request: Request): req_headers["Range"] = range_header req = client.build_request("GET", hf_url, headers=req_headers) r = await client.send(req, stream=True) - + if r.status_code not in (200, 206): await client.aclose() raise HTTPException(status_code=404, detail="Not found") - + filename = path.split("/")[-1] headers = { "Content-Disposition": f'attachment; filename="{filename}"', "Content-Type": r.headers.get("Content-Type", "application/octet-stream"), "Accept-Ranges": "bytes", } + for h in ["Content-Length", "Content-Encoding", "Etag", "Content-Range"]: if h in r.headers: headers[h] = r.headers[h] - - return StreamingResponse(_proxy_stream(client, r), status_code=r.status_code, headers=headers) + + return StreamingResponse( + _proxy_stream(client, r), + status_code=r.status_code, + headers=headers, + ) + UPLOAD_LIMIT_PER_MINUTE = 50 * 1024 * 1024 UPLOAD_LIMIT_PER_HOUR = 300 * 1024 * 1024 + class _UploadRateLimiter: def __init__(self): self._usage: dict[str, deque] = {} @@ -186,6 +205,7 @@ class _UploadRateLimiter: status_code=429, detail=f"Upload rate limit exceeded: {format_size(UPLOAD_LIMIT_PER_MINUTE)}/minute. Try again shortly.", ) + if hour_used + size > UPLOAD_LIMIT_PER_HOUR: raise HTTPException( status_code=429, @@ -194,10 +214,12 @@ class _UploadRateLimiter: dq.append((now, size)) + _upload_limiter = _UploadRateLimiter() BATCH_MIN_FILES = 2 ALLOWED_UPLOAD_FOLDERS = {"uploads", "third-party"} + def _validate_folder(folder: str) -> str: folder = (folder or "uploads").strip().strip("/") if folder not in ALLOWED_UPLOAD_FOLDERS: @@ -207,6 +229,7 @@ def _validate_folder(folder: str) -> str: ) return folder + @app.post("/api/put") async def handle_upload( request: Request, @@ -219,8 +242,10 @@ async def handle_upload( for file in files: temp_path = f"/tmp/{uuid.uuid4()}-{file.filename}" + with open(temp_path, "wb") as f: shutil.copyfileobj(file.file, f) + size = os.path.getsize(temp_path) try: @@ -244,6 +269,7 @@ async def handle_upload( }) response = {"files": results} + if len(results) >= BATCH_MIN_FILES: batch_id = uuid.uuid4().hex[:10] await write_batch_manifest(batch_id, results) @@ -252,38 +278,69 @@ async def handle_upload( return response + GH_SYNC_LIMIT_PER_HOUR = 2 * 1024 * 1024 * 1024 _gh_sync_limiter = _UploadRateLimiter() + def _safe_extract_tar(tar: tarfile.TarFile, dest: str): dest_real = os.path.realpath(dest) + for member in tar.getmembers(): member_path = os.path.realpath(os.path.join(dest, member.name)) + if not member_path.startswith(dest_real + os.sep) and member_path != dest_real: - raise HTTPException(status_code=400, detail="Archive contains an unsafe path.") + raise HTTPException( + status_code=400, + detail="Archive contains an unsafe path.", + ) + if member.issym() or member.islnk(): - raise HTTPException(status_code=400, detail="Archive contains symlinks, which are not allowed.") + raise HTTPException( + status_code=400, + detail="Archive contains symlinks, which are not allowed.", + ) + tar.extractall(dest) + @app.get("/gh-sync") async def gh_sync_docs(request: Request): - return templates.TemplateResponse(request, "gh_sync_docs.html", {"page": "gh-sync"}) + return templates.TemplateResponse( + request, + "gh_sync_docs.html", + {"page": "gh-sync"}, + ) + @app.get("/documentation") async def documentation(request: Request): - return templates.TemplateResponse(request, "documentation.html", {"page": "documentation"}) + return templates.TemplateResponse( + request, + "documentation.html", + {"page": "documentation"}, + ) + @app.post("/api/gh-sync") -async def gh_sync(request: Request, token: str = Form(...), archive: UploadFile = File(...)): +async def gh_sync( + request: Request, + token: str = Form(...), + archive: UploadFile = File(...), +): claims = verify_actions_token(token) owner, repo = claims["owner"], claims["repo"] client_ip = request.client.host if request.client else "unknown" contents = await archive.read() - _gh_sync_limiter.check_and_record(f"gh-sync:{owner}/{repo}:{client_ip}", len(contents)) + _gh_sync_limiter.check_and_record( + f"gh-sync:{owner}/{repo}:{client_ip}", + len(contents), + ) with tempfile.TemporaryDirectory() as tmp_upload, tempfile.TemporaryDirectory() as tmp_extract: archive_path = os.path.join(tmp_upload, "payload.tar.gz") + with open(archive_path, "wb") as f: f.write(contents) @@ -291,9 +348,16 @@ async def gh_sync(request: Request, token: str = Form(...), archive: UploadFile with tarfile.open(archive_path, "r:gz") as tar: _safe_extract_tar(tar, tmp_extract) except tarfile.TarError: - raise HTTPException(status_code=400, detail="Could not read archive (expected a .tar.gz).") + raise HTTPException( + status_code=400, + detail="Could not read archive (expected a .tar.gz).", + ) - dest_prefix = await upload_folder_scoped(tmp_extract, owner, repo) + dest_prefix = await upload_folder_scoped( + tmp_extract, + owner, + repo, + ) return { "synced_to": dest_prefix, @@ -303,14 +367,17 @@ async def gh_sync(request: Request, token: str = Form(...), archive: UploadFile "ref": claims["ref"], } + @app.get("/ytmusic") async def url_ytmusic_page(request: Request): ctx = await render_context({"page": "ytmusic"}) return templates.TemplateResponse(request, "ytmusic.html", ctx) + # --------------------------------------------------------------------------------- # YTMusic routes: Cobalt API Proxy # --------------------------------------------------------------------------------- + class YtRequest(BaseModel): url: str @@ -328,12 +395,6 @@ COBALT_PUBLIC_PREFIX = "/cobalt" methods=["GET", "HEAD"], ) async def cobalt_tunnel_proxy(path: str, request: Request): - """ - Proxy Cobalt tunnel requests through the same Render service. - - Cobalt itself listens only on 127.0.0.1:9000, while this FastAPI - application is the only publicly exposed process. - """ cobalt_url = f"{COBALT_INTERNAL_URL}/{path}" query_string = request.url.query @@ -342,30 +403,52 @@ async def cobalt_tunnel_proxy(path: str, request: Request): headers = {} - for header in ( + for header in [ "Range", "If-Range", "If-None-Match", "If-Modified-Since", - ): + ]: value = request.headers.get(header) if value: headers[header] = value client = httpx.AsyncClient( follow_redirects=False, - timeout=None, + timeout=60.0, ) try: - upstream = await client.send( - client.build_request( - request.method, - cobalt_url, - headers=headers, - ), - stream=True, + req = client.build_request( + request.method, + cobalt_url, + headers=headers, + ) + + res = await client.send(req, stream=True) + + response_headers = {} + + for header in [ + "Content-Type", + "Content-Length", + "Content-Range", + "Accept-Ranges", + "Content-Disposition", + "Cache-Control", + "ETag", + "Last-Modified", + ]: + value = res.headers.get(header) + if value: + response_headers[header] = value + + return StreamingResponse( + _proxy_stream(client, res), + status_code=res.status_code, + headers=response_headers, ) + except httpx.RequestError as exc: await client.aclose() raise HTTPException( @@ -373,36 +456,6 @@ async def cobalt_tunnel_proxy(path: str, request: Request): detail=f"Error connecting to Cobalt tunnel: {exc}", ) - response_headers = {} - - for header in ( - "content-type", - "content-length", - "content-range", - "accept-ranges", - "content-disposition", - "cache-control", - "etag", - "last-modified", - "estimated-content-length", - ): - value = upstream.headers.get(header) - if value: - response_headers[header] = value - - async def stream_cobalt(): - try: - async for chunk in upstream.aiter_raw(): - yield chunk - finally: - await client.aclose() - - return StreamingResponse( - stream_cobalt(), - status_code=upstream.status_code, - headers=response_headers, - ) - @app.post("/api/yt/download") async def yt_download_cobalt(request: Request, body: YtRequest): @@ -486,13 +539,13 @@ async def yt_download_cobalt(request: Request, body: YtRequest): print("========== COBALT ERROR ==========") print(f"Full response: {data}") - + if isinstance(error, dict): print(f"Error code: {error.get('code')}") print(f"Error context: {error.get('context')}") else: print(f"Error: {error}") - + print("==================================") if isinstance(error, dict): @@ -526,8 +579,6 @@ async def yt_download_cobalt(request: Request, body: YtRequest): ), ) - # Cobalt may return its own tunnel URL. - # Convert it to our public FastAPI proxy URL. if status == "tunnel": parsed = urlparse(dl_url) @@ -555,57 +606,170 @@ async def yt_download_cobalt(request: Request, body: YtRequest): # --------------------------------------------------------------------------------- +# TEMPORARY COBALT DIAGNOSTIC ENDPOINT +# --------------------------------------------------------------------------------- + +@app.get("/api/yt/debug") +async def yt_debug(url: str): + url = url.strip() + + if not url: + raise HTTPException( + status_code=400, + detail="Missing YouTube URL.", + ) + + cobalt_api = f"{COBALT_INTERNAL_URL}/" + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.36" + ), + } + + cobalt_api_token = os.getenv("COBALT_API_TOKEN") + + if cobalt_api_token: + if " " not in cobalt_api_token: + headers["Authorization"] = f"Api-Key {cobalt_api_token}" + else: + headers["Authorization"] = cobalt_api_token + + payload = { + "url": url, + "downloadMode": "audio", + "audioFormat": "mp3", + "audioBitrate": "128", + "filenameStyle": "basic", + } + + async with httpx.AsyncClient( + timeout=60.0, + follow_redirects=False, + ) as client: + try: + res = await client.post( + cobalt_api, + json=payload, + headers=headers, + ) + + raw_text = res.text + + try: + response_body = res.json() + except Exception: + response_body = raw_text + + print("========== COBALT DEBUG ==========") + print(f"HTTP status: {res.status_code}") + print(f"Response headers: {dict(res.headers)}") + print(f"Response body: {response_body}") + print("===================================") + + return { + "cobalt_url": cobalt_api, + "http_status": res.status_code, + "response_headers": dict(res.headers), + "response": response_body, + } + + except httpx.RequestError as exc: + print("========== COBALT DEBUG ERROR ==========") + print(f"Request error: {repr(exc)}") + print("========================================") + + raise HTTPException( + status_code=502, + detail={ + "message": "Error connecting to Cobalt", + "error": repr(exc), + }, + ) + + +# --------------------------------------------------------------------------------- @app.get("/shorten") async def url_shortener_page(request: Request): ctx = await render_context({"page": "shorten"}) return templates.TemplateResponse(request, "url_shortener.html", ctx) + class ShortenRequest(BaseModel): url: str + @app.post("/api/shorten") async def api_shorten(request: Request, body: ShortenRequest): url = body.url.strip() _assert_public_url(url) short_id = await shorten_url(url) + return { "id": short_id, "masked_url": f"{CDN_BASE_URL}/mask/{short_id}", } + @app.get("/mask/{short_id}") async def mask_redirect(request: Request, short_id: str): destination = await get_destination_url(short_id) + if destination is None: - raise HTTPException(status_code=404, detail="Short URL not found") + raise HTTPException( + status_code=404, + detail="Short URL not found", + ) + return RedirectResponse( destination, status_code=307, - headers={"Referrer-Policy": "no-referrer"} + headers={"Referrer-Policy": "no-referrer"}, ) + _MAX_URL_UPLOAD_BYTES = UPLOAD_LIMIT_PER_HOUR + def _assert_public_url(url: str): parsed = urlparse(url) + if parsed.scheme != "https": - raise HTTPException(status_code=400, detail="Only https:// URLs are supported.") + raise HTTPException( + status_code=400, + detail="Only https:// URLs are supported.", + ) + if not parsed.hostname: - raise HTTPException(status_code=400, detail="Invalid URL.") + raise HTTPException( + status_code=400, + detail="Invalid URL.", + ) + hostname_lower = parsed.hostname.lower() + if hostname_lower == "localhost" or hostname_lower.endswith(".localhost"): raise HTTPException( status_code=400, detail="URLs pointing to private/internal addresses are not allowed.", ) + try: addrinfos = socket.getaddrinfo(parsed.hostname, None) except socket.gaierror: - raise HTTPException(status_code=400, detail="Could not resolve host.") - + raise HTTPException( + status_code=400, + detail="Could not resolve host.", + ) + for family, _, _, _, sockaddr in addrinfos: ip = ipaddress.ip_address(sockaddr[0]) + if ( ip.is_private or ip.is_loopback @@ -619,33 +783,54 @@ def _assert_public_url(url: str): detail="URLs pointing to private/internal addresses are not allowed.", ) + class UploadUrlRequest(BaseModel): url: str folder: str = "uploads" + @app.post("/api/put-url") -async def handle_upload_from_url(request: Request, body: UploadUrlRequest): +async def handle_upload_from_url( + request: Request, + body: UploadUrlRequest, +): folder = _validate_folder(body.folder) client_ip = request.client.host if request.client else "unknown" url = body.url.strip() + _assert_public_url(url) + parsed = urlparse(url) - filename = os.path.basename(unquote(parsed.path)) or f"download-{uuid.uuid4().hex[:8]}" + filename = ( + os.path.basename(unquote(parsed.path)) + or f"download-{uuid.uuid4().hex[:8]}" + ) + temp_path = f"/tmp/{uuid.uuid4()}-{filename}" downloaded = 0 - async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client: + async with httpx.AsyncClient( + follow_redirects=True, + timeout=30.0, + ) as client: try: async with client.stream("GET", url) as r: if r.status_code != 200: - raise HTTPException(status_code=400, detail=f"Source returned status {r.status_code}.") + raise HTTPException( + status_code=400, + detail=f"Source returned status {r.status_code}.", + ) + content_length = r.headers.get("Content-Length") + if content_length and int(content_length) > _MAX_URL_UPLOAD_BYTES: raise HTTPException( status_code=413, detail=f"File too large (limit {format_size(_MAX_URL_UPLOAD_BYTES)} per fetch).", ) + cd = r.headers.get("Content-Disposition", "") + if "filename=" in cd: filename = cd.split("filename=")[-1].strip('"; ') or filename temp_path = f"/tmp/{uuid.uuid4()}-{filename}" @@ -653,29 +838,44 @@ async def handle_upload_from_url(request: Request, body: UploadUrlRequest): with open(temp_path, "wb") as f: async for chunk in r.aiter_bytes(): downloaded += len(chunk) + if downloaded > _MAX_URL_UPLOAD_BYTES: raise HTTPException( status_code=413, detail=f"File too large (limit {format_size(_MAX_URL_UPLOAD_BYTES)} per fetch).", ) + f.write(chunk) + except httpx.RequestError: if os.path.exists(temp_path): os.remove(temp_path) - raise HTTPException(status_code=400, detail="Could not fetch the URL.") + + raise HTTPException( + status_code=400, + detail="Could not fetch the URL.", + ) + except HTTPException: if os.path.exists(temp_path): os.remove(temp_path) raise try: - _upload_limiter.check_and_record(client_ip, downloaded) + _upload_limiter.check_and_record( + client_ip, + downloaded, + ) except HTTPException: os.remove(temp_path) raise try: - hf_path = await upload_temp_file(temp_path, filename, folder) + hf_path = await upload_temp_file( + temp_path, + filename, + folder, + ) finally: if os.path.exists(temp_path): os.remove(temp_path) @@ -690,176 +890,360 @@ async def handle_upload_from_url(request: Request, body: UploadUrlRequest): }] } + @app.get("/api/pack-info/{path:path}") async def zip_stats(path: str): clean_path = path.strip("/") + try: files = await list_files_recursive(clean_path) except ValueError as e: - raise HTTPException(status_code=413, detail=str(e)) + raise HTTPException( + status_code=413, + detail=str(e), + ) + if not files: - raise HTTPException(status_code=404, detail="Folder is empty or not found") + raise HTTPException( + status_code=404, + detail="Folder is empty or not found", + ) + total_size = sum(f["size"] for f in files) + return { "file_count": len(files), "total_size": total_size, "size_str": format_size(total_size), } + @app.get("/api/pack/{path:path}") async def download_zip(path: str): clean_path = path.strip("/") + try: files = await list_files_recursive(clean_path) except ValueError as e: - raise HTTPException(status_code=413, detail=str(e)) + raise HTTPException( + status_code=413, + detail=str(e), + ) + if not files: - raise HTTPException(status_code=404, detail="Folder is empty or not found") + raise HTTPException( + status_code=404, + detail="Folder is empty or not found", + ) + + prefix_len = ( + len(clean_path.rstrip("/")) + 1 + if clean_path + else 0 + ) - prefix_len = len(clean_path.rstrip("/")) + 1 if clean_path else 0 ZIP_FETCH_CONCURRENCY = 8 semaphore = asyncio.Semaphore(ZIP_FETCH_CONCURRENCY) async def fetch(client: httpx.AsyncClient, f: dict): - hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{f['path']}" + hf_url = ( + f"https://huggingface.co/datasets/" + f"{HF_REPO_ID}/resolve/main/{f['path']}" + ) + async with semaphore: r = await client.get(hf_url) + return f, r buffer = io.BytesIO() - async with httpx.AsyncClient(follow_redirects=True) as client: - results = await asyncio.gather(*(fetch(client, f) for f in files)) - with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf: + + async with httpx.AsyncClient( + follow_redirects=True, + ) as client: + results = await asyncio.gather( + *(fetch(client, f) for f in files) + ) + + with zipfile.ZipFile( + buffer, + "w", + zipfile.ZIP_DEFLATED, + ) as zf: for f, r in results: if r.status_code != 200: continue - arcname = f["path"][prefix_len:] if prefix_len else f["path"] - zf.writestr(arcname, r.content) + + arcname = ( + f["path"][prefix_len:] + if prefix_len + else f["path"] + ) + + zf.writestr( + arcname, + r.content, + ) buffer.seek(0) - zip_filename = (clean_path.rstrip("/").split("/")[-1] if clean_path else HF_REPO_ID.split("/")[-1]) + ".zip" + + zip_filename = ( + clean_path.rstrip("/").split("/")[-1] + if clean_path + else HF_REPO_ID.split("/")[-1] + ) + ".zip" + return StreamingResponse( buffer, media_type="application/zip", - headers={"Content-Disposition": f'attachment; filename="{zip_filename}"'}, + headers={ + "Content-Disposition": ( + f'attachment; filename="{zip_filename}"' + ) + }, ) + @app.get("/api/find") async def api_search(q: str = ""): results = await search_files(q) return {"results": results} + @app.get("/pack/{batch_id}") async def batch_page(request: Request, batch_id: str): manifest = await get_batch_manifest(batch_id) + if not manifest: - raise HTTPException(status_code=404, detail="Batch not found") + raise HTTPException( + status_code=404, + detail="Batch not found", + ) + batch_files = [] + for f in manifest.get("files", []): item = dict(f) - item["size_str"] = format_size(f.get("size")) + item["size_str"] = format_size( + f.get("size") + ) batch_files.append(item) + ctx = await render_context({ "page": "batch", "batch_id": batch_id, "batch_files": batch_files, }) - return templates.TemplateResponse(request, "index.html", ctx) + + return templates.TemplateResponse( + request, + "index.html", + ctx, + ) + @app.get("/api/pack-info-batch/{batch_id}") async def zip_stats_batch(batch_id: str): manifest = await get_batch_manifest(batch_id) + if not manifest: - raise HTTPException(status_code=404, detail="Batch not found") + raise HTTPException( + status_code=404, + detail="Batch not found", + ) + files = manifest.get("files", []) + if not files: - raise HTTPException(status_code=404, detail="Batch is empty") - total_size = sum(f.get("size", 0) or 0 for f in files) + raise HTTPException( + status_code=404, + detail="Batch is empty", + ) + + total_size = sum( + f.get("size", 0) or 0 + for f in files + ) + return { "file_count": len(files), "total_size": total_size, "size_str": format_size(total_size), } + @app.get("/api/pack-batch/{batch_id}") async def download_zip_batch(batch_id: str): manifest = await get_batch_manifest(batch_id) + if not manifest: - raise HTTPException(status_code=404, detail="Batch not found") + raise HTTPException( + status_code=404, + detail="Batch not found", + ) + files = manifest.get("files", []) + if not files: - raise HTTPException(status_code=404, detail="Batch is empty") + raise HTTPException( + status_code=404, + detail="Batch is empty", + ) + ZIP_FETCH_CONCURRENCY = 8 semaphore = asyncio.Semaphore(ZIP_FETCH_CONCURRENCY) async def fetch(client: httpx.AsyncClient, f: dict): - hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{f['hf_path']}" + hf_url = ( + f"https://huggingface.co/datasets/" + f"{HF_REPO_ID}/resolve/main/{f['hf_path']}" + ) + async with semaphore: r = await client.get(hf_url) + return f, r seen_names = set() buffer = io.BytesIO() - async with httpx.AsyncClient(follow_redirects=True) as client: - results = await asyncio.gather(*(fetch(client, f) for f in files)) - with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf: + + async with httpx.AsyncClient( + follow_redirects=True, + ) as client: + results = await asyncio.gather( + *(fetch(client, f) for f in files) + ) + + with zipfile.ZipFile( + buffer, + "w", + zipfile.ZIP_DEFLATED, + ) as zf: for f, r in results: if r.status_code != 200: continue - arcname = f.get("filename") or f["hf_path"].split("/")[-1] + + arcname = ( + f.get("filename") + or f["hf_path"].split("/")[-1] + ) + if arcname in seen_names: - arcname = f"{f['hf_path'].split('/')[-1].split('-')[0]}-{arcname}" + arcname = ( + f"{f['hf_path'].split('/')[-1].split('-')[0]}" + f"-{arcname}" + ) + seen_names.add(arcname) - zf.writestr(arcname, r.content) + + zf.writestr( + arcname, + r.content, + ) buffer.seek(0) + return StreamingResponse( buffer, media_type="application/zip", - headers={"Content-Disposition": f'attachment; filename="batch-{batch_id}.zip"'}, + headers={ + "Content-Disposition": ( + f'attachment; filename="batch-{batch_id}.zip"' + ) + }, ) -@app.api_route("/ping", methods=["GET", "HEAD"], response_class=PlainTextResponse) + +@app.api_route( + "/ping", + methods=["GET", "HEAD"], + response_class=PlainTextResponse, +) async def ping(): return "Server is awake!" -@app.api_route("/{path:path}", methods=["GET", "HEAD"]) + +@app.api_route( + "/{path:path}", + methods=["GET", "HEAD"], +) async def serve(request: Request, path: str): clean_path = path.strip("/") - + if request.url.hostname == RAW_DOMAIN: if not clean_path: - return HTMLResponse("Specify a file path.", status_code=200) + return HTMLResponse( + "Specify a file path.", + status_code=200, + ) + try: - return await stream_raw(clean_path, request) + return await stream_raw( + clean_path, + request, + ) except HTTPException as e: if e.status_code == 404: entries = await list_directory(clean_path) + if entries: - return RedirectResponse(f"{CDN_BASE_URL}/{clean_path}/") + return RedirectResponse( + f"{CDN_BASE_URL}/{clean_path}/" + ) + raise - if clean_path == RAW_PREFIX.rstrip("/") or clean_path.startswith(RAW_PREFIX): + if ( + clean_path == RAW_PREFIX.rstrip("/") + or clean_path.startswith(RAW_PREFIX) + ): raw_path = clean_path[len(RAW_PREFIX):] + if not raw_path: - return HTMLResponse("/raw/ specify a file path after this prefix.", status_code=200) - return RedirectResponse(f"{RAW_BASE_URL}/{raw_path}") + return HTMLResponse( + "/raw/ specify a file path after this prefix.", + status_code=200, + ) + + return RedirectResponse( + f"{RAW_BASE_URL}/{raw_path}" + ) if clean_path == "new": ctx = await render_context({"page": "upload"}) - return templates.TemplateResponse(request, "index.html", ctx) + return templates.TemplateResponse( + request, + "index.html", + ctx, + ) + if clean_path == "find": ctx = await render_context({"page": "search"}) - return templates.TemplateResponse(request, "index.html", ctx) + return templates.TemplateResponse( + request, + "index.html", + ctx, + ) + if clean_path == "history": ctx = await render_context({"page": "history"}) - return templates.TemplateResponse(request, "index.html", ctx) + return templates.TemplateResponse( + request, + "index.html", + ctx, + ) if clean_path: info = await get_file_info(clean_path) + if info["exists"]: filename = clean_path.split("/")[-1] - ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" + ext = ( + filename.rsplit(".", 1)[-1].lower() + if "." in filename + else "" + ) + ctx = await render_context({ "page": "file", "path": clean_path, @@ -868,20 +1252,35 @@ async def serve(request: Request, path: str): "file_ext": ext, "file_size_str": format_size(info["size"]), }) - return templates.TemplateResponse(request, "index.html", ctx) + + return templates.TemplateResponse( + request, + "index.html", + ctx, + ) items = await list_directory(clean_path) + if clean_path and not items: - raise HTTPException(status_code=404, detail="Not Found") - + raise HTTPException( + status_code=404, + detail="Not Found", + ) + extra_ctx = { "page": "listing", "path": clean_path, "items": items, "raw_base_url": RAW_BASE_URL, } + if not clean_path: extra_ctx["recent_activity"] = await get_recent_activity() - + ctx = await render_context(extra_ctx) - return templates.TemplateResponse(request, "index.html", ctx) + + return templates.TemplateResponse( + request, + "index.html", + ctx, + )