Spaces:
Paused
Paused
| import os | |
| import sys | |
| import io | |
| import socket | |
| import subprocess | |
| import threading | |
| import mimetypes | |
| import json | |
| from pathlib import Path | |
| from datetime import datetime | |
| from typing import Optional | |
| sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1) | |
| import qrcode | |
| import uvicorn | |
| from fastapi import FastAPI, UploadFile, File, HTTPException, WebSocket, WebSocketDisconnect | |
| from fastapi.responses import FileResponse, JSONResponse, Response, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from dotenv import load_dotenv | |
| from pyngrok import ngrok, conf | |
| load_dotenv() | |
| # βββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PORT = int(os.environ.get("PORT", 8000)) | |
| BASE_DIR = Path(__file__).resolve().parent | |
| FRONTEND_DIR = BASE_DIR.parent / "frontend" | |
| UPLOAD_DIR = BASE_DIR / "uploads" | |
| URL_FILE = BASE_DIR / "public_url.txt" | |
| UPLOAD_DIR.mkdir(exist_ok=True) | |
| # βββ Rooms State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # { "room_code": { "clients": [WebSocket], "messages": [dict] } } | |
| rooms = {} | |
| # βββ Kill any process already on PORT ββββββββββββββββββββββββββββββββββββββββ | |
| def free_port(port: int): | |
| try: | |
| result = subprocess.run(["netstat", "-ano"], capture_output=True, text=True) | |
| for line in result.stdout.splitlines(): | |
| if f":{port}" in line and "LISTENING" in line: | |
| pid = line.strip().split()[-1] | |
| subprocess.run(["taskkill", "/F", "/PID", pid], capture_output=True) | |
| print(f" [~] Freed port {port} (killed PID {pid})") | |
| except Exception: | |
| pass | |
| # βββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI(title="RadarDrop v2") | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], | |
| allow_methods=["*"], allow_headers=["*"]) | |
| # βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_local_ip() -> str: | |
| try: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
| s.connect(("8.8.8.8", 80)) | |
| ip = s.getsockname()[0] | |
| s.close() | |
| return ip | |
| except Exception: | |
| return "127.0.0.1" | |
| def human_size(n: int) -> str: | |
| for unit in ("B", "KB", "MB", "GB"): | |
| if n < 1024: | |
| return f"{n:.1f} {unit}" | |
| n /= 1024 | |
| return f"{n:.1f} TB" | |
| def make_qr_png(url: str) -> bytes: | |
| img = qrcode.make(url) | |
| buf = io.BytesIO() | |
| img.save(buf, format="PNG") | |
| buf.seek(0) | |
| return buf.read() | |
| def get_public_url() -> Optional[str]: | |
| try: | |
| txt = URL_FILE.read_text().strip() | |
| return txt if txt else None | |
| except Exception: | |
| return None | |
| # βββ Ngrok Tunnel ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def start_ngrok_tunnel(port: int): | |
| URL_FILE.write_text("", encoding="utf-8") | |
| auth_token = os.environ.get("NGROK_AUTH_TOKEN") | |
| if not auth_token: | |
| print("\n [!] NGROK_AUTH_TOKEN is missing in .env file.") | |
| print(" [!] Running strictly on Local Network.") | |
| URL_FILE.write_text("FAILED", encoding="utf-8") | |
| return | |
| try: | |
| ngrok.set_auth_token(auth_token) | |
| # Suppress verbose ngrok logs | |
| conf.get_default().log_event_callback = lambda log: None | |
| domain = os.environ.get("NGROK_DOMAIN") | |
| if domain: | |
| tunnel = ngrok.connect(port, domain=domain) | |
| else: | |
| tunnel = ngrok.connect(port) | |
| public_url = tunnel.public_url | |
| URL_FILE.write_text(public_url, encoding="utf-8") | |
| print(f"\n [PUBLIC] Tunnel live: {public_url}") | |
| print(f" [PUBLIC] Share with anyone!\n") | |
| except Exception as e: | |
| print(f"\n [WARN] Ngrok failed: {e}") | |
| URL_FILE.write_text("FAILED", encoding="utf-8") | |
| async def broadcast_to_room(room_code: str, message: dict): | |
| if room_code in rooms: | |
| for client in rooms[room_code]["clients"]: | |
| try: | |
| await client.send_json(message) | |
| except Exception: | |
| pass | |
| # βββ WebSockets (Chat & Live Sync) βββββββββββββββββββββββββββββββββββββββββββ | |
| async def websocket_endpoint(websocket: WebSocket, room_code: str): | |
| await websocket.accept() | |
| if room_code not in rooms: | |
| rooms[room_code] = {"clients": [], "messages": []} | |
| (UPLOAD_DIR / room_code).mkdir(exist_ok=True) | |
| rooms[room_code]["clients"].append(websocket) | |
| # Send chat history to new connection | |
| for msg in rooms[room_code]["messages"]: | |
| await websocket.send_json(msg) | |
| try: | |
| while True: | |
| data = await websocket.receive_text() | |
| payload = json.loads(data) | |
| if payload.get("type") == "chat": | |
| msg = { | |
| "type": "chat", | |
| "sender": payload.get("sender", "Anonymous"), | |
| "text": payload.get("text"), | |
| "timestamp": datetime.now().strftime("%H:%M") | |
| } | |
| rooms[room_code]["messages"].append(msg) | |
| await broadcast_to_room(room_code, msg) | |
| except WebSocketDisconnect: | |
| rooms[room_code]["clients"].remove(websocket) | |
| except Exception as e: | |
| if websocket in rooms[room_code]["clients"]: | |
| rooms[room_code]["clients"].remove(websocket) | |
| # βββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def serve_index(): | |
| return HTMLResponse((FRONTEND_DIR / "index.html").read_text(encoding="utf-8")) | |
| async def get_ip(): | |
| ip = get_local_ip() | |
| public_url = get_public_url() | |
| failed = public_url == "FAILED" | |
| return { | |
| "local_url": f"http://{ip}:{PORT}", | |
| "public_url": None if (not public_url or failed) else public_url, | |
| "tunnel_status": "failed" if failed else ("live" if public_url else "connecting"), | |
| } | |
| async def local_qr(room: str = ""): | |
| url = f"http://{get_local_ip()}:{PORT}" | |
| if room: url += f"?room={room}" | |
| return Response(content=make_qr_png(url), media_type="image/png") | |
| async def public_qr(room: str = ""): | |
| url = get_public_url() | |
| if not url or url == "FAILED": | |
| raise HTTPException(503, "Tunnel not ready yet") | |
| if room: url += f"?room={room}" | |
| return Response(content=make_qr_png(url), media_type="image/png") | |
| # βββ Room File Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def list_files(room_code: str): | |
| room_dir = UPLOAD_DIR / room_code | |
| room_dir.mkdir(exist_ok=True) | |
| files = [] | |
| for f in sorted(room_dir.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True): | |
| if f.is_file(): | |
| stat = f.stat() | |
| mime, _ = mimetypes.guess_type(f.name) | |
| files.append({ | |
| "name": f.name, | |
| "size": stat.st_size, | |
| "size_hr": human_size(stat.st_size), | |
| "mime": mime or "application/octet-stream", | |
| "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"), | |
| }) | |
| return JSONResponse(files) | |
| async def upload(room_code: str, file: UploadFile = File(...)): | |
| room_dir = UPLOAD_DIR / room_code | |
| room_dir.mkdir(exist_ok=True) | |
| dest = room_dir / file.filename | |
| stem = Path(file.filename).stem | |
| suffix = Path(file.filename).suffix | |
| n = 1 | |
| while dest.exists(): | |
| dest = room_dir / f"{stem} ({n}){suffix}"; n += 1 | |
| dest.write_bytes(await file.read()) | |
| # Broadcast to room that files updated | |
| await broadcast_to_room(room_code, {"type": "files_updated"}) | |
| return {"filename": dest.name} | |
| async def download(room_code: str, filename: str): | |
| p = UPLOAD_DIR / room_code / filename | |
| if not p.exists(): | |
| raise HTTPException(404, "Not found") | |
| return FileResponse(p, filename=filename) | |
| async def delete(room_code: str, filename: str): | |
| p = UPLOAD_DIR / room_code / filename | |
| if not p.exists(): | |
| raise HTTPException(404, "Not found") | |
| p.unlink() | |
| await broadcast_to_room(room_code, {"type": "files_updated"}) | |
| return {"ok": True} | |
| # βββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| ip = get_local_ip() | |
| # If we are NOT running on Hugging Face Spaces, free port and prompt for Ngrok | |
| is_huggingface = os.environ.get("SPACE_ID") is not None | |
| if not is_huggingface: | |
| free_port(PORT) | |
| if not os.environ.get("NGROK_AUTH_TOKEN"): | |
| print("\n" + "="*55) | |
| print(" [!] FIXED URL SETUP REQUIRED") | |
| print(" To get a fixed URL, you need a free Ngrok account.") | |
| print(" 1. Go to: https://dashboard.ngrok.com/get-started/your-authtoken") | |
| print(" 2. Copy your Authtoken.") | |
| print(" 3. (Optional) Claim your free static domain at: https://dashboard.ngrok.com/cloud-edge/domains") | |
| print(" (Or leave blank and press Enter to use Local Network only)") | |
| print("="*55) | |
| token = input(" Enter Ngrok Authtoken: ").strip() | |
| if token: | |
| domain = input(" (Optional) Enter your Ngrok Static Domain (e.g., xxx.ngrok-free.app) or leave blank: ").strip() | |
| with open(BASE_DIR / ".env", "w") as f: | |
| f.write(f"NGROK_AUTH_TOKEN={token}\n") | |
| if domain: | |
| f.write(f"NGROK_DOMAIN={domain}\n") | |
| os.environ["NGROK_AUTH_TOKEN"] = token | |
| if domain: | |
| os.environ["NGROK_DOMAIN"] = domain | |
| print(" [+] Saved to .env!") | |
| else: | |
| print(" [-] No token provided. Running in Local Mode only.") | |
| threading.Thread(target=start_ngrok_tunnel, args=(PORT,), daemon=True).start() | |
| print("\n" + "=" * 55) | |
| print(" RadarDrop v2 is LIVE") | |
| print(f" Local : http://localhost:{PORT}") | |
| if is_huggingface: | |
| print(" Running in Hugging Face Space") | |
| else: | |
| print(f" Network: http://{ip}:{PORT}") | |
| print("=" * 55 + "\n") | |
| uvicorn.run(app, host="0.0.0.0", port=PORT) | |