diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index 5e6f3bed..ec9a45bc 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -28,11 +28,12 @@ from rare.shared import ( ) from rare.shared.image_manager import ImageSize from rare.shared.workers.verify import VerifyWorker +from rare.shared.workers.move import MoveWorker from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.utils.misc import get_size from rare.utils.steam_grades import SteamWorker from rare.widgets.image_widget import ImageWidget -from .move_game import CopyGameInstallation, MoveGamePopUp, is_game_dir +from .move_game import MoveGamePopUp, is_game_dir logger = getLogger("GameInfo") @@ -157,12 +158,12 @@ class GameInfo(QWidget): verify_worker.signals.result.connect(self.__on_verify_result) verify_worker.signals.error.connect(self.__on_verify_error) self.ui.verify_progress.setValue(0) - self.rgame.active_worker = verify_worker + self.rgame.__worker = verify_worker self.verify_pool.start(verify_worker) self.ui.move_button.setEnabled(False) def verify_cleanup(self, rgame: RareGame): - rgame.active_worker = None + rgame.__worker = None self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page) self.ui.move_button.setEnabled(True) self.ui.verify_button.setEnabled(True) @@ -257,7 +258,8 @@ class GameInfo(QWidget): self.ui.move_progress.setValue(progress_int) def start_copy_diff_drive(self): - copy_worker = CopyGameInstallation( + copy_worker = MoveWorker( + self.core, install_path=self.rgame.igame.install_path, dest_path=self.dest_path_with_suffix, is_existing_dir=self.existing_game_dir, @@ -305,7 +307,7 @@ class GameInfo(QWidget): rgame = self.rcore.get_game(rgame) if self.rgame is not None: - if (worker := self.rgame.active_worker) is not None: + if (worker := self.rgame.__worker) is not None: if isinstance(worker, VerifyWorker): try: worker.signals.progress.disconnect(self.__on_verify_progress) @@ -316,7 +318,7 @@ class GameInfo(QWidget): self.rgame = rgame self.rgame.signals.game.installed.connect(self.update_game) self.rgame.signals.game.uninstalled.connect(self.update_game) - if (worker := self.rgame.active_worker) is not None: + if (worker := self.rgame.__worker) is not None: if isinstance(worker, VerifyWorker): self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page) self.ui.verify_progress.setValue(self.rgame.progress) @@ -381,7 +383,7 @@ class GameInfo(QWidget): self.ui.move_stack.setCurrentWidget(self.ui.move_button_page) # If a game is verifying or moving, disable both verify and moving buttons. - if rgame.active_worker is not None: + if rgame.__worker is not None: self.ui.verify_button.setEnabled(False) self.ui.move_button.setEnabled(False) if self.is_moving: diff --git a/rare/components/tabs/games/game_info/move_game.py b/rare/components/tabs/games/game_info/move_game.py index 2645d2bb..25c7d694 100644 --- a/rare/components/tabs/games/game_info/move_game.py +++ b/rare/components/tabs/games/game_info/move_game.py @@ -4,10 +4,8 @@ from logging import getLogger from pathlib import Path from typing import Tuple -from PyQt5.QtCore import pyqtSignal, QRunnable, QObject, Qt +from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog -from legendary.models.game import VerifyResult, InstalledGame -from legendary.lfs.utils import validate_files from rare.shared import LegendaryCoreSingleton from rare.utils.extra_widgets import PathEdit @@ -142,109 +140,6 @@ class MoveGamePopUp(QWidget): ) -# noinspection PyUnresolvedReferences -class CopyGameInstallation(QRunnable): - class Signals(QObject): - progress = pyqtSignal(int) - finished = pyqtSignal(str) - no_space_left = pyqtSignal() - - def __init__( - self, - install_path: str, - dest_path: Path, - is_existing_dir: bool, - igame: InstalledGame, - ): - super(CopyGameInstallation, self).__init__() - self.signals = CopyGameInstallation.Signals() - self.install_path = install_path - self.dest_path = dest_path - self.source_size = 0 - self.dest_size = 0 - self.is_existing_dir = is_existing_dir - self.core = LegendaryCoreSingleton() - self.igame = igame - self.file_list = None - self.total: int = 0 - - def run(self): - root_directory = Path(self.install_path) - self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file()) - - # if game dir is not existing, just copying: - if not self.is_existing_dir: - shutil.copytree( - self.install_path, - self.dest_path, - copy_function=self.copy_each_file_with_progress, - dirs_exist_ok=True, - ) - else: - manifest_data, _ = self.core.get_installed_manifest(self.igame.app_name) - manifest = self.core.load_manifest(manifest_data) - files = sorted( - manifest.file_manifest_list.elements, - key=lambda a: a.filename.lower(), - ) - self.file_list = [(f.filename, f.sha_hash.hex()) for f in files] - self.total = len(self.file_list) - - # recreate dir structure - shutil.copytree( - self.install_path, - self.dest_path, - copy_function=self.copy_dir_structure, - dirs_exist_ok=True, - ) - - for i, (result, relative_path, _, _) in enumerate( - validate_files(str(self.dest_path), self.file_list) - ): - dst_path = f"{self.dest_path}/{relative_path}" - src_path = f"{self.install_path}/{relative_path}" - if Path(src_path).is_file(): - if result == VerifyResult.HASH_MISMATCH: - try: - shutil.copy(src_path, dst_path) - except IOError: - self.signals.no_space_left.emit() - return - elif result == VerifyResult.FILE_MISSING: - try: - shutil.copy(src_path, dst_path) - except (IOError, OSError): - self.signals.no_space_left.emit() - return - elif result == VerifyResult.OTHER_ERROR: - logger.warning(f"Copying file {src_path} to {dst_path} failed") - self.signals.progress.emit(int(i * 10 / self.total * 10)) - else: - logger.warning( - f"Source dir does not have file {src_path}. File will be missing in the destination " - f"dir. " - ) - - shutil.rmtree(self.install_path) - self.signals.finished.emit(str(self.dest_path)) - - def copy_each_file_with_progress(self, src, dst): - shutil.copy(src, dst) - self.dest_size += Path(src).stat().st_size - self.signals.progress.emit(int(self.dest_size * 10 / self.source_size * 10)) - - # This method is a copy_func, and only copies the src if it's a dir. - # Thus, it can be used to re-create the dir strucute. - @staticmethod - def copy_dir_structure(src, dst): - if os.path.isdir(dst): - dst = os.path.join(dst, os.path.basename(src)) - if os.path.isdir(src): - shutil.copyfile(src, dst) - shutil.copystat(src, dst) - return dst - - def is_game_dir(install_path: Path, dest_path: Path): # This iterates over the destination dir, then iterates over the current install dir and if the file names # matches, we have an exisiting dir diff --git a/rare/models/game.py b/rare/models/game.py index 4a1d84aa..a2bd8fea 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -14,6 +14,8 @@ from rare.lgndr.core import LegendaryCore from rare.models.install import InstallOptionsModel, UninstallOptionsModel from rare.shared.game_process import GameProcess from rare.shared.image_manager import ImageManager +from rare.shared.workers.move import MoveWorker +from rare.shared.workers.verify import VerifyWorker from rare.utils.misc import get_rare_executable from rare.utils.paths import data_dir @@ -117,7 +119,7 @@ class RareGame(QObject): logger.info(f"Update available for game: {self.app_name} ({self.app_title})") self.progress: int = 0 - self.active_worker: Optional[QRunnable] = None + self.__worker: Optional[QRunnable] = None self.__state = RareGame.State.IDLE @@ -128,6 +130,20 @@ class RareGame(QObject): self.game_process.connect_to_server(on_startup=True) # self.grant_date(True) + @property + def worker(self) -> Optional[QRunnable]: + return self.__worker + + @worker.setter + def worker(self, worker: Optional[QRunnable]): + if worker is None: + self.state = RareGame.State.IDLE + if isinstance(worker, VerifyWorker): + self.state = RareGame.State.VERIFYING + if isinstance(worker, MoveWorker): + self.state = RareGame.State.MOVING + self.__worker = worker + @property def state(self) -> 'RareGame.State': return self.__state @@ -138,6 +154,10 @@ class RareGame(QObject): self.signals.widget.update.emit() self.__state = state + @property + def is_idle(self): + return self.state == RareGame.State.IDLE + @pyqtSlot(int) def __game_launched(self, code: int): if code == GameProcess.Code.ON_STARTUP: diff --git a/rare/shared/workers/move.py b/rare/shared/workers/move.py new file mode 100644 index 00000000..a33677af --- /dev/null +++ b/rare/shared/workers/move.py @@ -0,0 +1,111 @@ +import os +import shutil +from pathlib import Path + +from PyQt5.QtCore import pyqtSignal, QRunnable, QObject +from legendary.lfs.utils import validate_files +from legendary.models.game import VerifyResult, InstalledGame + + +# noinspection PyUnresolvedReferences +class MoveWorker(QRunnable): + class Signals(QObject): + progress = pyqtSignal(int) + finished = pyqtSignal(str) + no_space_left = pyqtSignal() + + def __init__( + self, + core: LegendaryCore, + install_path: str, + dest_path: Path, + is_existing_dir: bool, + igame: InstalledGame, + ): + super(MoveWorker, self).__init__() + self.signals = MoveWorker.Signals() + self.core = core + self.install_path = install_path + self.dest_path = dest_path + self.source_size = 0 + self.dest_size = 0 + self.is_existing_dir = is_existing_dir + self.igame = igame + self.file_list = None + self.total: int = 0 + + def run(self): + root_directory = Path(self.install_path) + self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file()) + + # if game dir is not existing, just copying: + if not self.is_existing_dir: + shutil.copytree( + self.install_path, + self.dest_path, + copy_function=self.copy_each_file_with_progress, + dirs_exist_ok=True, + ) + else: + manifest_data, _ = self.core.get_installed_manifest(self.igame.app_name) + manifest = self.core.load_manifest(manifest_data) + files = sorted( + manifest.file_manifest_list.elements, + key=lambda a: a.filename.lower(), + ) + self.file_list = [(f.filename, f.sha_hash.hex()) for f in files] + self.total = len(self.file_list) + + # recreate dir structure + shutil.copytree( + self.install_path, + self.dest_path, + copy_function=self.copy_dir_structure, + dirs_exist_ok=True, + ) + + for i, (result, relative_path, _, _) in enumerate( + validate_files(str(self.dest_path), self.file_list) + ): + dst_path = f"{self.dest_path}/{relative_path}" + src_path = f"{self.install_path}/{relative_path}" + if Path(src_path).is_file(): + if result == VerifyResult.HASH_MISMATCH: + try: + shutil.copy(src_path, dst_path) + except IOError: + self.signals.no_space_left.emit() + return + elif result == VerifyResult.FILE_MISSING: + try: + shutil.copy(src_path, dst_path) + except (IOError, OSError): + self.signals.no_space_left.emit() + return + elif result == VerifyResult.OTHER_ERROR: + logger.warning(f"Copying file {src_path} to {dst_path} failed") + self.signals.progress.emit(int(i * 10 / self.total * 10)) + else: + logger.warning( + f"Source dir does not have file {src_path}. File will be missing in the destination " + f"dir. " + ) + + shutil.rmtree(self.install_path) + self.signals.finished.emit(str(self.dest_path)) + + def copy_each_file_with_progress(self, src, dst): + shutil.copy(src, dst) + self.dest_size += Path(src).stat().st_size + self.signals.progress.emit(int(self.dest_size * 10 / self.source_size * 10)) + + # This method is a copy_func, and only copies the src if it's a dir. + # Thus, it can be used to re-create the dir strucute. + @staticmethod + def copy_dir_structure(src, dst): + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) + if os.path.isdir(src): + shutil.copyfile(src, dst) + shutil.copystat(src, dst) + return dst