From a3df4984449a4326f05e3fa2422c67883e98c749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Fri, 13 Dec 2024 11:26:48 +0700 Subject: [PATCH] chore: move repair-related functions to common Preparation for Genshin Impact support xD --- vollerei/common/functions.py | 81 ++++++++++++++++++++++++++++++++++- vollerei/hsr/launcher/game.py | 70 ++---------------------------- 2 files changed, 83 insertions(+), 68 deletions(-) diff --git a/vollerei/common/functions.py b/vollerei/common/functions.py index 0f2c504..86f1261 100644 --- a/vollerei/common/functions.py +++ b/vollerei/common/functions.py @@ -3,10 +3,17 @@ import json import hashlib import py7zr from io import IOBase +from os import PathLike from pathlib import Path +from shutil import move, copyfile from vollerei.abc.launcher.game import GameABC -from vollerei.exceptions.game import RepairError -from vollerei.utils import HDiffPatch, HPatchZPatchError +from vollerei.common.api import resource +from vollerei.exceptions.game import ( + RepairError, + GameNotInstalledError, + ScatteredFilesNotAvailableError, +) +from vollerei.utils import HDiffPatch, HPatchZPatchError, download _hdiff = HDiffPatch() @@ -139,6 +146,74 @@ def apply_update_archive( archive.close() +def _repair_file(game: GameABC, file: PathLike, game_info: resource.Main) -> None: + # .replace("\\", "/") is needed because Windows uses backslashes :) + relative_file = file.relative_to(game.path) + url = game_info.major.res_list_url + "/" + str(relative_file).replace("\\", "/") + # Backup the file + if file.exists(): + backup_file = file.with_suffix(file.suffix + ".bak") + if backup_file.exists(): + backup_file.unlink() + file.rename(backup_file) + dest_file = file.with_suffix("") + else: + dest_file = file + try: + # Download the file + temp_file = game.cache.joinpath(relative_file) + dest_file.parent.mkdir(parents=True, exist_ok=True) + print(f"Downloading repair file {url} to {temp_file}") + download(url, temp_file, overwrite=True, stream=True) + # Move the file + move(temp_file, dest_file) + print("OK") + except Exception as e: + # Restore the backup + print("Failed", e) + if file.exists(): + file.rename(file.with_suffix("")) + raise e + # Delete the backup + if file.exists(): + file.unlink(missing_ok=True) + + +def repair_files( + game: GameABC, + files: list[PathLike], + pre_download: bool = False, + game_info: resource.Game = None, +) -> None: + """ + Repairs multiple game files. + + This will automatically handle backup and restore the file if the repair + fails. + + Args: + game (GameABC): The game to repair the files for. + file (PathLike): The file to repair. + pre_download (bool): Whether to get the pre-download version. + Defaults to False. + """ + if not game.is_installed(): + raise GameNotInstalledError("Game is not installed.") + files_path = [Path(file) for file in files] + for file in files_path: + if not file.is_relative_to(game.path): + raise ValueError("File is not in the game folder.") + if not game_info: + game_info = game.get_remote_game(pre_download=pre_download) + if game_info.latest.decompressed_path is None: + raise ScatteredFilesNotAvailableError("Scattered files are not available.") + executor = concurrent.futures.ThreadPoolExecutor() + for file in files_path: + executor.submit(_repair_file, file, game=game_info) + # self._repair_file(file, game=game) + executor.shutdown(wait=True) + + def repair_game( game: GameABC, pre_download: bool = False, @@ -154,6 +229,8 @@ def repair_game( # Most code here are copied from worthless-launcher. # worthless-launcher uses asyncio for multithreading while this one uses # ThreadPoolExecutor, probably better for this use case. + if not game.is_installed(): + raise GameNotInstalledError("Game is not installed.") game_info = game.get_remote_game(pre_download=pre_download) pkg_version_file = game.path.joinpath("pkg_version") pkg_version: dict[str, dict[str, str]] = {} diff --git a/vollerei/hsr/launcher/game.py b/vollerei/hsr/launcher/game.py index f682164..1fa33e5 100644 --- a/vollerei/hsr/launcher/game.py +++ b/vollerei/hsr/launcher/game.py @@ -1,10 +1,8 @@ -import concurrent.futures from configparser import ConfigParser from hashlib import md5 from io import IOBase from os import PathLike from pathlib import Path, PurePath -from shutil import move, copyfile from vollerei.abc.launcher.game import GameABC from vollerei.common import ConfigFile, functions from vollerei.common.api import resource @@ -13,7 +11,6 @@ from vollerei.exceptions.game import ( GameAlreadyUpdatedError, GameNotInstalledError, PreDownloadNotAvailable, - ScatteredFilesNotAvailableError, ) from vollerei.hsr.constants import MD5SUMS from vollerei.hsr.launcher import api @@ -357,38 +354,6 @@ class Game(GameABC): return patch return None - def _repair_file(self, file: PathLike, game: resource.Main) -> None: - # .replace("\\", "/") is needed because Windows uses backslashes :) - relative_file = file.relative_to(self._path) - url = game.major.res_list_url + "/" + str(relative_file).replace("\\", "/") - # Backup the file - if file.exists(): - backup_file = file.with_suffix(file.suffix + ".bak") - if backup_file.exists(): - backup_file.unlink() - file.rename(backup_file) - dest_file = file.with_suffix("") - else: - dest_file = file - try: - # Download the file - temp_file = self.cache.joinpath(relative_file) - dest_file.parent.mkdir(parents=True, exist_ok=True) - print(f"Downloading repair file {url} to {temp_file}") - download(url, temp_file, overwrite=True, stream=True) - # Move the file - copyfile(temp_file, dest_file) - print("OK") - except Exception as e: - # Restore the backup - print("Failed", e) - if file.exists(): - file.rename(file.with_suffix("")) - raise e - # Delete the backup - if file.exists(): - file.unlink(missing_ok=True) - def repair_file( self, file: PathLike, @@ -406,18 +371,7 @@ class Game(GameABC): pre_download (bool): Whether to get the pre-download version. Defaults to False. """ - if not self.is_installed(): - raise GameNotInstalledError("Game is not installed.") - file = Path(file) - if not file.is_relative_to(self._path): - raise ValueError("File is not in the game folder.") - if not game_info: - game = self.get_remote_game(pre_download=pre_download) - else: - game = game_info - if isinstance(game.major, str | None) or game.major.res_list_url in [None, ""]: - raise ScatteredFilesNotAvailableError("Scattered files are not available.") - self._repair_file(file, game=game) + return self.repair_files([file], pre_download=pre_download, game_info=game_info) def repair_files( self, @@ -436,31 +390,15 @@ class Game(GameABC): pre_download (bool): Whether to get the pre-download version. Defaults to False. """ - if not self.is_installed(): - raise GameNotInstalledError("Game is not installed.") - files_path = [Path(file) for file in files] - for file in files_path: - if not file.is_relative_to(self._path): - raise ValueError("File is not in the game folder.") - if not game_info: - game = self.get_remote_game(pre_download=pre_download) - else: - game = game_info - if game.latest.decompressed_path is None: - raise ScatteredFilesNotAvailableError("Scattered files are not available.") - executor = concurrent.futures.ThreadPoolExecutor() - for file in files_path: - executor.submit(self._repair_file, file, game=game) - # self._repair_file(file, game=game) - executor.shutdown(wait=True) + functions.repair_files( + self, files, pre_download=pre_download, game_info=game_info + ) def repair_game(self) -> None: """ Tries to repair the game by reading "pkg_version" file and downloading the mismatched files from the server. """ - if not self.is_installed(): - raise GameNotInstalledError("Game is not installed.") functions.repair_game(self) def apply_update_archive(