compression added
This commit is contained in:
+95
-9
@@ -1,11 +1,13 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import gzip
|
||||
import hashlib
|
||||
import threading
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import subprocess
|
||||
import configparser
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
@@ -22,7 +24,7 @@ except ImportError:
|
||||
GAME_NAME = "My Game" # Shown in the window title
|
||||
UPDATER_NAME = "Launcher" # Updater, Launcher, etc.
|
||||
WINDOW_WIDTH = 450
|
||||
WINDOW_HEIGHT = 300
|
||||
WINDOW_HEIGHT = 280
|
||||
|
||||
# Banner image is scaled to exactly this size (pixels) regardless of the
|
||||
# source file's actual dimensions, so the layout never shifts if someone
|
||||
@@ -61,6 +63,13 @@ ICON_RELATIVE_PATH = "icon.ico"
|
||||
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
|
||||
# Suffix used for precompressed files on the server (must match
|
||||
# COMPRESSED_SUFFIX in generate_manifest.py).
|
||||
COMPRESSED_SUFFIX = ".gz"
|
||||
|
||||
# Local settings file (created next to the updater on first run).
|
||||
SETTINGS_FILENAME = "updater.ini"
|
||||
|
||||
# ============================================================
|
||||
# END CONFIGURATION
|
||||
# ============================================================
|
||||
@@ -82,6 +91,42 @@ BASE_PATH = get_base_path()
|
||||
BANNER_FILE = get_resource_path(BANNER_RELATIVE_PATH)
|
||||
ICON_FILE = get_resource_path(ICON_RELATIVE_PATH)
|
||||
GAME_EXE = os.path.join(BASE_PATH, GAME_EXE_RELATIVE_PATH)
|
||||
SETTINGS_FILE = os.path.join(BASE_PATH, SETTINGS_FILENAME)
|
||||
|
||||
|
||||
def load_settings():
|
||||
"""
|
||||
Reads settings.ini next to the updater. If it doesn't exist yet (first
|
||||
run) or is missing/invalid, creates it with default values and returns
|
||||
those defaults. Unknown/corrupt values fall back to the default rather
|
||||
than crashing the app.
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
settings = {"client": DEFAULT_CLIENT}
|
||||
|
||||
if os.path.exists(SETTINGS_FILE):
|
||||
try:
|
||||
config.read(SETTINGS_FILE, encoding="utf-8")
|
||||
client = config.get("updater", "client", fallback=DEFAULT_CLIENT)
|
||||
if client in CLIENT_KEYS:
|
||||
settings["client"] = client
|
||||
except (configparser.Error, OSError):
|
||||
pass # Fall back to defaults below and rewrite the file.
|
||||
else:
|
||||
save_settings(settings) # First run: create the file with defaults.
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
def save_settings(settings):
|
||||
"""Writes updater.ini next to the updater. Failures are non-fatal."""
|
||||
config = configparser.ConfigParser()
|
||||
config["updater"] = {"client": settings.get("client", DEFAULT_CLIENT)}
|
||||
try:
|
||||
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||
config.write(f)
|
||||
except OSError:
|
||||
pass # Not critical - the app just won't remember the choice.
|
||||
|
||||
|
||||
def build_server_base_url():
|
||||
@@ -92,6 +137,8 @@ def build_server_base_url():
|
||||
class UpdaterApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.settings = load_settings()
|
||||
|
||||
self.root.title(f"{GAME_NAME} {UPDATER_NAME}")
|
||||
self.root.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}")
|
||||
self.root.resizable(False, False)
|
||||
@@ -116,12 +163,13 @@ class UpdaterApp:
|
||||
|
||||
tk.Label(mode_frame, text="Client version:").pack(side="left")
|
||||
|
||||
self.mode_var = tk.StringVar(value=DEFAULT_CLIENT)
|
||||
self.mode_var = tk.StringVar(value=self.settings["client"])
|
||||
self.mode_combo = ttk.Combobox(
|
||||
mode_frame, textvariable=self.mode_var,
|
||||
values=list(CLIENT_KEYS.keys()), state="readonly", width=15
|
||||
)
|
||||
self.mode_combo.pack(side="left", padx=5)
|
||||
self.mode_combo.bind("<<ComboboxSelected>>", self.on_client_changed)
|
||||
|
||||
# Status text
|
||||
self.status_var = tk.StringVar(value="Select a version and click Check for Updates...")
|
||||
@@ -181,6 +229,10 @@ class UpdaterApp:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def on_client_changed(self, event=None):
|
||||
self.settings["client"] = self.mode_var.get()
|
||||
save_settings(self.settings)
|
||||
|
||||
def update_ui(self, text=None, prog_val=None, prog_max=None):
|
||||
if text: self.root.after(0, lambda: self.status_var.set(text))
|
||||
if prog_max: self.root.after(0, lambda: self.progress.config(maximum=prog_max))
|
||||
@@ -198,6 +250,9 @@ class UpdaterApp:
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
def get_md5_bytes(self, data):
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
def start_update(self):
|
||||
self.btn_update.config(state="disabled")
|
||||
self.btn_play.config(state="disabled")
|
||||
@@ -222,13 +277,13 @@ class UpdaterApp:
|
||||
total_files = len(server_manifest)
|
||||
|
||||
self.update_ui(prog_max=total_files, prog_val=0)
|
||||
for i, (rel_path, server_hash) in enumerate(server_manifest.items()):
|
||||
for i, (rel_path, entry) in enumerate(server_manifest.items()):
|
||||
self.update_ui(text=f"Checking: {rel_path}", prog_val=i)
|
||||
|
||||
local_path = os.path.join(BASE_PATH, os.path.normpath(rel_path))
|
||||
local_hash = self.get_md5(local_path)
|
||||
|
||||
if local_hash != server_hash:
|
||||
if local_hash != entry["hash"]:
|
||||
files_to_download.append(rel_path)
|
||||
|
||||
if files_to_download:
|
||||
@@ -238,13 +293,11 @@ class UpdaterApp:
|
||||
for i, rel_path in enumerate(files_to_download):
|
||||
self.update_ui(text=f"Downloading ({i + 1}/{total_dl}): {rel_path}", prog_val=i)
|
||||
|
||||
safe_url_path = urllib.parse.quote(rel_path, safe='/')
|
||||
file_url = current_server_url + safe_url_path
|
||||
|
||||
entry = server_manifest[rel_path]
|
||||
local_path = os.path.join(BASE_PATH, os.path.normpath(rel_path))
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
|
||||
urllib.request.urlretrieve(file_url, local_path)
|
||||
self._download_file(current_server_url, rel_path, entry, local_path)
|
||||
|
||||
self.update_ui(text=f"Client '{display_name}' is up to date! Ready to play.", prog_val=self.progress['maximum'])
|
||||
self.root.after(0, lambda: self.btn_play.config(state="normal"))
|
||||
@@ -256,6 +309,39 @@ class UpdaterApp:
|
||||
self.root.after(0, lambda: self.btn_update.config(state="normal"))
|
||||
self.root.after(0, lambda: self.mode_combo.config(state="readonly"))
|
||||
|
||||
def _download_file(self, base_url, rel_path, entry, local_path):
|
||||
"""
|
||||
Downloads one file. If the manifest marks it as compressed, fetches
|
||||
the .gz copy, verifies its hash, and decompresses it into local_path.
|
||||
Otherwise downloads the original file directly. Falls back to the
|
||||
uncompressed original if the compressed copy fails verification or
|
||||
download, so a bad/missing .gz on the server doesn't block the update.
|
||||
"""
|
||||
if entry.get("compressed"):
|
||||
gz_url = base_url + urllib.parse.quote(rel_path + COMPRESSED_SUFFIX, safe='/')
|
||||
try:
|
||||
with urllib.request.urlopen(gz_url, timeout=30) as resp:
|
||||
compressed_data = resp.read()
|
||||
|
||||
if self.get_md5_bytes(compressed_data) != entry.get("compressed_hash"):
|
||||
raise ValueError("compressed file hash mismatch")
|
||||
|
||||
data = gzip.decompress(compressed_data)
|
||||
|
||||
if self.get_md5_bytes(data) != entry["hash"]:
|
||||
raise ValueError("decompressed file hash mismatch")
|
||||
|
||||
with open(local_path, "wb") as f:
|
||||
f.write(data)
|
||||
return
|
||||
except Exception:
|
||||
# Fall through to downloading the uncompressed original below.
|
||||
pass
|
||||
|
||||
safe_url_path = urllib.parse.quote(rel_path, safe='/')
|
||||
file_url = base_url + safe_url_path
|
||||
urllib.request.urlretrieve(file_url, local_path)
|
||||
|
||||
def play_game(self):
|
||||
if os.path.exists(GAME_EXE):
|
||||
game_dir = os.path.dirname(GAME_EXE)
|
||||
@@ -275,4 +361,4 @@ class UpdaterApp:
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = UpdaterApp(root)
|
||||
root.mainloop()
|
||||
root.mainloop()
|
||||
|
||||
Reference in New Issue
Block a user