From 0a5b53ab6f2a69a639a7a9eec1888de0f4f8588f Mon Sep 17 00:00:00 2001 From: derrod Date: Wed, 10 Jun 2020 18:21:47 +0200 Subject: [PATCH] [cli/core/utils] Only remove files in manifest during uninstall Some games are using the installation directory to store savegames. To avoid deleting those, only remove files that are actually in the manifest and only delete the directory if it is empty. --- legendary/cli.py | 12 +++++------- legendary/core.py | 16 ++++++++++----- legendary/utils/lfs.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/legendary/cli.py b/legendary/cli.py index b5e9431..9d1c38e 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -697,17 +697,15 @@ class LegendaryCLI: exit(0) try: - logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') - self.core.uninstall_game(igame) - - # DLCs are already removed once we delete the main game, so this just removes them from the list + # Remove DLC first so directory is empty when game uninstall runs dlcs = self.core.get_dlc_for_game(igame.app_name) for dlc in dlcs: - idlc = self.core.get_installed_game(dlc.app_name) - if self.core.is_installed(dlc.app_name): + if (idlc := self.core.get_installed_game(dlc.app_name)) is not None: logger.info(f'Uninstalling DLC "{dlc.app_name}"...') - self.core.uninstall_game(idlc, delete_files=False) + self.core.uninstall_game(idlc) + logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') + self.core.uninstall_game(igame, delete_root_directory=True) logger.info('Game has been uninstalled.') except Exception as e: logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.') diff --git a/legendary/core.py b/legendary/core.py index f22e381..fc541ef 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -21,7 +21,7 @@ from legendary.api.egs import EPCAPI from legendary.downloader.manager import DLManager from legendary.lfs.egl import EPCLFS from legendary.lfs.lgndry import LGDLFS -from legendary.utils.lfs import clean_filename, delete_folder +from legendary.utils.lfs import clean_filename, delete_folder, delete_filelist from legendary.models.downloading import AnalysisResult, ConditionCheckResult from legendary.models.egl import EGLManifest from legendary.models.exceptions import * @@ -828,14 +828,20 @@ class LegendaryCore: return dict() - def uninstall_game(self, installed_game: InstalledGame, delete_files=True): - self.lgd.remove_installed_game(installed_game.app_name) + def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delete_root_directory=False): if installed_game.egl_guid: self.egl_uninstall(installed_game, delete_files=delete_files) if delete_files: - if not delete_folder(installed_game.install_path, recursive=True): - self.log.error(f'Unable to delete "{installed_game.install_path}" from disk, please remove manually.') + try: + manifest = self.load_manifest(self.get_installed_manifest(installed_game.app_name)[0]) + filelist = [fm.filename for fm in manifest.file_manifest_list.elements] + if not delete_filelist(installed_game.install_path, filelist, delete_root_directory): + self.log.error(f'Deleting "{installed_game.install_path}" failed, please remove manually.') + except Exception as e: + self.log.error(f'Deleting failed with {e!r}, please remove {installed_game.install_path} manually.') + + self.lgd.remove_installed_game(installed_game.app_name) def prereq_installed(self, app_name): igame = self.lgd.get_installed_game(app_name) diff --git a/legendary/utils/lfs.py b/legendary/utils/lfs.py index e293ac3..c549cff 100644 --- a/legendary/utils/lfs.py +++ b/legendary/utils/lfs.py @@ -26,6 +26,50 @@ def delete_folder(path: str, recursive=True) -> bool: return True +def delete_filelist(path: str, filenames: List[str], + delete_root_directory: bool = False) -> bool: + dirs = set() + no_error = True + + # delete all files that were installed + for filename in filenames: + _dir, _fn = os.path.split(filename) + if _dir: + dirs.add(_dir) + + try: + os.remove(os.path.join(path, _dir, _fn)) + except Exception as e: + logger.error(f'Failed deleting file {filename} with {e!r}') + no_error = False + + # add intermediate directories that would have been missed otherwise + for _dir in sorted(dirs): + head, _ = os.path.split(_dir) + while head: + dirs.add(head) + head, _ = os.path.split(head) + + # remove all directories + for _dir in sorted(dirs, key=len, reverse=True): + try: + os.rmdir(os.path.join(path, _dir)) + except FileNotFoundError: + # directory has already been deleted, ignore that + continue + except Exception as e: + logger.error(f'Failed removing directory "{_dir}" with {e!r}') + no_error = False + + if delete_root_directory: + try: + os.rmdir(path) + except Exception as e: + logger.error(f'Removing game directory failed with {e!r}') + + return no_error + + def validate_files(base_path: str, filelist: List[tuple], hash_type='sha1') -> Iterator[tuple]: """ Validates the files in filelist in path against the provided hashes