mirror of
				https://github.com/derrod/legendary.git
				synced 2025-11-04 10:24:49 +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)
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            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)
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            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')
 | 
			
		||||
        refresh_token = re_data['Token']
 | 
			
		||||
        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
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            self.log.error(f'Logging in failed with {e!r}, please try again.')
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def login(self, force_refresh=False) -> bool:
 | 
			
		||||
    def _login(self, lock, force_refresh=False) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Attempts logging in with existing credentials.
 | 
			
		||||
 | 
			
		||||
        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')
 | 
			
		||||
        elif self.logged_in and self.lgd.userdata['expires_at']:
 | 
			
		||||
            dt_exp = datetime.fromisoformat(self.lgd.userdata['expires_at'][:-1])
 | 
			
		||||
        elif self.logged_in and lock.data['expires_at']:
 | 
			
		||||
            dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
 | 
			
		||||
            dt_now = datetime.utcnow()
 | 
			
		||||
            td = dt_now - dt_exp
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -212,8 +215,8 @@ class LegendaryCore:
 | 
			
		|||
            except Exception as e:
 | 
			
		||||
                self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}')
 | 
			
		||||
 | 
			
		||||
        if self.lgd.userdata['expires_at'] and not force_refresh:
 | 
			
		||||
            dt_exp = datetime.fromisoformat(self.lgd.userdata['expires_at'][:-1])
 | 
			
		||||
        if lock.data['expires_at'] and not force_refresh:
 | 
			
		||||
            dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
 | 
			
		||||
            dt_now = datetime.utcnow()
 | 
			
		||||
            td = dt_now - dt_exp
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -221,7 +224,7 @@ class LegendaryCore:
 | 
			
		|||
            if dt_exp > dt_now and abs(td.total_seconds()) > 600:
 | 
			
		||||
                self.log.info('Trying to re-use existing login session...')
 | 
			
		||||
                try:
 | 
			
		||||
                    self.egs.resume_session(self.lgd.userdata)
 | 
			
		||||
                    self.egs.resume_session(lock.data)
 | 
			
		||||
                    self.logged_in = True
 | 
			
		||||
                    return True
 | 
			
		||||
                except InvalidCredentialsError as e:
 | 
			
		||||
| 
						 | 
				
			
			@ -233,7 +236,7 @@ class LegendaryCore:
 | 
			
		|||
 | 
			
		||||
        try:
 | 
			
		||||
            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:
 | 
			
		||||
            self.log.error('Stored credentials are no longer valid! Please login again.')
 | 
			
		||||
            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.')
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        self.lgd.userdata = userdata
 | 
			
		||||
        lock.data = userdata
 | 
			
		||||
        self.logged_in = 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):
 | 
			
		||||
        return not self.lgd.config.getboolean('Legendary', 'disable_update_check', fallback=False)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,12 @@ import json
 | 
			
		|||
import os
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from contextlib import contextmanager
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from time import time
 | 
			
		||||
 | 
			
		||||
from .utils import clean_filename
 | 
			
		||||
from .utils import clean_filename, LockedJSONData
 | 
			
		||||
 | 
			
		||||
from legendary.models.game import *
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILELOCK_DEBUG = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LGDLFS:
 | 
			
		||||
    def __init__(self, config_file=None):
 | 
			
		||||
        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: '
 | 
			
		||||
                                 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:
 | 
			
		||||
            self.config.read(self.config_path)
 | 
			
		||||
| 
						 | 
				
			
			@ -130,31 +139,35 @@ class LGDLFS:
 | 
			
		|||
            except Exception as e:
 | 
			
		||||
                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
 | 
			
		||||
    def userdata(self):
 | 
			
		||||
        if self._user_data is not None:
 | 
			
		||||
            return self._user_data
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self._user_data = json.load(open(os.path.join(self.path, 'user.json')))
 | 
			
		||||
            return self._user_data
 | 
			
		||||
            with self.userdata_lock as locked:
 | 
			
		||||
                return locked.data
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            self.log.debug(f'Failed to load user data: {e!r}')
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    @userdata.setter
 | 
			
		||||
    def userdata(self, userdata):
 | 
			
		||||
        if userdata is None:
 | 
			
		||||
            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)
 | 
			
		||||
        raise NotImplementedError('The setter has been removed, use the locked userdata instead.')
 | 
			
		||||
 | 
			
		||||
    def invalidate_userdata(self):
 | 
			
		||||
        self._user_data = None
 | 
			
		||||
        if os.path.exists(os.path.join(self.path, 'user.json')):
 | 
			
		||||
            os.remove(os.path.join(self.path, 'user.json'))
 | 
			
		||||
        with self.userdata_lock as lock:
 | 
			
		||||
            lock.clear()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def entitlements(self):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
import hashlib
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +11,8 @@ from sys import stdout
 | 
			
		|||
from time import perf_counter
 | 
			
		||||
from typing import List, Iterator
 | 
			
		||||
 | 
			
		||||
from filelock import FileLock
 | 
			
		||||
 | 
			
		||||
from legendary.models.game import VerifyResult
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('LFS Utils')
 | 
			
		||||
| 
						 | 
				
			
			@ -153,3 +156,45 @@ def clean_filename(filename):
 | 
			
		|||
 | 
			
		||||
def get_dir_size(path):
 | 
			
		||||
    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
 | 
			
		||||
filelock
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue