mirror of
https://github.com/derrod/legendary.git
synced 2024-12-22 01:45:28 +00:00
[core/lfs] Use filelock for user data
Closes #566 Co-authored-by: Mathis Dröge <mathis.droege@ewe.net>
This commit is contained in:
parent
bdd53fb8f8
commit
e26b9e60ff
|
@ -131,7 +131,8 @@ class LegendaryCore:
|
||||||
Handles authentication via authorization code (either retrieved manually or automatically)
|
Handles authentication via authorization code (either retrieved manually or automatically)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.lgd.userdata = self.egs.start_session(authorization_code=code)
|
with self.lgd.userdata_lock as lock:
|
||||||
|
lock.data = self.egs.start_session(authorization_code=code)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f'Logging in failed with {e!r}, please try again.')
|
self.log.error(f'Logging in failed with {e!r}, please try again.')
|
||||||
|
@ -142,7 +143,8 @@ class LegendaryCore:
|
||||||
Handles authentication via exchange token (either retrieved manually or automatically)
|
Handles authentication via exchange token (either retrieved manually or automatically)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.lgd.userdata = self.egs.start_session(exchange_token=code)
|
with self.lgd.userdata_lock as lock:
|
||||||
|
lock.data = self.egs.start_session(exchange_token=code)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f'Logging in failed with {e!r}, please try again.')
|
self.log.error(f'Logging in failed with {e!r}, please try again.')
|
||||||
|
@ -171,22 +173,23 @@ class LegendaryCore:
|
||||||
raise ValueError('No login session in config')
|
raise ValueError('No login session in config')
|
||||||
refresh_token = re_data['Token']
|
refresh_token = re_data['Token']
|
||||||
try:
|
try:
|
||||||
self.lgd.userdata = self.egs.start_session(refresh_token=refresh_token)
|
with self.lgd.userdata_lock as lock:
|
||||||
|
lock.data = self.egs.start_session(refresh_token=refresh_token)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f'Logging in failed with {e!r}, please try again.')
|
self.log.error(f'Logging in failed with {e!r}, please try again.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def login(self, force_refresh=False) -> bool:
|
def _login(self, lock, force_refresh=False) -> bool:
|
||||||
"""
|
"""
|
||||||
Attempts logging in with existing credentials.
|
Attempts logging in with existing credentials.
|
||||||
|
|
||||||
raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error
|
raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error
|
||||||
"""
|
"""
|
||||||
if not self.lgd.userdata:
|
if not lock.data:
|
||||||
raise ValueError('No saved credentials')
|
raise ValueError('No saved credentials')
|
||||||
elif self.logged_in and self.lgd.userdata['expires_at']:
|
elif self.logged_in and lock.data['expires_at']:
|
||||||
dt_exp = datetime.fromisoformat(self.lgd.userdata['expires_at'][:-1])
|
dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
|
||||||
dt_now = datetime.utcnow()
|
dt_now = datetime.utcnow()
|
||||||
td = dt_now - dt_exp
|
td = dt_now - dt_exp
|
||||||
|
|
||||||
|
@ -212,8 +215,8 @@ class LegendaryCore:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}')
|
self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}')
|
||||||
|
|
||||||
if self.lgd.userdata['expires_at'] and not force_refresh:
|
if lock.data['expires_at'] and not force_refresh:
|
||||||
dt_exp = datetime.fromisoformat(self.lgd.userdata['expires_at'][:-1])
|
dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
|
||||||
dt_now = datetime.utcnow()
|
dt_now = datetime.utcnow()
|
||||||
td = dt_now - dt_exp
|
td = dt_now - dt_exp
|
||||||
|
|
||||||
|
@ -221,7 +224,7 @@ class LegendaryCore:
|
||||||
if dt_exp > dt_now and abs(td.total_seconds()) > 600:
|
if dt_exp > dt_now and abs(td.total_seconds()) > 600:
|
||||||
self.log.info('Trying to re-use existing login session...')
|
self.log.info('Trying to re-use existing login session...')
|
||||||
try:
|
try:
|
||||||
self.egs.resume_session(self.lgd.userdata)
|
self.egs.resume_session(lock.data)
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
return True
|
return True
|
||||||
except InvalidCredentialsError as e:
|
except InvalidCredentialsError as e:
|
||||||
|
@ -233,7 +236,7 @@ class LegendaryCore:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.log.info('Logging in...')
|
self.log.info('Logging in...')
|
||||||
userdata = self.egs.start_session(self.lgd.userdata['refresh_token'])
|
userdata = self.egs.start_session(lock.data['refresh_token'])
|
||||||
except InvalidCredentialsError:
|
except InvalidCredentialsError:
|
||||||
self.log.error('Stored credentials are no longer valid! Please login again.')
|
self.log.error('Stored credentials are no longer valid! Please login again.')
|
||||||
self.lgd.invalidate_userdata()
|
self.lgd.invalidate_userdata()
|
||||||
|
@ -242,10 +245,14 @@ class LegendaryCore:
|
||||||
self.log.error(f'HTTP request for login failed: {e!r}, please try again later.')
|
self.log.error(f'HTTP request for login failed: {e!r}, please try again later.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.lgd.userdata = userdata
|
lock.data = userdata
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def login(self, force_refresh=False) -> bool:
|
||||||
|
with self.lgd.userdata_lock as lock:
|
||||||
|
return self._login(lock, force_refresh=force_refresh)
|
||||||
|
|
||||||
def update_check_enabled(self):
|
def update_check_enabled(self):
|
||||||
return not self.lgd.config.getboolean('Legendary', 'disable_update_check', fallback=False)
|
return not self.lgd.config.getboolean('Legendary', 'disable_update_check', fallback=False)
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ import json
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .utils import clean_filename
|
from .utils import clean_filename, LockedJSONData
|
||||||
|
|
||||||
from legendary.models.game import *
|
from legendary.models.game import *
|
||||||
from legendary.utils.aliasing import generate_aliases
|
from legendary.utils.aliasing import generate_aliases
|
||||||
|
@ -16,6 +17,9 @@ from legendary.models.config import LGDConf
|
||||||
from legendary.utils.env import is_windows_mac_or_pyi
|
from legendary.utils.env import is_windows_mac_or_pyi
|
||||||
|
|
||||||
|
|
||||||
|
FILELOCK_DEBUG = False
|
||||||
|
|
||||||
|
|
||||||
class LGDLFS:
|
class LGDLFS:
|
||||||
def __init__(self, config_file=None):
|
def __init__(self, config_file=None):
|
||||||
self.log = logging.getLogger('LGDLFS')
|
self.log = logging.getLogger('LGDLFS')
|
||||||
|
@ -84,6 +88,11 @@ class LGDLFS:
|
||||||
self.log.warning(f'Removing "{os.path.join(self.path, "manifests", "old")}" folder failed: '
|
self.log.warning(f'Removing "{os.path.join(self.path, "manifests", "old")}" folder failed: '
|
||||||
f'{e!r}, please remove manually')
|
f'{e!r}, please remove manually')
|
||||||
|
|
||||||
|
if not FILELOCK_DEBUG:
|
||||||
|
# Prevent filelock logger from spamming Legendary debug output
|
||||||
|
filelock_logger = logging.getLogger('filelock')
|
||||||
|
filelock_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# try loading config
|
# try loading config
|
||||||
try:
|
try:
|
||||||
self.config.read(self.config_path)
|
self.config.read(self.config_path)
|
||||||
|
@ -130,31 +139,35 @@ class LGDLFS:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f'Loading aliases failed with {e!r}')
|
self.log.debug(f'Loading aliases failed with {e!r}')
|
||||||
|
|
||||||
|
@property
|
||||||
|
@contextmanager
|
||||||
|
def userdata_lock(self) -> LockedJSONData:
|
||||||
|
"""Wrapper around the lock to automatically update user data when it is released"""
|
||||||
|
with LockedJSONData(os.path.join(self.path, 'user.json')) as lock:
|
||||||
|
try:
|
||||||
|
yield lock
|
||||||
|
finally:
|
||||||
|
self._user_data = lock.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def userdata(self):
|
def userdata(self):
|
||||||
if self._user_data is not None:
|
if self._user_data is not None:
|
||||||
return self._user_data
|
return self._user_data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._user_data = json.load(open(os.path.join(self.path, 'user.json')))
|
with self.userdata_lock as locked:
|
||||||
return self._user_data
|
return locked.data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f'Failed to load user data: {e!r}')
|
self.log.debug(f'Failed to load user data: {e!r}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@userdata.setter
|
@userdata.setter
|
||||||
def userdata(self, userdata):
|
def userdata(self, userdata):
|
||||||
if userdata is None:
|
raise NotImplementedError('The setter has been removed, use the locked userdata instead.')
|
||||||
raise ValueError('Userdata is none!')
|
|
||||||
|
|
||||||
self._user_data = userdata
|
|
||||||
json.dump(userdata, open(os.path.join(self.path, 'user.json'), 'w'),
|
|
||||||
indent=2, sort_keys=True)
|
|
||||||
|
|
||||||
def invalidate_userdata(self):
|
def invalidate_userdata(self):
|
||||||
self._user_data = None
|
with self.userdata_lock as lock:
|
||||||
if os.path.exists(os.path.join(self.path, 'user.json')):
|
lock.clear()
|
||||||
os.remove(os.path.join(self.path, 'user.json'))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entitlements(self):
|
def entitlements(self):
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -10,6 +11,8 @@ from sys import stdout
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from typing import List, Iterator
|
from typing import List, Iterator
|
||||||
|
|
||||||
|
from filelock import FileLock
|
||||||
|
|
||||||
from legendary.models.game import VerifyResult
|
from legendary.models.game import VerifyResult
|
||||||
|
|
||||||
logger = logging.getLogger('LFS Utils')
|
logger = logging.getLogger('LFS Utils')
|
||||||
|
@ -153,3 +156,45 @@ def clean_filename(filename):
|
||||||
|
|
||||||
def get_dir_size(path):
|
def get_dir_size(path):
|
||||||
return sum(f.stat().st_size for f in Path(path).glob('**/*') if f.is_file())
|
return sum(f.stat().st_size for f in Path(path).glob('**/*') if f.is_file())
|
||||||
|
|
||||||
|
|
||||||
|
class LockedJSONData(FileLock):
|
||||||
|
def __init__(self, file_path: str):
|
||||||
|
super().__init__(file_path + '.lock')
|
||||||
|
|
||||||
|
self._file_path = file_path
|
||||||
|
self._data = None
|
||||||
|
self._initial_data = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
super().__enter__()
|
||||||
|
|
||||||
|
if os.path.exists(self._file_path):
|
||||||
|
with open(self._file_path, 'r', encoding='utf-8') as f:
|
||||||
|
self._data = json.load(f)
|
||||||
|
self._initial_data = self._data
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
super().__exit__(exc_type, exc_val, exc_tb)
|
||||||
|
|
||||||
|
if self._data != self._initial_data:
|
||||||
|
if self._data is not None:
|
||||||
|
with open(self._file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._data, f, indent=2, sort_keys=True)
|
||||||
|
else:
|
||||||
|
if os.path.exists(self._file_path):
|
||||||
|
os.remove(self._file_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@data.setter
|
||||||
|
def data(self, new_data):
|
||||||
|
if new_data is None:
|
||||||
|
raise ValueError('Invalid new data, use clear() explicitly to reset file data')
|
||||||
|
self._data = new_data
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._data = None
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
requests<3.0
|
requests<3.0
|
||||||
|
filelock
|
||||||
|
|
Loading…
Reference in a new issue