From aaf7e0934f8893b603006b7ba936b898d8f7e0f1 Mon Sep 17 00:00:00 2001 From: derrod Date: Fri, 22 Oct 2021 04:22:37 +0200 Subject: [PATCH] [core/utils] Attempt to find save path case-insensitively --- legendary/core.py | 9 +++++-- legendary/utils/wine_helpers.py | 47 ++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/legendary/core.py b/legendary/core.py index 6b78ae3..4baf0b1 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -37,7 +37,7 @@ from legendary.utils.game_workarounds import is_opt_enabled, update_workarounds from legendary.utils.savegame_helper import SaveGameHelper from legendary.utils.selective_dl import games as sdl_games from legendary.utils.manifests import combine_manifests -from legendary.utils.wine_helpers import read_registry, get_shell_folders +from legendary.utils.wine_helpers import read_registry, get_shell_folders, case_insensitive_path_search # ToDo: instead of true/false return values for success/failure actually raise an exception that the CLI/GUI @@ -696,7 +696,12 @@ class LegendaryCore: # these paths should always use a forward slash new_save_path = [path_vars.get(p.lower(), p) for p in save_path.split('/')] - return os.path.realpath(os.path.join(*new_save_path)) + absolute_path = os.path.realpath(os.path.join(*new_save_path)) + # attempt to resolve as much as possible on case-insensitive file-systems + if os.name != 'nt': + absolute_path = case_insensitive_path_search(absolute_path) + + return absolute_path def check_savegame_state(self, path: str, save: SaveGameFile) -> (SaveGameStatus, (datetime, datetime)): latest = 0 diff --git a/legendary/utils/wine_helpers.py b/legendary/utils/wine_helpers.py index 23c54ab..550ddf8 100644 --- a/legendary/utils/wine_helpers.py +++ b/legendary/utils/wine_helpers.py @@ -1,5 +1,8 @@ -import os import configparser +import logging +import os + +logger = logging.getLogger('WineHelpers') def read_registry(wine_pfx): @@ -15,3 +18,45 @@ def get_shell_folders(registry, wine_pfx): path_cleaned = v.strip('"').strip().replace('\\\\', '/').replace('C:/', '') folders[k.strip('"').strip()] = os.path.join(wine_pfx, 'drive_c', path_cleaned) return folders + + +def case_insensitive_path_search(path): + """ + Attempts to find a path case-insensitively + """ + # Legendary's save path resolver always returns absolute paths, so this is not as horrible as it looks + path_parts = path.replace('\\', '/').split('/') + path_parts[0] = '/' + # filter out empty parts + path_parts = [i for i in path_parts if i] + + # attempt to find lowest level directory that exists case-sensitively + longest_path = '' + remaining_parts = [] + for i in range(len(path_parts), 0, -1): + if os.path.exists(os.path.join(*path_parts[:i])): + longest_path = path_parts[:i] + remaining_parts = path_parts[i:] + break + logger.debug(f'Longest valid path: {longest_path}') + logger.debug(f'Remaining parts: {remaining_parts}') + + # Iterate over remaining parts, find matching directories case-insensitively + still_remaining = [] + for idx, part in enumerate(remaining_parts): + for item in os.listdir(os.path.join(*longest_path)): + if not os.path.isdir(os.path.join(*longest_path, item)): + continue + if item.lower() == part.lower(): + longest_path.append(item) + break + else: + # once we stop finding parts break + still_remaining = remaining_parts[idx-1:] + break + + logger.debug(f'New longest path: {longest_path}') + logger.debug(f'Still unresolved: {still_remaining}') + final_path = os.path.join(*longest_path, *still_remaining) + logger.debug('Final path:', final_path) + return os.path.realpath(final_path)