From f40cf7afedf29b3c2b2cb43caaf3a5b4174c8ef9 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Thu, 7 May 2026 16:44:10 +0000 Subject: [PATCH] Server: Add legacy build support - Add version parsing to distinguish new vs legacy format builds - New format: ZernMC-win-*.zip (1.0.8+ with bundled JRE21/JavaFX) - Legacy: ZernMCLauncher-*.zip (< 1.0.8 or with suffix) - /launcher/download/latest now returns new format by default - Add /launcher/download/legacy endpoint for old builds - Add legacy info to /launcher/info and /launcher/version responses - Update download_zip to accept both ZernMCLauncher- and ZernMC-win- patterns --- server/main.py | 166 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 148 insertions(+), 18 deletions(-) diff --git a/server/main.py b/server/main.py index 8706ebe..599d180 100644 --- a/server/main.py +++ b/server/main.py @@ -696,14 +696,66 @@ def get_current_launcher_version() -> str: return "1.0.0" +def parse_version(version_str: str) -> dict: + """Parse version string to determine if it's new or legacy format""" + import re + + match = re.match(r'^(\d+)\.(\d+)\.(\d+)(.*)$', version_str) + if not match: + return {"major": 0, "minor": 0, "patch": 0, "suffix": version_str, "is_legacy": True} + + major, minor, patch, suffix = match.groups() + suffix = suffix.strip("-") + + is_legacy = bool(suffix) + + return { + "major": int(major), + "minor": int(minor), + "patch": int(patch), + "suffix": suffix, + "is_legacy": is_legacy + } + + +def is_new_format(filename: str) -> bool: + """Check if filename represents new format build""" + return filename.startswith("ZernMC-win-") + + def get_available_zips() -> list: - """Get list of available zip archives""" + """Get list of available zip archives (new format only)""" if not BUILDS_DIR.exists(): return [] zips = [] for zip_file in BUILDS_DIR.glob("ZernMCLauncher-*.zip"): + if is_new_format(zip_file.name): + continue + version = zip_file.stem.replace("ZernMCLauncher-", "") + parsed = parse_version(version) + stat = zip_file.stat() + zips.append({ + "version": version, + "filename": zip_file.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), + "is_legacy": parsed["is_legacy"] + }) + + zips.sort(key=lambda x: x["version"], reverse=True) + return zips + + +def get_new_format_zips() -> list: + """Get list of available zip archives (new format: ZernMC-win-*.zip)""" + if not BUILDS_DIR.exists(): + return [] + + zips = [] + for zip_file in BUILDS_DIR.glob("ZernMC-win-*.zip"): + version = zip_file.stem.replace("ZernMC-win-", "") stat = zip_file.stat() zips.append({ "version": version, @@ -716,11 +768,42 @@ def get_available_zips() -> list: return zips +def get_legacy_zips() -> list: + """Get list of available legacy zip archives (< 1.0.8 or with suffix)""" + if not BUILDS_DIR.exists(): + return [] + + zips = [] + for zip_file in BUILDS_DIR.glob("ZernMCLauncher-*.zip"): + version = zip_file.stem.replace("ZernMCLauncher-", "") + parsed = parse_version(version) + + is_legacy = ( + parsed["is_legacy"] or + (parsed["major"] < 1) or + (parsed["major"] == 1 and parsed["minor"] == 0 and parsed["patch"] < 8) + ) + + if is_legacy: + stat = zip_file.stat() + zips.append({ + "version": version, + "filename": zip_file.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), + "is_legacy": True + }) + + zips.sort(key=lambda x: x["version"], reverse=True) + return zips + + @app.get("/launcher/version") async def get_launcher_version(): """Return launcher version information""" version = get_current_launcher_version() - zips = get_available_zips() + new_zips = get_new_format_zips() + legacy_zips = get_legacy_zips() response = { "version": version, @@ -737,11 +820,15 @@ async def get_launcher_version(): response["download_exe"] = "/launcher/download/exe" response["exe_size"] = exe_path.stat().st_size - if zips: - response["download_zip"] = f"/launcher/download/zip/{zips[0]['filename']}" - response["zip_version"] = zips[0]["version"] - response["zip_size"] = zips[0]["size"] - response["all_zips"] = zips + if new_zips: + response["download_zip"] = f"/launcher/download/zip/{new_zips[0]['filename']}" + response["zip_version"] = new_zips[0]["version"] + response["zip_size"] = new_zips[0]["size"] + response["all_zips"] = new_zips + + if legacy_zips: + response["legacy_zips"] = legacy_zips + response["legacy_download_url"] = "/launcher/download/legacy" return response @@ -796,25 +883,56 @@ async def download_launcher_zip(filename: str): @app.get("/launcher/download/latest") async def download_latest_launcher(): - """Download the latest launcher (prefer ZIP if available, fallback to JAR)""" - zips = get_available_zips() + """Download the latest launcher (new format: ZernMC-win-*.zip)""" + zips = get_new_format_zips() if zips: latest_zip = zips[0]["filename"] return await download_launcher_zip(latest_zip) - jar_path = BUILDS_DIR / "ZernMCLauncher.jar" - if jar_path.exists(): - return await download_launcher_jar() + raise HTTPException(404, "No new format launcher files available") + + +@app.get("/launcher/download/legacy") +async def download_legacy_launcher(): + """Download the latest legacy launcher (< 1.0.8 or with suffix)""" + zips = get_legacy_zips() - raise HTTPException(404, "No launcher files available") + if zips: + latest_zip = zips[0]["filename"] + return await download_launcher_zip(latest_zip) + + raise HTTPException(404, "No legacy launcher files available") + + +@app.get("/launcher/download/zip/{filename}") +async def download_launcher_zip(filename: str): + """Download specific launcher ZIP archive""" + if ".." in filename: + raise HTTPException(400, "Invalid filename") + + valid_patterns = ["ZernMCLauncher-", "ZernMC-win-"] + if not any(filename.startswith(p) for p in valid_patterns) or not filename.endswith(".zip"): + raise HTTPException(400, "Invalid filename") + + file_path = BUILDS_DIR / filename + + if not file_path.exists(): + raise HTTPException(404, "ZIP file not found") + + return FileResponse( + path=file_path, + filename=filename, + media_type="application/zip" + ) @app.get("/launcher/info") async def get_launcher_full_info(): """Full launcher information with all available files""" version = get_current_launcher_version() - zips = get_available_zips() + new_zips = get_new_format_zips() + legacy_zips = get_legacy_zips() info = { "current_version": version, @@ -822,9 +940,21 @@ async def get_launcher_full_info(): "files": { "jar": None, "exe": None, - "zips": zips + "zips": new_zips + legacy_zips }, - "recommended": "zip" if zips else ("exe" if (BUILDS_DIR / "ZernMCLauncher.exe").exists() else "jar") + "recommended": "zip" if new_zips else ("exe" if (BUILDS_DIR / "ZernMCLauncher.exe").exists() else "jar"), + "new_format": { + "available": len(new_zips) > 0, + "latest": new_zips[0] if new_zips else None, + "download_url": "/launcher/download/latest" + }, + "legacy": { + "available": len(legacy_zips) > 0, + "count": len(legacy_zips), + "latest": legacy_zips[0] if legacy_zips else None, + "download_url": "/launcher/download/legacy", + "warning": "Legacy builds are technically compatible but not recommended. Consider upgrading to new format." + } } jar_path = BUILDS_DIR / "ZernMCLauncher.jar" @@ -841,8 +971,8 @@ async def get_launcher_full_info(): "download_url": "/launcher/download/exe" } - if zips: - info["files"]["latest_zip"] = zips[0] + if new_zips: + info["files"]["latest_zip"] = new_zips[0] info["files"]["download_latest"] = "/launcher/download/latest" return info