diff --git "a/panel.py" "b/panel.py" --- "a/panel.py" +++ "b/panel.py" @@ -1,2123 +1,1013 @@ -#!/usr/bin/env python3 -""" -OSP Panel — Minecraft Server Management Panel -Single-file deployment for HuggingFace Docker Spaces. - -Required HF Secrets: - HF_USERNAME — Panel login username - HF_PASSWORD — Panel login password - SERVER_ZIP_URL — Google Drive share link to server zip (optional) - -Usage: - pip install fastapi uvicorn python-multipart - python app.py -""" - -import os, sys, asyncio, collections, shutil, urllib.request, json, time, re, secrets, hashlib -import tarfile, zipfile, threading +import os, asyncio, collections, shutil, urllib.request, json, time, re, threading from pathlib import Path -from datetime import datetime -from typing import Optional - -from fastapi import ( - FastAPI, WebSocket, WebSocketDisconnect, Form, UploadFile, File, - HTTPException, Request, Response, Depends, Cookie -) -from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse +from fastapi import FastAPI, WebSocket, Form, UploadFile, File, HTTPException, WebSocketDisconnect +from fastapi.responses import HTMLResponse, Response, JSONResponse from fastapi.middleware.cors import CORSMiddleware import uvicorn -# ═══════════════════════════════════════════════════════════════════════════════ -# CONFIG -# ═══════════════════════════════════════════════════════════════════════════════ -app = FastAPI(title="OSP Panel", docs_url=None, redoc_url=None) -app.add_middleware( - CORSMiddleware, allow_origins=["*"], allow_credentials=True, - allow_methods=["*"], allow_headers=["*"] -) - -BASE_DIR = os.environ.get("SERVER_DIR", "/app") -PLUGINS_DIR = os.path.join(BASE_DIR, "plugins") -BACKUPS_DIR = os.path.join(BASE_DIR, "backups") -PANEL_CFG = os.path.join(BASE_DIR, "panel.json") -EULA_PATH = os.path.join(BASE_DIR, "eula.txt") -STORAGE_LIMIT = 20 * 1024 * 1024 * 1024 # 20 GB software limit - -HF_USERNAME = os.environ.get("HF_USERNAME", "admin") -HF_PASSWORD = os.environ.get("HF_PASSWORD", "admin") -SERVER_ZIP_URL = os.environ.get("SERVER_ZIP_URL", "") - -mc_process: Optional[asyncio.subprocess.Process] = None -output_history = collections.deque(maxlen=1000) -connected_clients: set = set() -server_start_time: Optional[float] = None -active_sessions: dict = {} # token -> expiry - -schedule_tasks: dict = {} # schedule_id -> asyncio.Task - -# ═══════════════════════════════════════════════════════════════════════════════ -# PANEL.JSON PERSISTENCE -# ═══════════════════════════════════════════════════════════════════════════════ -DEFAULT_PANEL = { - "theme": "dark", - "accent": "blue", - "fontSize": "default", - "reducedMotion": False, - "compactMode": False, - "serverAddress": "", - "serverPort": "25565", - "schedules": [], - "backups": { - "gdrive_enabled": False, - "gdrive_client_id": "", - "gdrive_client_secret": "", - "gdrive_refresh_token": "", - "gdrive_folder_id": "" - } -} +# ─── CONFIG ────────────────────────────────────────────────────────────────── +app = FastAPI() +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, + allow_methods=["*"], allow_headers=["*"]) -def load_panel() -> dict: - if os.path.isfile(PANEL_CFG): - try: - with open(PANEL_CFG) as f: - data = json.load(f) - merged = {**DEFAULT_PANEL, **data} - if "backups" not in merged or not isinstance(merged["backups"], dict): - merged["backups"] = DEFAULT_PANEL["backups"] - return merged - except: - pass - return dict(DEFAULT_PANEL) - -def save_panel(cfg: dict): - with open(PANEL_CFG, "w") as f: - json.dump(cfg, f, indent=2) - -# ═══════════════════════════════════════════════════════════════════════════════ -# AUTH -# ═══════════════════════════════════════════════════════════════════════════════ -def create_session(remember: bool = False) -> tuple: - token = secrets.token_hex(32) - expiry = time.time() + (30 * 86400 if remember else 86400) - active_sessions[token] = expiry - return token, expiry - -def verify_session(token: str) -> bool: - if not token or token not in active_sessions: - return False - if time.time() > active_sessions[token]: - del active_sessions[token] - return False - return True - -async def require_auth(request: Request): - token = request.cookies.get("osp_session") - if not verify_session(token): - raise HTTPException(401, "Unauthorized") - -# ═══════════════════════════════════════════════════════════════════════════════ -# PATH SAFETY -# ═══════════════════════════════════════════════════════════════════════════════ -def safe_path(p: str) -> str: - clean = os.path.normpath((p or "").strip("/")).replace("..", "") - full = os.path.abspath(os.path.join(BASE_DIR, clean)) - if not full.startswith(os.path.abspath(BASE_DIR)): - raise HTTPException(403, "Access denied") - return full +BASE_DIR = os.environ.get("SERVER_DIR", os.path.abspath("/app")) +PLUGINS_DIR = os.path.join(BASE_DIR, "plugins") +PANEL_CFG = os.path.join(BASE_DIR, ".panel_config.json") -# ═══════════════════════════════════════════════════════════════════════════════ -# STORAGE USAGE -# ═══════════════════════════════════════════════════════════════════════════════ -def get_dir_size(path: str) -> int: - total = 0 - for dirpath, dirnames, filenames in os.walk(path): - for f in filenames: - fp = os.path.join(dirpath, f) - try: - total += os.path.getsize(fp) - except: - pass - return total - -# ═══════════════════════════════════════════════════════════════════════════════ -# GOOGLE DRIVE HELPERS -# ═══════════════════════════════════════════════════════════════════════════════ -def gdrive_download(share_url: str, dest_path: str): - """Download file from Google Drive share link.""" - file_id = None - patterns = [ - r'/file/d/([a-zA-Z0-9_-]+)', - r'id=([a-zA-Z0-9_-]+)', - r'/d/([a-zA-Z0-9_-]+)', - ] - for pat in patterns: - m = re.search(pat, share_url) - if m: - file_id = m.group(1) - break - if not file_id: - raise Exception(f"Cannot extract file ID from: {share_url}") +mc_process = None +output_history = collections.deque(maxlen=500) +connected_clients: set = set() +server_start_time: float | None = None - url = f"https://drive.google.com/uc?export=download&id={file_id}&confirm=t" - req = urllib.request.Request(url, headers={"User-Agent": "OSPPanel/1.0"}) - with urllib.request.urlopen(req, timeout=600) as resp: - with open(dest_path, "wb") as f: - shutil.copyfileobj(resp, f) +# ─── HTML FRONTEND ──────────────────────────────────────────────────────────── +HTML_CONTENT = r"""
+ + +