import os import asyncio import collections import shutil from fastapi import FastAPI, WebSocket, Form, UploadFile, File, HTTPException from fastapi.responses import HTMLResponse, Response, FileResponse from fastapi.middleware.cors import CORSMiddleware import uvicorn app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) mc_process = None output_history = collections.deque(maxlen=300) connected_clients = set() BASE_DIR = os.path.abspath("/app") HTML_CONTENT = """ Server Engine
engine-live-stream
server.properties

Plugin Manager

system.status = "COMING_SOON"

""" # ----------------- # UTILITIES # ----------------- def get_safe_path(subpath: str): subpath = (subpath or "").strip("/") target = os.path.abspath(os.path.join(BASE_DIR, subpath)) if not target.startswith(BASE_DIR): raise HTTPException(status_code=403, detail="Access denied outside /app") return target async def broadcast(message: str): output_history.append(message) dead_clients = set() for client in connected_clients: try: await client.send_text(message) except: dead_clients.add(client) connected_clients.difference_update(dead_clients) # ----------------- # SERVER PROCESSES # ----------------- async def read_stream(stream, prefix=""): while True: try: line = await stream.readline() if not line: break line_str = line.decode('utf-8', errors='replace').rstrip('\r\n') await broadcast(prefix + line_str) except Exception: break async def start_minecraft(): global mc_process java_args = [ "java", "-server", "-Xmx8G", "-Xms8G", "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled", "-XX:ParallelGCThreads=2", "-XX:ConcGCThreads=1", "-XX:MaxGCPauseMillis=50", "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch", "-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=50", "-XX:G1HeapRegionSize=16M", "-XX:G1ReservePercent=15", "-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=3", "-XX:InitiatingHeapOccupancyPercent=10", "-XX:G1MixedGCLiveThresholdPercent=90", "-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32", "-XX:+PerfDisableSharedMem", "-XX:MaxTenuringThreshold=1", "-XX:G1SATBBufferEnqueueingThresholdPercent=30", "-XX:G1ConcMarkStepDurationMillis=5", "-XX:G1ConcRSHotCardLimit=16", "-XX:+UseStringDeduplication", "-Dfile.encoding=UTF-8", "-Dspring.output.ansi.enabled=ALWAYS", "-jar", "purpur.jar", "--nogui" ] mc_process = await asyncio.create_subprocess_exec( *java_args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=BASE_DIR ) asyncio.create_task(read_stream(mc_process.stdout)) @app.on_event("startup") async def startup_event(): os.makedirs(BASE_DIR, exist_ok=True) asyncio.create_task(start_minecraft()) # ----------------- # API ROUTING # ----------------- @app.get("/") def get_panel(): return HTMLResponse(content=HTML_CONTENT) @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() connected_clients.add(websocket) for line in output_history: await websocket.send_text(line) try: while True: cmd = await websocket.receive_text() if mc_process and mc_process.stdin: mc_process.stdin.write((cmd + "\n").encode('utf-8')) await mc_process.stdin.drain() except: connected_clients.remove(websocket) @app.get("/api/fs/list") def fs_list(path: str = ""): target = get_safe_path(path) if not os.path.exists(target): return [] items = [] for f in os.listdir(target): fp = os.path.join(target, f) items.append({"name": f, "is_dir": os.path.isdir(fp), "size": os.path.getsize(fp) if not os.path.isdir(fp) else 0}) return sorted(items, key=lambda x: (not x["is_dir"], x["name"].lower())) @app.get("/api/fs/read") def fs_read(path: str): target = get_safe_path(path) if not os.path.isfile(target): raise HTTPException(400, "Not a file") try: with open(target, 'r', encoding='utf-8') as f: return Response(content=f.read(), media_type="text/plain") except UnicodeDecodeError: raise HTTPException(400, "File is binary or unsupported encoding") @app.get("/api/fs/download") def fs_download(path: str): target = get_safe_path(path) if not os.path.isfile(target): raise HTTPException(400, "Not a file") return FileResponse(target, filename=os.path.basename(target)) @app.post("/api/fs/write") def fs_write(path: str = Form(...), content: str = Form(...)): target = get_safe_path(path) with open(target, 'w', encoding='utf-8') as f: f.write(content) return {"status": "ok"} @app.post("/api/fs/upload") async def fs_upload(path: str = Form(""), file: UploadFile = File(...)): target_dir = get_safe_path(path) os.makedirs(target_dir, exist_ok=True) target_file = os.path.join(target_dir, file.filename) with open(target_file, "wb") as buffer: shutil.copyfileobj(file.file, buffer) return {"status": "ok"} @app.post("/api/fs/delete") def fs_delete(path: str = Form(...)): target = get_safe_path(path) if os.path.isdir(target): shutil.rmtree(target) else: os.remove(target) return {"status": "ok"} @app.post("/api/fs/create_dir") def fs_create_dir(path: str = Form(...)): target = get_safe_path(path) try: os.makedirs(target, exist_ok=True) return {"status": "ok"} except Exception as e: raise HTTPException(400, str(e)) @app.post("/api/fs/create_file") def fs_create_file(path: str = Form(...)): target = get_safe_path(path) try: os.makedirs(os.path.dirname(target), exist_ok=True) open(target, 'a').close() return {"status": "ok"} except Exception as e: raise HTTPException(400, str(e)) @app.post("/api/fs/rename") def fs_rename(old_path: str = Form(...), new_name: str = Form(...)): src = get_safe_path(old_path) base_dir = os.path.dirname(src) dst = os.path.join(base_dir, new_name) try: os.rename(src, dst) return {"status": "ok"} except Exception as e: raise HTTPException(400, str(e)) @app.post("/api/fs/move") def fs_move(source: str = Form(...), dest: str = Form(...)): src = get_safe_path(source) dst = get_safe_path(dest) try: os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.move(src, dst) return {"status": "ok"} except Exception as e: raise HTTPException(400, str(e)) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")