cdn

Log | Files | Refs | Activity

commit 44d3b81a84a2b9406ccec7f71045a780b31e0cae
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Thu,  3 Sep 2026 15:11:57 +0530

Add files via upload
Diffstat:
ADockerfile | 21+++++++++++++++++++++
Aapp/auth.py | 17+++++++++++++++++
Aapp/main.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/storage.py | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/templates/index.html | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/ytmusic.py | 48++++++++++++++++++++++++++++++++++++++++++++++++
Arender.yaml | 13+++++++++++++
Arequirements.txt | 9+++++++++
8 files changed, 484 insertions(+), 0 deletions(-)

diff --git a/Dockerfile b/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +# Phase 6: Includes ffmpeg required by yt-dlp to extract/convert to MP3 +RUN apt-get update && \ + apt-get install -y ffmpeg && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the app structure +COPY app/ ./app/ + +EXPOSE 8000 + +# --proxy-headers lets FastAPI see real client IPs (needed for slowapi +# rate-limiting behind Render's load balancer) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]+ \ No newline at end of file diff --git a/app/auth.py b/app/auth.py @@ -0,0 +1,16 @@ +import os +from fastapi import Header, HTTPException + +# Phase 6: Security +ADMIN_TOKEN = os.getenv("ADMIN_TOKEN") + +def verify_token(authorization: str = Header(None)): + if not ADMIN_TOKEN: + raise HTTPException(status_code=500, detail="Server missing ADMIN_TOKEN configuration") + + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Unauthorized") + + token = authorization.split(" ")[1] + if token != ADMIN_TOKEN: + raise HTTPException(status_code=401, detail="Unauthorized")+ \ No newline at end of file diff --git a/app/main.py b/app/main.py @@ -0,0 +1,127 @@ +import os +import shutil +import httpx +from fastapi import FastAPI, Request, Form, File, UploadFile, Depends, HTTPException, BackgroundTasks +from fastapi.responses import StreamingResponse, HTMLResponse +from fastapi.templating import Jinja2Templates +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +from .storage import is_file, list_directory, upload_temp_file, HF_REPO_ID +from .ytmusic import process_and_stream_ytmusic +from .auth import verify_token + +# Phase 6: Rate Limiting setup +limiter = Limiter(key_func=get_remote_address) +app = FastAPI() +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +templates = Jinja2Templates(directory="app/templates") + +# Phase 2: Dual-domain routing middleware +@app.middleware("http") +async def route_by_host(request: Request, call_next): + host = request.headers.get("host", "") + request.state.is_raw = host.startswith("raw.") + return await call_next(request) + +async def stream_raw(path: str): + """Streams file from Hugging Face for the raw. domain.""" + hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}" + client = httpx.AsyncClient() + req = client.build_request("GET", hf_url) + r = await client.send(req, stream=True) + + if r.status_code != 200: + raise HTTPException(status_code=404, detail="File not found") + + headers = { + "Access-Control-Allow-Origin": "*", + "Cache-Control": "public, max-age=31536000", + "Content-Type": r.headers.get("Content-Type", "application/octet-stream") + } + return StreamingResponse(r.aiter_raw(), headers=headers) + +@app.get("/api/download/{path:path}") +async def download_file(path: str): + """Forces a file download from the cdn. domain.""" + hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}" + client = httpx.AsyncClient() + req = client.build_request("GET", hf_url) + r = await client.send(req, stream=True) + + if r.status_code != 200: + 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") + } + return StreamingResponse(r.aiter_raw(), headers=headers) + +# Phase 4: Token-protected Upload endpoint +@app.post("/api/upload") +async def handle_upload(file: UploadFile = File(...), _ = Depends(verify_token)): + temp_path = f"/tmp/{file.filename}" + with open(temp_path, "wb") as f: + shutil.copyfileobj(file.file, f) + + try: + hf_path = upload_temp_file(temp_path, file.filename) + finally: + if os.path.exists(temp_path): + os.remove(temp_path) + + return { + "cdn_url": f"https://cdn.amit.is-a.dev/{hf_path}", + "raw_url": f"https://raw.cdn.amit.is-a.dev/{hf_path}" + } + +# Phase 5: Public, rate-limited YouTube Music downloader +@app.post("/api/ytmusic") +@limiter.limit("10/hour") +async def ytmusic_endpoint(request: Request, url: str = Form(...), background_tasks: BackgroundTasks = BackgroundTasks()): + try: + return process_and_stream_ytmusic(url, background_tasks) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/{path:path}") +async def serve(request: Request, path: str): + # Route raw traffic + if request.state.is_raw: + if not path: + return HTMLResponse("raw. domain root. Please specify a file path.", status_code=200) + return await stream_raw(path) + + # Route CDN traffic + clean_path = path.strip("/") + + if clean_path == "upload": + return templates.TemplateResponse("index.html", {"request": request, "page": "upload"}) + if clean_path == "ytmusic": + return templates.TemplateResponse("index.html", {"request": request, "page": "ytmusic"}) + + # Phase 1/2: UI Rendering (File vs Directory logic) + if clean_path and await is_file(clean_path): + filename = clean_path.split("/")[-1] + return templates.TemplateResponse("index.html", { + "request": request, + "page": "file", + "path": clean_path, + "filename": filename + }) + + items = list_directory(clean_path) + if clean_path and not items: + raise HTTPException(status_code=404, detail="Not Found") + + return templates.TemplateResponse("index.html", { + "request": request, + "page": "listing", + "path": clean_path, + "items": items + })+ \ No newline at end of file diff --git a/app/storage.py b/app/storage.py @@ -0,0 +1,66 @@ +import os +import uuid +import httpx +from huggingface_hub import HfApi + +# Phase 1: Storage Layer +HF_REPO_ID = os.getenv("HF_REPO_ID", "notamitgamer/cdn") +HF_TOKEN = os.getenv("HF_TOKEN") + +api = HfApi(token=HF_TOKEN) + +async def is_file(path: str) -> bool: + """Check if a path in the HF dataset is a file using a quick HEAD request.""" + if not path: + return False + hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}" + async with httpx.AsyncClient() as client: + r = await client.head(hf_url) + return r.status_code == 200 + +def list_directory(path: str): + """Returns list of dicts for items in directory, sorted folders first.""" + try: + items = list(api.list_repo_tree(repo_id=HF_REPO_ID, path_in_repo=path, repo_type="dataset")) + except Exception: + return [] + + files_and_folders = [] + for item in items: + name = item.path.split("/")[-1] + is_dir = not hasattr(item, "size") + size = getattr(item, "size", 0) + + size_str = "-" + if not is_dir and size is not None: + if size < 1024: + size_str = f"{size} B" + elif size < 1024 * 1024: + size_str = f"{size / 1024:.1f} KB" + else: + size_str = f"{size / (1024 * 1024):.1f} MB" + + files_and_folders.append({ + "name": name, + "path": item.path, + "is_dir": is_dir, + "size_str": size_str + }) + + files_and_folders.sort(key=lambda x: (not x["is_dir"], x["name"].lower())) + return files_and_folders + +def upload_temp_file(temp_path: str, filename: str) -> str: + """Uploads a local file to HF and returns the relative path.""" + slug = str(uuid.uuid4())[:8] + safe_filename = filename.replace(" ", "_") + hf_path = f"uploads/{slug}-{safe_filename}" + + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=hf_path, + repo_id=HF_REPO_ID, + repo_type="dataset", + token=HF_TOKEN + ) + return hf_path+ \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html @@ -0,0 +1,180 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>CDN | {{ page | capitalize }}</title> + <!-- Phase 3: Minimal, bare-but-fresh frontend (CSS included inline to enforce single-file HTML mandate) --> + <style> + :root { --accent: #2563eb; --bg: #f8fafc; --text: #0f172a; --border: #e2e8f0; } + body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2rem; } + .container { max-width: 800px; margin: 0 auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } + a { color: var(--accent); text-decoration: none; } + a:hover { text-decoration: underline; } + .nav { margin-bottom: 2rem; font-size: 1.1rem; } + .btn { display: inline-block; background: var(--accent); color: white; padding: 0.5rem 1rem; border-radius: 4px; border: none; cursor: pointer; text-decoration: none; font-size: 0.9rem; } + .btn:hover { background: #1d4ed8; text-decoration: none; } + .btn-secondary { background: #f1f5f9; color: var(--text); border: 1px solid var(--border); } + .btn-secondary:hover { background: #e2e8f0; } + + table { width: 100%; border-collapse: collapse; margin-top: 1rem; } + th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); } + th { font-weight: 500; color: #64748b; } + + .form-group { margin-bottom: 1rem; } + .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; } + .form-group input { width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; } + + #result { margin-top: 1rem; padding: 1rem; background: #f1f5f9; border-radius: 4px; display: none; word-break: break-all; } + </style> +</head> +<body> + <div class="container"> + <!-- Breadcrumb Navigation --> + <nav class="nav"> + <a href="/">~ Root</a> + {% if page in ['listing', 'file'] and path %} + {% set parts = path.split('/') %} + {% set current = namespace(val='') %} + {% for part in parts %} + {% set current.val = current.val + part + '/' %} + / <a href="/{{ current.val[:-1] }}">{{ part }}</a> + {% endfor %} + {% elif page == 'upload' %} + / <a href="/upload">upload</a> + {% elif page == 'ytmusic' %} + / <a href="/ytmusic">ytmusic</a> + {% endif %} + </nav> + + <!-- Directory Listing View --> + {% if page == 'listing' %} + <table> + <thead> + <tr> + <th>Name</th> + <th>Size</th> + <th>Action</th> + </tr> + </thead> + <tbody> + {% if path %} + <tr> + <td><a href="../">📁 ..</a></td> + <td>-</td> + <td></td> + </tr> + {% endif %} + {% for item in items %} + <tr> + <td> + {% if item.is_dir %} + 📁 <a href="/{{ item.path }}">{{ item.name }}</a> + {% else %} + 📄 <a href="/{{ item.path }}">{{ item.name }}</a> + {% endif %} + </td> + <td>{{ item.size_str }}</td> + <td> + {% if item.is_dir %} + <a href="/{{ item.path }}" class="btn btn-secondary">Open</a> + {% else %} + <a href="/api/download/{{ item.path }}" class="btn">Download</a> + {% endif %} + </td> + </tr> + {% endfor %} + {% if not items %} + <tr><td colspan="3" style="text-align: center; padding: 2rem;">Empty directory</td></tr> + {% endif %} + </tbody> + </table> + + <!-- Single File View --> + {% elif page == 'file' %} + <div style="text-align: center; padding: 3rem 0;"> + <h2>📄 {{ filename }}</h2> + <p style="color: #64748b; margin-bottom: 2rem;">{{ path }}</p> + <div style="display: flex; gap: 1rem; justify-content: center;"> + <a href="/api/download/{{ path }}" class="btn">Download File</a> + <button onclick="copyToClipboard('https://cdn.amit.is-a.dev/{{ path }}')" class="btn btn-secondary">Copy Link</button> + <button onclick="copyToClipboard('https://raw.cdn.amit.is-a.dev/{{ path }}')" class="btn btn-secondary">Copy Raw Link</button> + </div> + </div> + + <!-- Token-Protected Upload View --> + {% elif page == 'upload' %} + <h2>Upload File</h2> + <form id="uploadForm"> + <div class="form-group"> + <label>Admin Token</label> + <input type="password" id="token" required> + </div> + <div class="form-group"> + <label>File</label> + <input type="file" id="file" required> + </div> + <button type="submit" class="btn">Upload to CDN</button> + </form> + <div id="result"></div> + <script> + document.getElementById('uploadForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const btn = e.target.querySelector('button'); + btn.textContent = 'Uploading...'; + btn.disabled = true; + + const formData = new FormData(); + formData.append('file', document.getElementById('file').files[0]); + + try { + const res = await fetch('/api/upload', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + document.getElementById('token').value }, + body: formData + }); + + if (!res.ok) throw new Error('Upload failed: ' + res.statusText); + const data = await res.json(); + + const resultDiv = document.getElementById('result'); + resultDiv.style.display = 'block'; + resultDiv.innerHTML = ` + <strong>Success!</strong><br><br> + CDN Link: <a href="${data.cdn_url}" target="_blank">${data.cdn_url}</a><br> + Raw Link: <a href="${data.raw_url}" target="_blank">${data.raw_url}</a> + `; + } catch (err) { + alert(err.message); + } finally { + btn.textContent = 'Upload to CDN'; + btn.disabled = false; + } + }); + </script> + + <!-- YT Music Converter View --> + {% elif page == 'ytmusic' %} + <h2>YouTube Music Downloader</h2> + <p>Paste a YouTube Music link to download it directly as an MP3.</p> + <form method="POST" action="/api/ytmusic"> + <div class="form-group"> + <label>YouTube URL</label> + <input type="url" name="url" placeholder="https://music.youtube.com/watch?v=..." required> + </div> + <button type="submit" class="btn" onclick="this.textContent='Processing... Please wait';">Download MP3</button> + </form> + {% endif %} + </div> + + <script> + function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(() => { + alert('Copied to clipboard!'); + }).catch(err => { + console.error('Failed to copy: ', err); + }); + } + </script> +</body> +</html>+ \ No newline at end of file diff --git a/app/ytmusic.py b/app/ytmusic.py @@ -0,0 +1,47 @@ +import os +import shutil +import tempfile +import yt_dlp +from fastapi import BackgroundTasks +from fastapi.responses import FileResponse + +# Phase 5: YouTube Music processing +def process_and_stream_ytmusic(url: str, background_tasks: BackgroundTasks): + temp_dir = tempfile.mkdtemp() + + try: + ydl_opts = { + 'format': 'bestaudio', + 'extract_audio': True, + 'audio_format': 'mp3', + 'audio_quality': '0', + 'outtmpl': os.path.join(temp_dir, '%(artist)s - %(title)s.%(ext)s'), + 'noplaylist': True, + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=True) + filename = ydl.prepare_filename(info) + + # Predict the final mp3 filename based on the extracted extension + base, ext = os.path.splitext(filename) + mp3_filename = base + ".mp3" + + if not os.path.exists(mp3_filename): + mp3_filename = filename # Fallback if convert failed but original exists + + def cleanup(): + shutil.rmtree(temp_dir, ignore_errors=True) + + # Ensures Render disk space doesn't fill up + background_tasks.add_task(cleanup) + + return FileResponse( + mp3_filename, + media_type="audio/mpeg", + filename=os.path.basename(mp3_filename), + content_disposition_type="attachment" + ) + except Exception as e: + shutil.rmtree(temp_dir, ignore_errors=True) + raise e+ \ No newline at end of file diff --git a/render.yaml b/render.yaml @@ -0,0 +1,12 @@ +services: + - type: web + name: cdn-app + env: docker + plan: free + envVars: + - key: HF_REPO_ID + value: notamitgamer/cdn + - key: HF_TOKEN + sync: false + - key: ADMIN_TOKEN + sync: false+ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt @@ -0,0 +1,8 @@ +fastapi +uvicorn +huggingface_hub +yt-dlp +httpx +slowapi +python-multipart +jinja2+ \ No newline at end of file
© 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