commit 44d3b81a84a2b9406ccec7f71045a780b31e0cae
Author: Amit Dutta <mail@amit.is-a.dev>
Date: Thu, 3 Sep 2026 15:11:57 +0530
Add files via upload
Diffstat:
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