mirror of
https://github.com/derrod/legendary.git
synced 2024-12-22 17:55:27 +00:00
[cli/utils/models] Add "verify-game" command to check game install
This commit is contained in:
parent
1622b415ea
commit
5b2ebada78
|
@ -17,8 +17,9 @@ from sys import exit, stdout
|
||||||
from legendary import __version__, __codename__
|
from legendary import __version__, __codename__
|
||||||
from legendary.core import LegendaryCore
|
from legendary.core import LegendaryCore
|
||||||
from legendary.models.exceptions import InvalidCredentialsError
|
from legendary.models.exceptions import InvalidCredentialsError
|
||||||
from legendary.models.game import SaveGameStatus
|
from legendary.models.game import SaveGameStatus, VerifyResult
|
||||||
from legendary.utils.custom_parser import AliasedSubParsersAction
|
from legendary.utils.custom_parser import AliasedSubParsersAction
|
||||||
|
from legendary.utils.lfs import validate_files
|
||||||
|
|
||||||
# todo custom formatter for cli logger (clean info, highlighted error/warning)
|
# todo custom formatter for cli logger (clean info, highlighted error/warning)
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
@ -585,6 +586,47 @@ class LegendaryCLI:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
|
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
|
||||||
|
|
||||||
|
def verify_game(self, args):
|
||||||
|
if not self.core.is_installed(args.app_name):
|
||||||
|
logger.error(f'Game "{args.app_name}" is not installed')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f'Loading installed manifest for "{args.app_name}"')
|
||||||
|
igame = self.core.get_installed_game(args.app_name)
|
||||||
|
manifest_data, _ = self.core.get_installed_manifest(args.app_name)
|
||||||
|
manifest = self.core.load_manfiest(manifest_data)
|
||||||
|
|
||||||
|
files = sorted(manifest.file_manifest_list.elements,
|
||||||
|
key=lambda a: a.filename.lower())
|
||||||
|
|
||||||
|
# build list of hashes
|
||||||
|
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
|
||||||
|
total = len(file_list)
|
||||||
|
num = 0
|
||||||
|
failed = []
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
for result, path in validate_files(igame.install_path, file_list):
|
||||||
|
stdout.write(f'Verification progress: {num}/{total} ({num * 100 / total:.01f}%)\t\r')
|
||||||
|
stdout.flush()
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
if result == VerifyResult.HASH_MATCH:
|
||||||
|
continue
|
||||||
|
elif result == VerifyResult.HASH_MISMATCH:
|
||||||
|
logger.error(f'File does not match hash: "{path}"')
|
||||||
|
failed.append(path)
|
||||||
|
elif result == VerifyResult.FILE_MISSING:
|
||||||
|
logger.error(f'File is missing: "{path}"')
|
||||||
|
missing.append(path)
|
||||||
|
|
||||||
|
stdout.write(f'Verification progress: {num}/{total} ({num * 100 / total:.01f}%)\t\n')
|
||||||
|
|
||||||
|
if not missing and not failed:
|
||||||
|
logger.info('Verification finished successfully.')
|
||||||
|
else:
|
||||||
|
logger.fatal(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=f'Legendary v{__version__} - "{__codename__}"')
|
parser = argparse.ArgumentParser(description=f'Legendary v{__version__} - "{__codename__}"')
|
||||||
|
@ -611,6 +653,7 @@ def main():
|
||||||
list_saves_parser = subparsers.add_parser('list-saves', help='List available cloud saves')
|
list_saves_parser = subparsers.add_parser('list-saves', help='List available cloud saves')
|
||||||
download_saves_parser = subparsers.add_parser('download-saves', help='Download all cloud saves')
|
download_saves_parser = subparsers.add_parser('download-saves', help='Download all cloud saves')
|
||||||
sync_saves_parser = subparsers.add_parser('sync-saves', help='Sync cloud saves')
|
sync_saves_parser = subparsers.add_parser('sync-saves', help='Sync cloud saves')
|
||||||
|
verify_parser = subparsers.add_parser('verify-game', help='Verify a game\'s local files')
|
||||||
|
|
||||||
install_parser.add_argument('app_name', help='Name of the app', metavar='<App Name>')
|
install_parser.add_argument('app_name', help='Name of the app', metavar='<App Name>')
|
||||||
uninstall_parser.add_argument('app_name', help='Name of the app', metavar='<App Name>')
|
uninstall_parser.add_argument('app_name', help='Name of the app', metavar='<App Name>')
|
||||||
|
@ -623,6 +666,7 @@ def main():
|
||||||
help='Name of the app (optional)')
|
help='Name of the app (optional)')
|
||||||
sync_saves_parser.add_argument('app_name', nargs='?', metavar='<App Name>', default='',
|
sync_saves_parser.add_argument('app_name', nargs='?', metavar='<App Name>', default='',
|
||||||
help='Name of the app (optional)')
|
help='Name of the app (optional)')
|
||||||
|
verify_parser.add_argument('app_name', help='Name of the app (optional)', metavar='<App Name>')
|
||||||
|
|
||||||
# importing only works on Windows right now
|
# importing only works on Windows right now
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
|
@ -742,7 +786,7 @@ def main():
|
||||||
|
|
||||||
if args.subparser_name not in ('auth', 'list-games', 'list-installed', 'list-files',
|
if args.subparser_name not in ('auth', 'list-games', 'list-installed', 'list-files',
|
||||||
'launch', 'download', 'uninstall', 'install', 'update',
|
'launch', 'download', 'uninstall', 'install', 'update',
|
||||||
'list-saves', 'download-saves', 'sync-saves'):
|
'list-saves', 'download-saves', 'sync-saves', 'verify-game'):
|
||||||
print(parser.format_help())
|
print(parser.format_help())
|
||||||
|
|
||||||
# Print the main help *and* the help for all of the subcommands. Thanks stackoverflow!
|
# Print the main help *and* the help for all of the subcommands. Thanks stackoverflow!
|
||||||
|
@ -788,6 +832,8 @@ def main():
|
||||||
cli.download_saves(args)
|
cli.download_saves(args)
|
||||||
elif args.subparser_name == 'sync-saves':
|
elif args.subparser_name == 'sync-saves':
|
||||||
cli.sync_saves(args)
|
cli.sync_saves(args)
|
||||||
|
elif args.subparser_name == 'verify-game':
|
||||||
|
cli.verify_game(args)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('Command was aborted via KeyboardInterrupt, cleaning up...')
|
logger.info('Command was aborted via KeyboardInterrupt, cleaning up...')
|
||||||
|
|
||||||
|
|
|
@ -129,3 +129,8 @@ class SaveGameStatus(Enum):
|
||||||
SAME_AGE = 2
|
SAME_AGE = 2
|
||||||
NO_SAVE = 3
|
NO_SAVE = 3
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyResult(Enum):
|
||||||
|
HASH_MATCH = 0
|
||||||
|
HASH_MISMATCH = 1
|
||||||
|
FILE_MISSING = 2
|
||||||
|
|
|
@ -5,7 +5,9 @@ import shutil
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import List
|
from typing import List, Iterator
|
||||||
|
|
||||||
|
from legendary.models.game import VerifyResult
|
||||||
|
|
||||||
logger = logging.getLogger('LFS Utils')
|
logger = logging.getLogger('LFS Utils')
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ def delete_folder(path: str, recursive=True) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validate_files(base_path: str, filelist: List[tuple], hash_type='sha1') -> list:
|
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
|
Validates the files in filelist in path against the provided hashes
|
||||||
|
|
||||||
|
@ -34,24 +36,18 @@ def validate_files(base_path: str, filelist: List[tuple], hash_type='sha1') -> l
|
||||||
:return: list of files that failed hash check
|
:return: list of files that failed hash check
|
||||||
"""
|
"""
|
||||||
|
|
||||||
failed = list()
|
if not filelist:
|
||||||
|
raise ValueError('No files to validate!')
|
||||||
|
|
||||||
if not os.path.exists(base_path):
|
if not os.path.exists(base_path):
|
||||||
logger.error('Path does not exist!')
|
raise OSError('Path does not exist')
|
||||||
failed.extend(i[0] for i in filelist)
|
|
||||||
return failed
|
|
||||||
|
|
||||||
if not filelist:
|
|
||||||
logger.info('No files to validate')
|
|
||||||
return failed
|
|
||||||
|
|
||||||
for file_path, file_hash in filelist:
|
for file_path, file_hash in filelist:
|
||||||
full_path = os.path.join(base_path, file_path)
|
full_path = os.path.join(base_path, file_path)
|
||||||
logger.debug(f'Checking "{file_path}"...')
|
# logger.debug(f'Checking "{file_path}"...')
|
||||||
|
|
||||||
if not os.path.exists(full_path):
|
if not os.path.exists(full_path):
|
||||||
logger.warning(f'File "{full_path}" does not exist!')
|
yield VerifyResult.FILE_MISSING, file_path
|
||||||
failed.append(file_path)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with open(full_path, 'rb') as f:
|
with open(full_path, 'rb') as f:
|
||||||
|
@ -60,10 +56,9 @@ def validate_files(base_path: str, filelist: List[tuple], hash_type='sha1') -> l
|
||||||
real_file_hash.update(chunk)
|
real_file_hash.update(chunk)
|
||||||
|
|
||||||
if file_hash != real_file_hash.hexdigest():
|
if file_hash != real_file_hash.hexdigest():
|
||||||
logger.error(f'Hash for "{full_path}" does not match!')
|
yield VerifyResult.HASH_MISMATCH, file_path
|
||||||
failed.append(file_path)
|
else:
|
||||||
|
yield VerifyResult.HASH_MATCH, file_path
|
||||||
return failed
|
|
||||||
|
|
||||||
|
|
||||||
def clean_filename(filename):
|
def clean_filename(filename):
|
||||||
|
|
Loading…
Reference in a new issue