diff --git a/update.bat b/update.bat new file mode 100644 index 0000000..dee8b1d --- /dev/null +++ b/update.bat @@ -0,0 +1,24 @@ +@echo off +cd /d "%~dp0" +title SteamPanel Updater +chcp 65001 >nul + +if exist "venv\Scripts\python.exe" ( + venv\Scripts\python.exe update.py + goto :end +) + +python --version >nul 2>&1 +if errorlevel 1 ( + echo. + echo [!] Python not found. + echo Run run.bat first to set up the environment, + echo or install Python from https://python.org + goto :end +) + +python update.py + +:end +echo. +pause diff --git a/update.py b/update.py new file mode 100644 index 0000000..0de806a --- /dev/null +++ b/update.py @@ -0,0 +1,282 @@ +import sys +import os +import subprocess +import urllib.request +import urllib.error +import zipfile +import shutil +import hashlib +import json +import tempfile +from pathlib import Path +from datetime import datetime + +GITHUB_REPO = "daimyomizukagebay61/SteamPanel" +BRANCH = "main" +ROOT = Path(__file__).parent + +# dirs to never touch when copying from ZIP +PROTECTED = {"data", "data_backup", "venv", ".git", "__pycache__"} + + +# ── helpers ────────────────────────────────────────────────────────────────── + +def get_version(): + ver_file = ROOT / "version.py" + if not ver_file.exists(): + return "unknown" + try: + ns = {} + exec(ver_file.read_text(encoding="utf-8"), ns) + return ns.get("VERSION", "unknown") + except Exception: + return "unknown" + + +def file_hash(path): + try: + return hashlib.md5(Path(path).read_bytes()).hexdigest() + except Exception: + return None + + +def git(*args, capture=False): + kwargs = {"cwd": str(ROOT)} + if capture: + kwargs["capture_output"] = True + kwargs["text"] = True + return subprocess.run(["git", "--no-pager"] + list(args), **kwargs) + + +def reinstall_requirements(): + print("Installing dependencies...") + venv_pip = ROOT / "venv" / "Scripts" / "pip.exe" + if venv_pip.exists(): + cmd = [str(venv_pip), "install", "-r", str(ROOT / "requirements.txt"), "-q"] + else: + cmd = [sys.executable, "-m", "pip", "install", "-r", str(ROOT / "requirements.txt"), "-q"] + subprocess.run(cmd) + hash_file = ROOT / "data" / ".req_hash" + hash_file.parent.mkdir(exist_ok=True) + hash_file.write_text(file_hash(ROOT / "requirements.txt") or "") + + +def rebuild_frontend(): + frontend = ROOT / "frontend" + if not frontend.exists(): + return + if subprocess.run(["npm", "--version"], capture_output=True).returncode != 0: + print("[!] Node.js not found — skipping frontend rebuild.") + print(" If UI looks broken: cd frontend && npm install && npm run build") + return + print("Rebuilding frontend...") + subprocess.run(["npm", "install", "--silent"], cwd=str(frontend)) + subprocess.run(["npm", "run", "build"], cwd=str(frontend)) + print("Frontend rebuilt.") + + +def download(url, dest): + req = urllib.request.Request(url, headers={"User-Agent": "SteamPanel-Updater"}) + with urllib.request.urlopen(req, timeout=60) as resp: + total = int(resp.headers.get("Content-Length", 0)) + done = 0 + with open(dest, "wb") as f: + while True: + chunk = resp.read(65536) + if not chunk: + break + f.write(chunk) + done += len(chunk) + if total: + print(f"\r {done * 100 // total:3d}% {done / 1048576:.1f} MB", end="", flush=True) + else: + print(f"\r {done / 1048576:.1f} MB", end="", flush=True) + print() + + +# ── git update path ─────────────────────────────────────────────────────────── + +def update_via_git(): + if subprocess.run(["git", "--version"], capture_output=True).returncode != 0: + print("[!] git not found. Install Git from https://git-scm.com") + return + + print("Checking for updates...") + if git("fetch", "origin", BRANCH).returncode != 0: + print("[!] Failed to reach GitHub. Check your internet connection.") + return + + local = git("rev-parse", "HEAD", capture=True).stdout.strip() + remote = git("rev-parse", f"origin/{BRANCH}", capture=True).stdout.strip() + + if local == remote: + print("\n[+] Already up to date.") + return + + print("\nNew commits:") + print("-" * 40) + git("log", f"HEAD..origin/{BRANCH}", "--oneline") + print("-" * 40) + + if input("\nUpdate now? [Y/N]: ").strip().upper() != "Y": + print("Cancelled.") + return + + # stash local changes so pull is clean + stash = git("stash", capture=True) + had_stash = "No local changes" not in (stash.stdout or "") + if had_stash: + print("Local changes stashed.") + + # backup branch as a safety net + backup = f"backup_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}" + git("branch", backup) + print(f"Backup branch: {backup}") + + req_hash_before = file_hash(ROOT / "requirements.txt") + frontend_hash_before = file_hash(ROOT / "frontend" / "package.json") + + print("Pulling updates...") + if git("pull", "origin", BRANCH).returncode != 0: + print("\n[!] Pull failed. Restoring backup branch...") + git("reset", "--hard", backup) + if had_stash: + git("stash", "pop") + return + + if had_stash: + result = git("stash", "pop") + if result.returncode != 0: + print("[!] Could not restore stashed changes automatically.") + print(f" Run: git stash pop (or check: git stash list)") + + new_ver = get_version() + print(f"\nUpdated: {local[:7]} -> {new_ver}") + + if file_hash(ROOT / "requirements.txt") != req_hash_before: + reinstall_requirements() + + if file_hash(ROOT / "frontend" / "package.json") != frontend_hash_before: + rebuild_frontend() + + print(f"\n[+] Update complete! Run run.bat to start.") + + +# ── ZIP download path ───────────────────────────────────────────────────────── + +def update_via_zip(): + print("No .git folder — using GitHub release download.\n") + + # fetch latest release info + api = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest" + zip_url = tag_name = None + try: + req = urllib.request.Request(api, headers={"User-Agent": "SteamPanel-Updater"}) + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read()) + zip_url = data.get("zipball_url") + tag_name = data.get("tag_name", "latest") + except urllib.error.HTTPError as e: + if e.code != 404: + print(f"[!] GitHub API error: {e}") + return + except Exception as e: + print(f"[!] Failed to reach GitHub: {e}") + return + + if not zip_url: + # no releases yet — grab main branch zipball + zip_url = f"https://api.github.com/repos/{GITHUB_REPO}/zipball/{BRANCH}" + tag_name = BRANCH + + current_ver = get_version() + print(f"Installed : {current_ver}") + print(f"Available : {tag_name}") + + if input("\nDownload and install? [Y/N]: ").strip().upper() != "Y": + print("Cancelled.") + return + + # backup data/ before touching anything + data_dir = ROOT / "data" + data_backup = ROOT / "data_backup" + if data_dir.exists(): + print("Backing up data/...") + if data_backup.exists(): + shutil.rmtree(data_backup) + shutil.copytree(data_dir, data_backup) + print("Backed up to data_backup/") + + with tempfile.TemporaryDirectory() as tmp: + zip_path = os.path.join(tmp, "update.zip") + + print("Downloading...") + try: + download(zip_url, zip_path) + except Exception as e: + print(f"[!] Download failed: {e}") + return + + req_hash_before = file_hash(ROOT / "requirements.txt") + frontend_hash_before = file_hash(ROOT / "frontend" / "package.json") + + print("Extracting...") + extract_dir = os.path.join(tmp, "src") + with zipfile.ZipFile(zip_path) as z: + z.extractall(extract_dir) + + # GitHub always wraps in one top-level folder (reponame-sha/) + entries = os.listdir(extract_dir) + source = Path(extract_dir) / entries[0] if len(entries) == 1 else Path(extract_dir) + + print("Installing...") + for item in source.iterdir(): + if item.name in PROTECTED: + continue + dest = ROOT / item.name + if item.is_dir(): + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(item, dest) + else: + shutil.copy2(item, dest) + + # safety net: restore data if something went wrong + if not data_dir.exists() and data_backup.exists(): + print("Restoring data/ from backup...") + shutil.copytree(data_backup, data_dir) + + if file_hash(ROOT / "requirements.txt") != req_hash_before: + reinstall_requirements() + elif not (ROOT / "data" / ".req_hash").exists(): + reinstall_requirements() + + if file_hash(ROOT / "frontend" / "package.json") != frontend_hash_before: + rebuild_frontend() + + new_ver = get_version() + if new_ver != current_ver: + print(f"\nUpdated: {current_ver} -> {new_ver}") + + print("\n[+] Update complete! Run run.bat to start.") + + +# ── entry point ─────────────────────────────────────────────────────────────── + +def main(): + print() + print(" SteamPanel Updater") + print(" ==================") + print(f"\n Current version: {get_version()}\n") + + if (ROOT / ".git").exists(): + update_via_git() + else: + update_via_zip() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nCancelled.")