feat(repair): rework the repair feature
Mostly usable now
This commit is contained in:
parent
e8f63f175f
commit
08c51d2fd8
@ -87,6 +87,28 @@ class GameABC(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def repair_files(
|
||||
self,
|
||||
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.
|
||||
|
||||
This method is not multi-threaded, so it may take a while to repair
|
||||
multiple files.
|
||||
|
||||
Args:
|
||||
file (PathLike): The file to repair.
|
||||
pre_download (bool): Whether to get the pre-download version.
|
||||
Defaults to False.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_version(self) -> tuple[int, int, int]:
|
||||
"""
|
||||
Get the game version
|
||||
|
@ -154,7 +154,7 @@ def repair_game(
|
||||
# ThreadPoolExecutor, probably better for this use case.
|
||||
game_info = game.get_remote_game(pre_download=pre_download)
|
||||
pkg_version_file = game.path.joinpath("pkg_version")
|
||||
pkg_version = []
|
||||
pkg_version: dict[str, dict[str, str]] = {}
|
||||
if not pkg_version_file.is_file():
|
||||
try:
|
||||
game.repair_file(game.path.joinpath("pkg_version"), game_info=game_info)
|
||||
@ -167,31 +167,61 @@ def repair_game(
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
pkg_version.append(json.loads(line))
|
||||
line_json = json.loads(line)
|
||||
pkg_version[line_json["remoteName"]] = {
|
||||
"md5": line_json["md5"],
|
||||
"fileSize": line_json["fileSize"],
|
||||
}
|
||||
read_needed_files: list[Path] = []
|
||||
target_files: list[Path] = []
|
||||
repair_executor = concurrent.futures.ThreadPoolExecutor()
|
||||
for file in pkg_version:
|
||||
for file in game.path.rglob("*"):
|
||||
# Ignore webCaches folder (because it's user data)
|
||||
if file.is_dir():
|
||||
continue
|
||||
if "webCaches" in str(file):
|
||||
continue
|
||||
|
||||
def repair(target_file, game_info):
|
||||
def verify(file_path: Path):
|
||||
nonlocal target_files
|
||||
nonlocal pkg_version
|
||||
relative_path = file_path.relative_to(game.path)
|
||||
relative_path_str = str(relative_path).replace("\\", "/")
|
||||
# print(relative_path_str)
|
||||
# Wtf mihoyo, you build this game for Windows and then use Unix path separator :moyai:
|
||||
try:
|
||||
game.repair_file(target_file, game_info=game_info)
|
||||
except Exception as e:
|
||||
print(f"Failed to repair {target_file['remoteName']}: {e}")
|
||||
|
||||
def verify_and_repair(target_file, game_info):
|
||||
file_path = game.path.joinpath(target_file["remoteName"])
|
||||
if not file_path.is_file():
|
||||
print(f"File {target_file['remoteName']} not found, repairing...")
|
||||
repair(file_path, game_info)
|
||||
return
|
||||
target_file = pkg_version.pop(relative_path_str)
|
||||
if target_file:
|
||||
with file_path.open("rb", buffering=0) as f:
|
||||
file_hash = hashlib.file_digest(f, "md5").hexdigest()
|
||||
if file_hash != target_file["md5"]:
|
||||
if file_hash == target_file["md5"]:
|
||||
return
|
||||
print(
|
||||
f"Hash mismatch for {target_file['remoteName']} ({file_hash}; expected {target_file['md5']})"
|
||||
)
|
||||
repair(file_path, game_info)
|
||||
target_files.append(file_path)
|
||||
except KeyError:
|
||||
# File not found in pkg_version
|
||||
read_needed_files.append(file_path)
|
||||
|
||||
# Single-threaded for now
|
||||
# verify_and_repair(file, game_info)
|
||||
repair_executor.submit(verify_and_repair, file, game_info)
|
||||
repair_executor.submit(verify, file)
|
||||
repair_executor.shutdown(wait=True)
|
||||
for file in read_needed_files:
|
||||
try:
|
||||
with file.open("rb", buffering=0) as f:
|
||||
# We only need to read 4 bytes to see if the file is readable or not
|
||||
f.read(4)
|
||||
except Exception:
|
||||
print(f"File {file=} is corrupted.")
|
||||
target_files.append(file)
|
||||
# value not used for now
|
||||
for key, _ in pkg_version.items():
|
||||
target_file = game.path.joinpath(key)
|
||||
if target_file.is_file():
|
||||
continue
|
||||
print(f"{key} not found.")
|
||||
target_files.append(target_file)
|
||||
if not target_files:
|
||||
return
|
||||
print("Begin repairing files...")
|
||||
game.repair_files(target_files, game_info=game_info)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import concurrent.futures
|
||||
from configparser import ConfigParser
|
||||
from hashlib import md5
|
||||
from io import IOBase
|
||||
@ -349,6 +350,40 @@ class Game(GameABC):
|
||||
return diff
|
||||
return None
|
||||
|
||||
def _repair_file(self, file: PathLike, game: resource.Game) -> None:
|
||||
# .replace("\\", "/") is needed because Windows uses backslashes :)
|
||||
relative_file = file.relative_to(self._path)
|
||||
url = (
|
||||
game.latest.decompressed_path + "/" + 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,
|
||||
@ -377,36 +412,42 @@ class Game(GameABC):
|
||||
game = game_info
|
||||
if game.latest.decompressed_path is None:
|
||||
raise ScatteredFilesNotAvailableError("Scattered files are not available.")
|
||||
# .replace("\\", "/") is needed because Windows uses backslashes :)
|
||||
relative_file = file.relative_to(self._path)
|
||||
url = (
|
||||
game.latest.decompressed_path + "/" + str(relative_file).replace("\\", "/")
|
||||
)
|
||||
# Backup the file
|
||||
if file.exists():
|
||||
file.rename(file.with_suffix(file.suffix + ".bak"))
|
||||
dest_file = file.with_suffix("")
|
||||
self._repair_file(file, game=game)
|
||||
|
||||
def repair_files(
|
||||
self,
|
||||
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:
|
||||
file (PathLike): The file to repair.
|
||||
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:
|
||||
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
|
||||
else:
|
||||
# Delete the backup
|
||||
if file.exists():
|
||||
file.unlink(missing_ok=True)
|
||||
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)
|
||||
|
||||
def repair_game(self) -> None:
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user