[cli/core] Add "clean-saves" command to remove obsolete/broken cloud save data

This commit is contained in:
derrod 2021-10-21 13:26:36 +02:00
parent 85a275950d
commit 355b1107e6
2 changed files with 79 additions and 2 deletions

View file

@ -403,6 +403,13 @@ class LegendaryCLI:
logger.info(f'Downloading saves to "{self.core.get_default_install_dir()}"')
self.core.download_saves(self._resolve_aliases(args.app_name))
def clean_saves(self, args):
if not self.core.login():
logger.error('Login failed! Cannot continue with download process.')
exit(1)
logger.info(f'Cleaning saves...')
self.core.clean_saves(self._resolve_aliases(args.app_name))
def sync_saves(self, args):
if not self.core.login():
logger.error('Login failed! Cannot continue with download process.')
@ -1727,6 +1734,7 @@ def main():
list_files_parser = subparsers.add_parser('list-files', help='List files in manifest')
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')
clean_saves_parser = subparsers.add_parser('clean-saves', help='Clean 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')
import_parser = subparsers.add_parser('import-game', help='Import an already installed game')
@ -1745,6 +1753,8 @@ def main():
help='Name of the app (optional)')
download_saves_parser.add_argument('app_name', nargs='?', metavar='<App Name>', default='',
help='Name of the app (optional)')
clean_saves_parser.add_argument('app_name', nargs='?', metavar='<App Name>', default='',
help='Name of the app (optional)')
sync_saves_parser.add_argument('app_name', nargs='?', metavar='<App Name>', default='',
help='Name of the app (optional)')
verify_parser.add_argument('app_name', help='Name of the app', metavar='<App Name>')
@ -1979,8 +1989,8 @@ def main():
if args.subparser_name not in ('auth', 'list-games', 'list-installed', 'list-files',
'launch', 'download', 'uninstall', 'install', 'update',
'repair', 'list-saves', 'download-saves', 'sync-saves',
'verify-game', 'import-game', 'egl-sync', 'status',
'info', 'alias', 'cleanup'):
'clean-saves', 'verify-game', 'import-game', 'egl-sync',
'status', 'info', 'alias', 'cleanup'):
print(parser.format_help())
# Print the main help *and* the help for all of the subcommands. Thanks stackoverflow!
@ -2031,6 +2041,8 @@ def main():
cli.download_saves(args)
elif args.subparser_name == 'sync-saves':
cli.sync_saves(args)
elif args.subparser_name == 'clean-saves':
cli.clean_saves(args)
elif args.subparser_name == 'verify-game':
cli.verify_game(args)
elif args.subparser_name == 'import-game':

View file

@ -849,6 +849,71 @@ class LegendaryCore:
self.log.info('Successfully completed savegame download.')
def clean_saves(self, app_name=''):
savegames = self.egs.get_user_cloud_saves(app_name=app_name)
files = savegames['files']
deletion_list = []
used_chunks = set()
do_not_delete = set()
# check if all chunks for manifests are there
for fname, f in files.items():
if '.manifest' not in fname:
continue
app_name = fname.split('/', 3)[2]
self.log.info(f'Checking {app_name} "{fname.split("/", 2)[2]}"...')
# download manifest
r = self.egs.unauth_session.get(f['readLink'])
if r.status_code == 404:
self.log.error('Manifest is missing! Marking for deletion.')
deletion_list.append(fname)
continue
elif r.status_code != 200:
self.log.warning(f'Download failed, status code: {r.status_code}. Skipping...')
do_not_delete.add(app_name)
continue
if not r.content:
self.log.error('Manifest is empty! Marking for deletion.')
deletion_list.append(fname)
continue
m = self.load_manifest(r.content)
# check if all required chunks are present
chunk_fnames = set()
for chunk in m.chunk_data_list.elements:
cpath_p = fname.split('/', 3)[:3]
cpath_p.append(chunk.path)
cpath = '/'.join(cpath_p)
if cpath not in files:
self.log.error(f'Chunk missing, marking manifest for deletion.')
deletion_list.append(fname)
break
else:
chunk_fnames.add(cpath)
else:
used_chunks |= chunk_fnames
# check for orphaned chunks (not used in any manifests)
for fname, f in files.items():
if fname in used_chunks or '.manifest' in fname:
continue
# skip chunks where orphan status could not be reliably determined
if fname.split('/', 3)[2] in do_not_delete:
continue
self.log.debug(f'Marking orphaned chunk {fname} for deletion.')
deletion_list.append(fname)
self.log.info('Deleting unused/broken files...')
for fname in deletion_list:
self.log.debug(f'Deleting {fname}')
self.egs.delete_game_cloud_save_file(fname)
self.log.info('Successfully completed savegame cleanup.')
def is_offline_game(self, app_name: str) -> bool:
return self.lgd.config.getboolean(app_name, 'offline', fallback=False)