#!/usr/bin/env python3 """ Lightweight Mirror Server - only serves static files """ import os import asyncio from pathlib import Path import structlog import httpx MAIN_SERVER_URL = os.environ.get("MAIN_SERVER_URL", "http://87.120.187.36:1582") MASTER_KEY = os.environ.get("MASTER_KEY", "sashegdevsupeddevepta") PORT = int(os.environ.get("PORT", "1582")) BUILDS_DIR = Path("builds") VERSIONS_DIR = BUILDS_DIR / "versions" PACKS_DIR = Path("packs") BUILDS_DIR.mkdir(exist_ok=True) PACKS_DIR.mkdir(exist_ok=True) logging = structlog.get_logger() async def sync_with_main(): """Sync files from main server""" logging.info(f"Syncing from {MAIN_SERVER_URL}") client = httpx.AsyncClient(timeout=120.0) headers = {"X-Master-Key": MASTER_KEY} try: # Get launcher info resp = await client.get(f"{MAIN_SERVER_URL}/launcher/info", headers=headers) if resp.status_code != 200: logging.error(f"Failed to get launcher info: {resp.status_code}") return data = resp.json() current_version = data.get("current_version", "1.0.9") files = data.get("files", {}) zips = files.get("zips", []) logging.info(f"Current version: {current_version}, zips: {len(zips)}") # Download latest ZIP for z in zips: if not z.get("is_legacy"): zip_filename = z.get("filename") zip_path = BUILDS_DIR / zip_filename if not zip_path.exists(): logging.info(f"Downloading {zip_filename}...") # Try direct download download_url = f"{MAIN_SERVER_URL}/launcher/download/zip/{zip_filename}" resp = await client.get(download_url, headers=headers) if resp.status_code == 200: zip_path.write_bytes(resp.content) logging.info(f"Downloaded {zip_filename}") # Extract version = z.get("version") extract_to = VERSIONS_DIR / version extract_to.mkdir(parents=True, exist_ok=True) import zipfile with zipfile.ZipFile(zip_path, 'r') as zf: zf.extractall(extract_to) logging.info(f"Extracted {version}") # Get launcher meta resp = await client.get(f"{MAIN_SERVER_URL}/launcher/meta/{current_version}", headers=headers) if resp.status_code == 200: (BUILDS_DIR / "meta.json").write_text(resp.text) logging.info("Meta synced") # Sync packs list resp = await client.get(f"{MAIN_SERVER_URL}/packs", headers=headers) if resp.status_code == 200: packs_data = resp.json() packs = packs_data.get("packs", []) logging.info(f"Found {len(packs)} packs") for pack in packs: pack_name = pack.get("name") pack_meta_url = f"{MAIN_SERVER_URL}/pack/meta/{pack_name}" resp = await client.get(pack_meta_url, headers=headers) if resp.status_code == 200: pack_dir = PACKS_DIR / pack_name pack_dir.mkdir(parents=True, exist_ok=True) (pack_dir / "meta.json").write_text(resp.text) logging.info(f"Synced pack: {pack_name}") finally: await client.aclose() logging.info("Sync complete") async def run_server(): """Run static server""" from fastapi import FastAPI, HTTPException, Request from fastapi.responses import StreamingResponse import aiofiles import mimetypes import re import uvicorn app = FastAPI(title="ZernMC Mirror") async def send_file(file_path: Path, request: Request): if not file_path.exists(): raise HTTPException(404, "Not found") content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream" file_size = file_path.stat().st_size range_header = request.headers.get("range") if range_header: match = re.match(r"bytes=(\d+)-(\d+)?", range_header) if match: start = int(match.group(1)) end = min(file_size - 1, int(match.group(2)) if match.group(2) else file_size - 1) content_length = end - start + 1 async with aiofiles.open(file_path, "rb") as f: await f.seek(start) chunk = await f.read(content_length) return StreamingResponse(iter([chunk]), status_code=206, media_type=content_type, headers={"Content-Range": f"bytes {start}-{end}/{file_size}", "Accept-Ranges": "bytes", "Content-Length": str(content_length)}) async def file_iter(): async with aiofiles.open(file_path, "rb") as f: while True: chunk = await f.read(65536) if not chunk: break yield chunk return StreamingResponse(file_iter(), media_type=content_type, headers={"Accept-Ranges": "bytes", "Content-Length": str(file_size)}) @app.get("/launcher/info") async def get_launcher_info(): meta_path = BUILDS_DIR / "meta.json" if meta_path.exists(): import json return json.loads(meta_path.read_text()) return {"current_version": "unknown", "files": {}} @app.get("/launcher/version") async def get_version(): return await get_launcher_info() @app.get("/launcher/file/{version}/{file_path:path}") async def get_launcher_file(version: str, file_path: str, request: Request): full_path = BUILDS_DIR / "versions" / version / file_path if ".." in file_path: raise HTTPException(403, "Invalid path") if not full_path.exists(): raise HTTPException(404, f"File not found: {file_path}") return await send_file(full_path, request) @app.get("/launcher/download/zip/{filename}") async def download_zip(filename: str, request: Request): return await send_file(BUILDS_DIR / filename, request) @app.get("/launcher/meta/{version}") async def get_meta(version: str): meta_path = BUILDS_DIR / "meta.json" if meta_path.exists(): import json return json.loads(meta_path.read_text()) raise HTTPException(404, "Meta not found") @app.get("/launcher/mirrors") async def get_mirrors(): return {"mirrors": [{"name": "main", "url": MAIN_SERVER_URL}]} @app.get("/packs") async def list_packs(): import json packs = [] for pack_dir in PACKS_DIR.iterdir(): if pack_dir.is_dir(): meta_path = pack_dir / "meta.json" if meta_path.exists(): try: meta = json.loads(meta_path.read_text()) packs.append({ "name": pack_dir.name, "version": meta.get("version", 1), "files_count": len(meta.get("files", {})) }) except: packs.append({"name": pack_dir.name, "error": "invalid"}) return {"packs": packs} @app.get("/pack/{pack_name}") async def get_pack(pack_name: str): meta_path = PACKS_DIR / pack_name / "meta.json" if meta_path.exists(): import json return json.loads(meta_path.read_text()) raise HTTPException(404, "Pack not found") @app.get("/pack/meta/{pack_name}") async def get_pack_meta(pack_name: str): return await get_pack(pack_name) @app.get("/pack/{pack_name}/diff") async def get_pack_diff(pack_name: str): # For mirror, just return empty diff (no local changes) return {"added": [], "removed": [], "changed": []} @app.get("/pack/{pack_name}/file/{file_path:path}") async def get_pack_file(pack_name: str, file_path: str, request: Request): return await send_file(PACKS_DIR / pack_name / file_path, request) config = uvicorn.Config(app, host="0.0.0.0", port=PORT, log_level="info") server = uvicorn.Server(config) await server.serve() async def main(): logging.info("Starting ZernMC Mirror Server") await sync_with_main() await run_server() if __name__ == "__main__": asyncio.run(main())