229 lines
8.4 KiB
Python
229 lines
8.4 KiB
Python
#!/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()) |