diff --git "a/panel (1).py" "b/panel (1).py" new file mode 100644--- /dev/null +++ "b/panel (1).py" @@ -0,0 +1,1577 @@ +import os, asyncio, collections, shutil, urllib.request, json, time, re, threading +from pathlib import Path +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() +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, + allow_methods=["*"], allow_headers=["*"]) + +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") + +mc_process = None +output_history = collections.deque(maxlen=500) +connected_clients: set = set() +server_start_time: float | None = None + +# ─── HTML FRONTEND ──────────────────────────────────────────────────────────── +HTML_CONTENT = r""" + + +Orbit Panel + + + + +
+
+ +
+ +
+
+
+
+ + +
+ + + +""" + +# ─── PATH HELPER ───────────────────────────────────────────────────────────── +def safe_path(p: str) -> str: + clean = (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 + +# ─── MC PROCESS MANAGEMENT ─────────────────────────────────────────────────── +async def stream_output(pipe): + while True: + line = await pipe.readline() + if not line: + break + txt = line.decode("utf-8", errors="replace").rstrip() + output_history.append(txt) + dead = set() + for c in connected_clients: + try: + await c.send_text(txt) + except: + dead.add(c) + connected_clients.difference_update(dead) + +async def boot_mc(): + global mc_process, server_start_time + # Prefer purpur.jar → paper.jar → server.jar + jar = None + for candidate in ("purpur.jar", "paper.jar", "server.jar"): + p = os.path.join(BASE_DIR, candidate) + if os.path.exists(p): + jar = p + break + if not jar: + output_history.append("\x1b[33m[System] No server jar found in /app. Upload one via Files or install via Software tab.\x1b[0m") + return + server_start_time = time.time() + mc_process = await asyncio.create_subprocess_exec( + "java", "-Xmx4G", "-Xms1G", "-Dfile.encoding=UTF-8", + "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled", + "-jar", jar, "--nogui", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=BASE_DIR + ) + asyncio.create_task(stream_output(mc_process.stdout)) + await mc_process.wait() + server_start_time = None + output_history.append("[System] Server process exited.") + +@app.on_event("startup") +async def on_start(): + os.makedirs(PLUGINS_DIR, exist_ok=True) + asyncio.create_task(boot_mc()) + +# ─── ROUTES ─────────────────────────────────────────────────────────────────── +@app.get("/") +def index(): + return HTMLResponse(HTML_CONTENT) + +@app.websocket("/ws") +async def ws_endpoint(ws: WebSocket): + await ws.accept() + connected_clients.add(ws) + # Replay history + for line in output_history: + try: + await ws.send_text(line) + except: + break + try: + while True: + cmd = await ws.receive_text() + if mc_process and mc_process.stdin and not mc_process.stdin.is_closing(): + mc_process.stdin.write((cmd + "\n").encode()) + await mc_process.stdin.drain() + except (WebSocketDisconnect, Exception): + connected_clients.discard(ws) + +@app.get("/api/console/history") +def console_history(): + return list(output_history) + +# ─── SERVER STATUS ──────────────────────────────────────────────────────────── +@app.get("/api/server/status") +def server_status(): + running = mc_process is not None and mc_process.returncode is None + + # Uptime + uptime_str = "—" + if server_start_time and running: + secs = int(time.time() - server_start_time) + h, r = divmod(secs, 3600) + m, s = divmod(r, 60) + uptime_str = f"{h}h {m}m {s}s" if h else f"{m}m {s}s" + + # Memory (from /proc/meminfo if available) + ram_total, ram_used, ram_pct = 0, 0, 0 + try: + with open("/proc/meminfo") as f: + mem = {} + for line in f: + k, v = line.split(":", 1) + mem[k.strip()] = int(v.strip().split()[0]) + ram_total = mem.get("MemTotal", 0) + ram_free = mem.get("MemAvailable", mem.get("MemFree", 0)) + ram_used = ram_total - ram_free + ram_pct = round(ram_used / ram_total * 100) if ram_total else 0 + except: + pass + + # CPU (simple /proc/stat delta approximation) + cpu_pct = 0 + try: + with open("/proc/stat") as f: + vals = list(map(int, f.readline().split()[1:])) + idle, total = vals[3], sum(vals) + cpu_pct = max(0, round(100 - idle / total * 100)) if total else 0 + except: + pass + + # Disk + disk_total, disk_used, disk_pct = 0, 0, 0 + try: + st = os.statvfs(BASE_DIR) + disk_total = st.f_blocks * st.f_frsize + disk_free = st.f_bfree * st.f_frsize + disk_used = disk_total - disk_free + disk_pct = round(disk_used / disk_total * 100) if disk_total else 0 + except: + pass + + def mb(b): return f"{b//1048576} MB" if b else "—" + def gb(b): return f"{b/1073741824:.1f} GB" if b else "—" + + # Active jar + active_jar = None + for c in ("purpur.jar", "paper.jar", "server.jar"): + if os.path.exists(os.path.join(BASE_DIR, c)): + active_jar = c; break + + return { + "running": running, + "uptime": uptime_str, + "cpu_pct": cpu_pct, + "cpu_sub": f"{cpu_pct}% utilization", + "ram_pct": ram_pct, + "ram_sub": f"{mb(ram_used)} / {mb(ram_total)}", + "disk_pct": disk_pct, + "disk_sub": f"{gb(disk_used)} / {gb(disk_total)}", + "tps": "—", + "players": "—", + "mc_version": "1.20.4", + "active_jar": active_jar, + "address": "" + } + +@app.post("/api/server/{action}") +async def server_control(action: str): + global mc_process + if action == "stop": + if mc_process and mc_process.returncode is None: + try: + mc_process.stdin.write(b"stop\n") + await mc_process.stdin.drain() + except: + mc_process.terminate() + elif action == "start": + if mc_process is None or mc_process.returncode is not None: + asyncio.create_task(boot_mc()) + elif action == "restart": + if mc_process and mc_process.returncode is None: + try: + mc_process.stdin.write(b"stop\n") + await mc_process.stdin.drain() + await asyncio.sleep(3) + except: + pass + asyncio.create_task(boot_mc()) + return {"ok": True} + +# ─── FILE SYSTEM API ────────────────────────────────────────────────────────── +@app.get("/api/fs/list") +def fs_list(path: str = ""): + target = safe_path(path) + if not os.path.isdir(target): + raise HTTPException(404, "Not a directory") + items = [] + for name in os.listdir(target): + fp = os.path.join(target, name) + st = os.stat(fp) + items.append({ + "name": name, + "is_dir": os.path.isdir(fp), + "size": st.st_size if not os.path.isdir(fp) else -1, + "mtime": int(st.st_mtime) + }) + return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower())) + +@app.get("/api/fs/read") +def fs_read(path: str): + target = safe_path(path) + if not os.path.isfile(target): + raise HTTPException(404, "File not found") + try: + with open(target, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + if path.endswith(".json"): + try: + return json.loads(content) + except: + pass + return Response(content, media_type="text/plain; charset=utf-8") + except: + raise HTTPException(500, "Cannot read file") + +@app.get("/api/fs/download") +def fs_download(path: str): + target = safe_path(path) + if not os.path.isfile(target): + raise HTTPException(404, "File not found") + from fastapi.responses import FileResponse + return FileResponse(target, filename=os.path.basename(target)) + +@app.post("/api/fs/write") +async def fs_write(path: str = Form(...), content: str = Form(...)): + target = safe_path(path) + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, "w", encoding="utf-8") as f: + f.write(content) + return {"ok": True} + +@app.post("/api/fs/upload") +async def fs_upload(path: str = Form(""), file: UploadFile = File(...)): + target_dir = safe_path(path) + os.makedirs(target_dir, exist_ok=True) + dest = os.path.join(target_dir, file.filename) + with open(dest, "wb") as f: + shutil.copyfileobj(file.file, f) + return {"ok": True} + +@app.post("/api/fs/delete") +def fs_delete(path: str = Form(...)): + target = safe_path(path) + if not os.path.exists(target): + raise HTTPException(404, "Not found") + if os.path.isdir(target): + shutil.rmtree(target) + else: + os.remove(target) + return {"ok": True} + +@app.post("/api/fs/rename") +def fs_rename(old_path: str = Form(...), new_path: str = Form(...)): + src = safe_path(old_path) + dst = safe_path(new_path) + if not os.path.exists(src): + raise HTTPException(404, "Source not found") + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.move(src, dst) + return {"ok": True} + +@app.post("/api/fs/create") +def fs_create(path: str = Form(...), is_dir: str = Form("0")): + target = safe_path(path) + if is_dir == "1": + os.makedirs(target, exist_ok=True) + else: + os.makedirs(os.path.dirname(target), exist_ok=True) + if not os.path.exists(target): + open(target, "w").close() + return {"ok": True} + +# ─── PLUGIN INSTALLER ───────────────────────────────────────────────────────── +@app.post("/api/plugins/install") +def plugins_install( + url: str = Form(...), + filename: str = Form(...), + project_id: str = Form(...), + version_id: str = Form(...), + name: str = Form(...) +): + dest = os.path.join(PLUGINS_DIR, filename) + try: + req = urllib.request.Request(url, headers={"User-Agent": "OrbitPanel/2.0"}) + with urllib.request.urlopen(req, timeout=120) as resp, open(dest, "wb") as f: + shutil.copyfileobj(resp, f) + except Exception as e: + raise HTTPException(500, f"Download failed: {e}") + + record_path = os.path.join(PLUGINS_DIR, "plugins.json") + data = {} + if os.path.exists(record_path): + try: + with open(record_path) as f: + data = json.load(f) + except: + pass + data[project_id] = { + "name": name, + "filename": filename, + "version_id": version_id, + "installed_at": time.time() + } + with open(record_path, "w") as f: + json.dump(data, f, indent=2) + return {"ok": True} + +# ─── SOFTWARE INSTALLER ─────────────────────────────────────────────────────── +@app.post("/api/software/install") +async def software_install(type: str = Form(...), version: str = Form(...)): + """Download and install a server jar from official sources.""" + dest = os.path.join(BASE_DIR, "server.jar") + # Rename existing jar as backup + for candidate in ("purpur.jar", "paper.jar", "server.jar"): + p = os.path.join(BASE_DIR, candidate) + if os.path.exists(p): + shutil.copy2(p, p + ".bak") + + try: + dl_url = None + + if type == "paper": + # Get latest build for version + builds_url = f"https://api.papermc.io/v2/projects/paper/versions/{version}/builds" + with urllib.request.urlopen(builds_url, timeout=15) as r: + builds_data = json.loads(r.read()) + latest_build = builds_data["builds"][-1]["build"] + jar_name = f"paper-{version}-{latest_build}.jar" + dl_url = f"https://api.papermc.io/v2/projects/paper/versions/{version}/builds/{latest_build}/downloads/{jar_name}" + + elif type == "purpur": + dl_url = f"https://api.purpurmc.org/v2/purpur/{version}/latest/download" + + elif type == "vanilla": + with urllib.request.urlopen("https://launchermeta.mojang.com/mc/game/version_manifest.json", timeout=15) as r: + manifest = json.loads(r.read()) + ver_info = next((v for v in manifest["versions"] if v["id"] == version), None) + if not ver_info: + raise HTTPException(404, f"Version {version} not found in manifest") + with urllib.request.urlopen(ver_info["url"], timeout=15) as r: + ver_data = json.loads(r.read()) + dl_url = ver_data["downloads"]["server"]["url"] + + elif type == "fabric": + # Get latest loader + installer + with urllib.request.urlopen("https://meta.fabricmc.net/v2/versions/loader", timeout=10) as r: + loaders = json.loads(r.read()) + with urllib.request.urlopen("https://meta.fabricmc.net/v2/versions/installer", timeout=10) as r: + installers = json.loads(r.read()) + loader_ver = loaders[0]["version"] + installer_ver = installers[0]["version"] + dl_url = f"https://meta.fabricmc.net/v2/versions/loader/{version}/{loader_ver}/{installer_ver}/server/jar" + + else: + raise HTTPException(400, f"Unsupported type: {type}") + + def do_download(): + req = urllib.request.Request(dl_url, headers={"User-Agent": "OrbitPanel/2.0"}) + with urllib.request.urlopen(req, timeout=300) as resp, open(dest, "wb") as f: + shutil.copyfileobj(resp, f) + + # Run blocking download in thread + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, do_download) + output_history.append(f"[System] Installed {type} {version} → server.jar") + return {"ok": True} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(500, str(e)) + +# ─── SETTINGS API ───────────────────────────────────────────────────────────── +def _parse_properties(path: str) -> dict: + props = {} + if not os.path.isfile(path): + return props + with open(path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + if line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + props[k.strip()] = v.strip() + return props + +def _write_properties(path: str, props: dict): + lines = [f"# Managed by Orbit Panel\n"] + for k, v in sorted(props.items()): + lines.append(f"{k}={v}\n") + with open(path, "w", encoding="utf-8") as f: + f.writelines(lines) + +@app.get("/api/settings/properties") +def get_properties(): + path = os.path.join(BASE_DIR, "server.properties") + return _parse_properties(path) + +@app.post("/api/settings/properties") +async def save_properties(data: str = Form(...)): + path = os.path.join(BASE_DIR, "server.properties") + try: + props = json.loads(data) + _write_properties(path, props) + return {"ok": True} + except Exception as e: + raise HTTPException(500, str(e)) + +@app.get("/api/settings/panel") +def get_panel_config(): + if os.path.isfile(PANEL_CFG): + try: + with open(PANEL_CFG) as f: + return json.load(f) + except: + pass + return {"accentColor": "#00ff88", "bgColor": "#0a0a0a", "textSize": "14", "serverAddress": ""} + +@app.post("/api/settings/panel") +async def save_panel_config(data: str = Form(...)): + try: + cfg = json.loads(data) + with open(PANEL_CFG, "w") as f: + json.dump(cfg, f, indent=2) + return {"ok": True} + except Exception as e: + raise HTTPException(500, str(e)) + +# ─── ENTRY POINT ───────────────────────────────────────────────────────────── +if __name__ == "__main__": + uvicorn.run( + app, + host="0.0.0.0", + port=int(os.environ.get("PORT", 7860)), + log_level="warning" + )