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
|
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]:
|
def get_version(self) -> tuple[int, int, int]:
|
||||||
"""
|
"""
|
||||||
Get the game version
|
Get the game version
|
||||||
|
@ -154,7 +154,7 @@ def repair_game(
|
|||||||
# ThreadPoolExecutor, probably better for this use case.
|
# ThreadPoolExecutor, probably better for this use case.
|
||||||
game_info = game.get_remote_game(pre_download=pre_download)
|
game_info = game.get_remote_game(pre_download=pre_download)
|
||||||
pkg_version_file = game.path.joinpath("pkg_version")
|
pkg_version_file = game.path.joinpath("pkg_version")
|
||||||
pkg_version = []
|
pkg_version: dict[str, dict[str, str]] = {}
|
||||||
if not pkg_version_file.is_file():
|
if not pkg_version_file.is_file():
|
||||||
try:
|
try:
|
||||||
game.repair_file(game.path.joinpath("pkg_version"), game_info=game_info)
|
game.repair_file(game.path.joinpath("pkg_version"), game_info=game_info)
|
||||||
@ -167,31 +167,61 @@ def repair_game(
|
|||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
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()
|
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:
|
try:
|
||||||
game.repair_file(target_file, game_info=game_info)
|
target_file = pkg_version.pop(relative_path_str)
|
||||||
except Exception as e:
|
if target_file:
|
||||||
print(f"Failed to repair {target_file['remoteName']}: {e}")
|
with file_path.open("rb", buffering=0) as f:
|
||||||
|
file_hash = hashlib.file_digest(f, "md5").hexdigest()
|
||||||
|
if file_hash == target_file["md5"]:
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
f"Hash mismatch for {target_file['remoteName']} ({file_hash}; expected {target_file['md5']})"
|
||||||
|
)
|
||||||
|
target_files.append(file_path)
|
||||||
|
except KeyError:
|
||||||
|
# File not found in pkg_version
|
||||||
|
read_needed_files.append(file_path)
|
||||||
|
|
||||||
def verify_and_repair(target_file, game_info):
|
repair_executor.submit(verify, file)
|
||||||
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
|
|
||||||
with file_path.open("rb", buffering=0) as f:
|
|
||||||
file_hash = hashlib.file_digest(f, "md5").hexdigest()
|
|
||||||
if file_hash != target_file["md5"]:
|
|
||||||
print(
|
|
||||||
f"Hash mismatch for {target_file['remoteName']} ({file_hash}; expected {target_file['md5']})"
|
|
||||||
)
|
|
||||||
repair(file_path, game_info)
|
|
||||||
|
|
||||||
# Single-threaded for now
|
|
||||||
# verify_and_repair(file, game_info)
|
|
||||||
repair_executor.submit(verify_and_repair, file, game_info)
|
|
||||||
repair_executor.shutdown(wait=True)
|
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 configparser import ConfigParser
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from io import IOBase
|
from io import IOBase
|
||||||
@ -349,6 +350,40 @@ class Game(GameABC):
|
|||||||
return diff
|
return diff
|
||||||
return None
|
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(
|
def repair_file(
|
||||||
self,
|
self,
|
||||||
file: PathLike,
|
file: PathLike,
|
||||||
@ -377,36 +412,42 @@ class Game(GameABC):
|
|||||||
game = game_info
|
game = game_info
|
||||||
if game.latest.decompressed_path is None:
|
if game.latest.decompressed_path is None:
|
||||||
raise ScatteredFilesNotAvailableError("Scattered files are not available.")
|
raise ScatteredFilesNotAvailableError("Scattered files are not available.")
|
||||||
# .replace("\\", "/") is needed because Windows uses backslashes :)
|
self._repair_file(file, game=game)
|
||||||
relative_file = file.relative_to(self._path)
|
|
||||||
url = (
|
def repair_files(
|
||||||
game.latest.decompressed_path + "/" + str(relative_file).replace("\\", "/")
|
self,
|
||||||
)
|
files: list[PathLike],
|
||||||
# Backup the file
|
pre_download: bool = False,
|
||||||
if file.exists():
|
game_info: resource.Game = None,
|
||||||
file.rename(file.with_suffix(file.suffix + ".bak"))
|
) -> None:
|
||||||
dest_file = file.with_suffix("")
|
"""
|
||||||
|
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:
|
else:
|
||||||
dest_file = file
|
game = game_info
|
||||||
try:
|
if game.latest.decompressed_path is None:
|
||||||
# Download the file
|
raise ScatteredFilesNotAvailableError("Scattered files are not available.")
|
||||||
temp_file = self.cache.joinpath(relative_file)
|
executor = concurrent.futures.ThreadPoolExecutor()
|
||||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
for file in files_path:
|
||||||
print(f"Downloading repair file {url} to {temp_file}")
|
executor.submit(self._repair_file, file, game=game)
|
||||||
download(url, temp_file, overwrite=True, stream=True)
|
# self._repair_file(file, game=game)
|
||||||
# Move the file
|
executor.shutdown(wait=True)
|
||||||
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)
|
|
||||||
|
|
||||||
def repair_game(self) -> None:
|
def repair_game(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user