Launcher UI redesign + server mirror sync + file download optimization
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user