diff --git a/bauh/api/abstract/cache.py b/bauh/api/abstract/cache.py deleted file mode 100644 index c0bcabcf..00000000 --- a/bauh/api/abstract/cache.py +++ /dev/null @@ -1,50 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Set, Optional - - -class MemoryCache(ABC): - """ - Represents a memory cache. - """ - - @abstractmethod - def is_enabled(self): - pass - - @abstractmethod - def add(self, key: str, val: object): - pass - - @abstractmethod - def add_non_existing(self, key: str, val: object): - pass - - @abstractmethod - def get(self, key: str): - pass - - @abstractmethod - def delete(self, key): - pass - - @abstractmethod - def keys(self) -> Set[str]: - pass - - @abstractmethod - def clean_expired(self): - pass - - -class MemoryCacheFactory(ABC): - """ - Instantiate new memory cache instances. - """ - - @abstractmethod - def new(self, expiration: Optional[int]) -> MemoryCache: - """ - :param expiration: expiration time for the cache keys in seconds. Use -1 to disable this feature. - :return: - """ - pass diff --git a/bauh/api/abstract/context.py b/bauh/api/abstract/context.py deleted file mode 100644 index 3fe3f901..00000000 --- a/bauh/api/abstract/context.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging -import sys -from typing import Optional, Dict - -from bauh.api.abstract.cache import MemoryCacheFactory -from bauh.api.abstract.disk import DiskCacheLoaderFactory -from bauh.api.abstract.download import FileDownloader -from bauh.api.http import HttpClient -from bauh.commons.internet import InternetChecker -from bauh.view.util.translation import I18n - - -class ApplicationContext: - - def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n, - cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory, - logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str, - app_version: str, internet_checker: InternetChecker, root_user: bool, screen_width: int = -1, - screen_height: int = -1, suggestions_mapping: Optional[Dict[str, str]] = None): - """ - :param download_icons: if packages icons should be downloaded - :param http_client: a shared instance of http client - :param app_root_dir: GUI root dir - :param i18n: the translation keys - :param cache_factory: - :param disk_loader_factory: - :param logger: a logger instance - :param file_downloader - :param distro - :param app_name - :param app_version - :param internet_checker - :param screen_width - :param screen_height - :param suggestions_mapping - """ - self.download_icons = download_icons - self.http_client = http_client - self.app_root_dir = app_root_dir - self.i18n = i18n - self.cache_factory = cache_factory - self.disk_loader_factory = disk_loader_factory - self.logger = logger - self.file_downloader = file_downloader - self.arch_x86_64 = sys.maxsize > 2**32 - self.distro = distro - self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game', - 'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility') - self.app_name = app_name - self.app_version = app_version - self.root_user = root_user - self.root_password = None - self.internet_checker = internet_checker - self.screen_width = screen_width - self.screen_height = screen_height - self._suggestions_mapping = suggestions_mapping - - def is_system_x86_64(self): - return self.arch_x86_64 - - def get_view_path(self): - return self.app_root_dir + '/view' - - def is_internet_available(self) -> bool: - return self.internet_checker.is_available() - - def get_suggestion_url(self, module: str, default: Optional[str] = None) -> Optional[str]: - if self._suggestions_mapping: - module_split = module.split(f'{self.app_name}.gems.') - - if len(module_split) > 1: - gem_name = module_split[1].split('.')[0] - return self._suggestions_mapping.get(gem_name, default) - - return default diff --git a/bauh/api/http.py b/bauh/api/http.py deleted file mode 100644 index f325acb0..00000000 --- a/bauh/api/http.py +++ /dev/null @@ -1,121 +0,0 @@ -import logging -import time -import traceback -from typing import Optional - -import requests -import yaml - -from bauh.commons import system -from bauh.commons.view_utils import get_human_size_str - - -class HttpClient: - - def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = 30, sleep: float = 0.5): - self.max_attempts = max_attempts - self.session = requests.Session() - self.timeout = timeout - self.sleep = sleep - self.logger = logger - - def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False, - session: bool = True, stream: bool = False) -> Optional[requests.Response]: - cur_attempts = 1 - - while cur_attempts <= self.max_attempts: - cur_attempts += 1 - - try: - args = {'timeout': self.timeout, 'allow_redirects': allow_redirects, 'stream': stream} - - if params: - args['params'] = params - - if headers: - args['headers'] = headers - - if ignore_ssl: - args['verify'] = False - - if session: - res = self.session.get(url, **args) - else: - res = requests.get(url, **args) - - if 200 <= res.status_code < 300: - return res - - if single_call: - return res - - if self.sleep > 0: - time.sleep(self.sleep) - except Exception as e: - if isinstance(e, requests.exceptions.ConnectionError): - self.logger.error('Internet seems to be off') - raise e - elif isinstance(e, requests.exceptions.TooManyRedirects): - self.logger.warning(f"Too many redirects for GET -> {url}") - raise e - elif e.__class__ in (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema): - self.logger.warning(f"The URL '{url}' has an invalid schema") - raise e - - self.logger.error(f"Could not retrieve data from '{url}'") - traceback.print_exc() - continue - - self.logger.warning(f"Could not retrieve data from '{url}'") - - def get_json(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True): - res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session) - return res.json() if res else None - - def get_yaml(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True): - res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session) - return yaml.safe_load(res.text) if res else None - - def get_content_length_in_bytes(self, url: str, session: bool = True) -> Optional[int]: - if not url: - return - - params = {'url': url, 'allow_redirects': True, 'stream': True} - - try: - if session: - res = self.session.get(**params) - else: - res = requests.get(**params) - except requests.exceptions.ConnectionError: - self.logger.info(f"Internet seems to be off. Could not reach '{url}'") - return - - if res.status_code == 200: - size = res.headers.get('Content-Length') - - if size: - try: - return int(size) - except Exception: - pass - - def get_content_length(self, url: str, session: bool = True) -> Optional[str]: - size = self.get_content_length_in_bytes(url, session) - - if size: - return get_human_size_str(size) - - def exists(self, url: str, session: bool = True, timeout: int = 5) -> bool: - params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': timeout} - - try: - if session: - res = self.session.head(**params) - else: - res = self.session.get(**params) - except requests.exceptions.TooManyRedirects: - self.logger.warning(f"{url} seems to exist, but too many redirects have happened") - return True - - return res.status_code in (200, 403) diff --git a/bauh/api/paths.py b/bauh/api/paths.py deleted file mode 100644 index 8c388360..00000000 --- a/bauh/api/paths.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import shutil -from getpass import getuser -from pathlib import Path - -from bauh import __app_name__ -from bauh.api import user - - -def get_temp_dir(username: str) -> str: - return f'/tmp/{__app_name__}@{username}' - - -CACHE_DIR = f'/var/cache/{__app_name__}' if user.is_root() else f'{Path.home()}/.cache/{__app_name__}' -CONFIG_DIR = f'/etc/{__app_name__}' if user.is_root() else f'{Path.home()}/.config/{__app_name__}' -USER_THEMES_DIR = f'/usr/share/{__app_name__}/themes' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}/themes' -DESKTOP_ENTRIES_DIR = '/usr/share/applications' if user.is_root() else f'{Path.home()}/.local/share/applications' -TEMP_DIR = get_temp_dir(getuser()) -LOGS_DIR = f'{TEMP_DIR}/logs' -AUTOSTART_DIR = f'/etc/xdg/autostart' if user.is_root() else f'{Path.home()}/.config/autostart' -BINARIES_DIR = f'/usr/local/bin' if user.is_root() else f'{Path.home()}/.local/bin' -SHARED_FILES_DIR = f'/usr/local/share/{__app_name__}' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}' - - -def _migrate_dir_if_possible(old_path: str, new_path: str): - if old_path == new_path: - return - - if not os.path.exists(old_path): - return - - if os.path.exists(new_path): - return - - try: - parent = os.path.dirname(new_path) - if parent: - Path(parent).mkdir(parents=True, exist_ok=True) - - shutil.move(old_path, new_path) - except Exception: - # The migration is best-effort. If it fails, the app can still start with empty defaults. - pass - - -def migrate_legacy_paths(): - old_app_name = 'bauh' - - if old_app_name == __app_name__: - return - - if user.is_root(): - old_cache = f'/var/cache/{old_app_name}' - old_config = f'/etc/{old_app_name}' - old_shared = f'/usr/local/share/{old_app_name}' - old_temp = f'/tmp/{old_app_name}@{getuser()}' - else: - home = Path.home() - old_cache = f'{home}/.cache/{old_app_name}' - old_config = f'{home}/.config/{old_app_name}' - old_shared = f'{home}/.local/share/{old_app_name}' - old_temp = f'/tmp/{old_app_name}@{getuser()}' - - _migrate_dir_if_possible(old_cache, CACHE_DIR) - _migrate_dir_if_possible(old_config, CONFIG_DIR) - _migrate_dir_if_possible(old_shared, SHARED_FILES_DIR) - _migrate_dir_if_possible(old_temp, TEMP_DIR) - - -migrate_legacy_paths() diff --git a/bauh/api/user.py b/bauh/api/user.py deleted file mode 100644 index cd407ac2..00000000 --- a/bauh/api/user.py +++ /dev/null @@ -1,6 +0,0 @@ -import os -from typing import Optional - - -def is_root(user_id: Optional[int] = None): - return user_id == 0 if user_id is not None else os.getuid() == 0 diff --git a/bauh/commons/config.py b/bauh/commons/config.py deleted file mode 100644 index f2e6cb40..00000000 --- a/bauh/commons/config.py +++ /dev/null @@ -1,102 +0,0 @@ -import os -import traceback -from abc import abstractmethod, ABC -from pathlib import Path -from threading import Thread -from typing import Optional - -import yaml - -from bauh.api.paths import CONFIG_DIR -from bauh.commons import util - - -def read_config(file_path: str, template: dict, update_file: bool = False, update_async: bool = False) -> dict: - if not os.path.exists(file_path): - Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True) - save_config(template, file_path) - else: - with open(file_path) as f: - local_config = yaml.safe_load(f.read()) - - if local_config: - util.deep_update(template, local_config) - - if update_file: - if update_async: - Thread(target=save_config, args=(template, file_path), daemon=True).start() - else: - save_config(template, file_path) - - return template - - -def save_config(config: dict, file_path: str): - with open(file_path, 'w+') as f: - f.write(yaml.dump(config)) - - -class ConfigManager(ABC): - - @abstractmethod - def read_config(self) -> Optional[dict]: - pass - - @abstractmethod - def get_default_config(self) -> dict: - pass - - @abstractmethod - def is_config_cached(self) -> bool: - pass - - def get_config(self) -> dict: - default_config = self.get_default_config() - - if default_config: - cached_config = self.read_config() - - if cached_config: - self.merge_config(default_config, cached_config) - - return default_config - - @staticmethod - def merge_config(base_config: dict, current_config: dict): - util.deep_update(base_config, current_config) - - @abstractmethod - def save_config(self, config_obj: dict): - pass - - -class YAMLConfigManager(ConfigManager, ABC): - - def __init__(self, config_file_path: str): - self.file_path = config_file_path - - def is_config_cached(self) -> bool: - return os.path.exists(self.file_path) - - def read_config(self) -> Optional[dict]: - if self.is_config_cached(): - with open(self.file_path) as f: - local_config = yaml.safe_load(f.read()) - - if local_config is not None: - return local_config - - def save_config(self, config_obj: dict): - if config_obj: - config_dir = os.path.dirname(self.file_path) - try: - Path(config_dir).mkdir(parents=True, exist_ok=True) - except OSError: - traceback.print_exc() - return - - try: - with open(self.file_path, 'w+') as f: - f.write(yaml.dump(config_obj)) - except Exception: - traceback.print_exc() diff --git a/bauh/commons/internet.py b/bauh/commons/internet.py deleted file mode 100644 index e52f1702..00000000 --- a/bauh/commons/internet.py +++ /dev/null @@ -1,17 +0,0 @@ -import socket - - -class InternetChecker: - - def __init__(self, offline: bool): - self.offline = offline - - def is_available(self) -> bool: - if self.offline: - return False - - try: - socket.gethostbyname("w3.org") - return True - except Exception: - return False diff --git a/bauh/gems/web/resources/__init__.py b/bauh/gems/web/resources/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/bauh/gems/web/resources/img/__init__.py b/bauh/gems/web/resources/img/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/bauh/gems/web/resources/locale/__init__.py b/bauh/gems/web/resources/locale/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py deleted file mode 100755 index 4da5652b..00000000 --- a/bauh/view/core/controller.py +++ /dev/null @@ -1,702 +0,0 @@ -import shutil -import time -import traceback -from subprocess import Popen, STDOUT -from threading import Thread -from typing import List, Set, Type, Tuple, Dict, Optional, Generator, Callable - -from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ - UpgradeRequirement, TransactionResult, SoftwareAction, SettingsView, SettingsController -from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher, TaskManager -from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \ - CustomSoftwareAction -from bauh.api.abstract.view import TabGroupComponent, MessageType -from bauh.api.exception import NoInternetException -from bauh.commons.boot import CreateConfigFile -from bauh.commons.html import bold -from bauh.commons.regex import RE_URL -from bauh.commons.util import sanitize_command_input -from bauh.view.core.config import CoreConfigManager -from bauh.view.core.settings import GenericSettingsManager -from bauh.view.core.update import check_for_update -from bauh.view.util import resource -from bauh.view.util.resource import get_path -from bauh.view.util.util import clean_app_files, restart_app - - -class GenericUpgradeRequirements(UpgradeRequirements): - - def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement], - to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[SoftwarePackage], - sub_requirements: Dict[SoftwareManager, UpgradeRequirements]): - super(GenericUpgradeRequirements, self).__init__(to_install=to_install, to_upgrade=to_upgrade, - to_remove=to_remove, cannot_upgrade=cannot_upgrade) - self.sub_requirements = sub_requirements - - -class GenericSoftwareManager(SoftwareManager, SettingsController): - - def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict, - force_suggestions: bool = False): - - super(GenericSoftwareManager, self).__init__(context=context) - self.managers = managers - self.map = {t: m for m in self.managers for t in m.get_managed_types()} - self._available_cache = {} if config['system']['single_dependency_checking'] else None - self.thread_prepare = None - self.i18n = context.i18n - self.disk_loader_factory = context.disk_loader_factory - self.logger = context.logger - self._already_prepared = [] - self.working_managers = [] - self.config = config - self.settings_manager: Optional[GenericSettingsManager] = None - self.http_client = context.http_client - self.configman = CoreConfigManager() - self._action_reset: Optional[CustomSoftwareAction] = None - self._dynamic_extra_actions: Optional[Dict[CustomSoftwareAction, Callable[[dict], bool]]] = None - self.force_suggestions = force_suggestions - - @property - def dynamic_extra_actions(self) -> Dict[CustomSoftwareAction, Callable[[dict], bool]]: - if self._dynamic_extra_actions is None: - self._dynamic_extra_actions = { - CustomSoftwareAction(i18n_label_key='action.backups', - i18n_status_key='action.backups.status', - i18n_description_key='action.backups.desc', - manager_method='launch_timeshift', - manager=self, - icon_path='timeshift', - requires_root=False, - refresh=False): self.is_backups_action_available - } - - return self._dynamic_extra_actions - - @property - def action_reset(self) -> CustomSoftwareAction: - if self._action_reset is None: - self._action_reset = CustomSoftwareAction(i18n_label_key='action.reset', - i18n_status_key='action.reset.status', - i18n_description_key='action.reset.desc', - manager_method='reset', - icon_path=resource.get_path('img/logo.svg'), - requires_root=False, - manager=self, - refresh=False) - - return self._action_reset - - def _is_timeshift_launcher_available(self) -> bool: - return bool(shutil.which('timeshift-launcher')) - - def is_backups_action_available(self, app_config: dict) -> bool: - return bool(app_config['backup']['enabled']) and self._is_timeshift_launcher_available() - - def reset_cache(self): - if self._available_cache is not None: - self._available_cache = {} - self.working_managers.clear() - - def launch_timeshift(self, root_password: Optional[str], watcher: ProcessWatcher): - if self._is_timeshift_launcher_available(): - try: - Popen(['timeshift-launcher'], stderr=STDOUT) - return True - except Exception: - traceback.print_exc() - watcher.show_message(title=self.i18n["error"].capitalize(), - body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')), - type_=MessageType.ERROR) - return False - else: - watcher.show_message(title=self.i18n["error"].capitalize(), - body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')), - type_=MessageType.ERROR) - return False - - def _can_work(self, man: SoftwareManager): - if self._available_cache is not None: - available = False - for t in man.get_managed_types(): - available = self._available_cache.get(t) - - if available is None: - available = man.is_enabled() and man.can_work()[0] - self._available_cache[t] = available - - if available: - available = True - else: - available = man.is_enabled() and man.can_work()[0] - - if available: - if man not in self.working_managers: - self.working_managers.append(man) - else: - if man in self.working_managers: - self.working_managers.remove(man) - - return available - - def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult): - mti = time.time() - apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds') - - res.installed.extend(apps_found.installed) - res.new.extend(apps_found.new) - - def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = -1, is_url: bool = False) -> SearchResult: - ti = time.time() - self._wait_to_be_ready() - - res = SearchResult.empty() - - if self.context.is_internet_available(): - norm_query = sanitize_command_input(words).lower() - self.logger.info(f"Search query: {norm_query}") - - if norm_query: - is_url = bool(RE_URL.match(norm_query)) - disk_loader = self.disk_loader_factory.new() - disk_loader.start() - - threads = [] - - for man in self.managers: - if self._can_work(man): - t = Thread(target=self._search, args=(norm_query, is_url, man, disk_loader, res), daemon=True) - t.start() - threads.append(t) - - for t in threads: - t.join() - - if disk_loader: - disk_loader.stop_working() - disk_loader.join() - else: - raise NoInternetException() - - res.update_total() - tf = time.time() - self.logger.info(f'Took {tf - ti:.8f} seconds') - return res - - def _wait_to_be_ready(self): - if self.thread_prepare: - self.thread_prepare.join() - self.thread_prepare = None - - def set_enabled(self, enabled: bool): - pass - - def can_work(self) -> Tuple[bool, Optional[str]]: - return True, None - - def _get_package_lower_name(self, pkg: SoftwarePackage): - return pkg.name.lower() - - def _fill_read_installed(self, man: SoftwareManager, disk_loader: DiskCacheLoader, internet_available: bool, - output: List[SearchResult]): - mti = time.time() - man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available, - limit=-1, only_apps=False) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.4f} seconds') - output.append(man_res) - - def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: - ti = time.time() - self._wait_to_be_ready() - - res = SearchResult([], None, 0) - - disk_loader = None - - net_available = self.context.is_internet_available() - read_threads = list() - results = list() - - if not pkg_types: # any type - for man in self.managers: - if self._can_work(man): - if not disk_loader: - disk_loader = self.disk_loader_factory.new() - disk_loader.start() - - t = Thread(target=self._fill_read_installed, - args=(man, disk_loader, net_available, results), - daemon=True) - t.start() - read_threads.append(t) - else: - man_already_used = [] - - for t in pkg_types: - man = self.map.get(t) - if man and (man not in man_already_used) and self._can_work(man): - - if not disk_loader: - disk_loader = self.disk_loader_factory.new() - disk_loader.start() - - t = Thread(target=self._fill_read_installed, - args=(man, disk_loader, net_available, results), - daemon=True) - t.start() - read_threads.append(t) - - for t in read_threads: - t.join() - - if disk_loader: - disk_loader.stop_working() - disk_loader.join() - - for result in results: - if result.installed: - res.installed.extend(result.installed) - res.total += result.total - - if res.installed: - for p in res.installed: - if p.is_update_ignored(): - if p.categories is None: - p.categories = ['updates_ignored'] - elif 'updates_ignored' not in p.categories: - self._add_category(p, 'updates_ignored') - - res.installed.sort(key=self._get_package_lower_name) - - tf = time.time() - self.logger.info(f'Took {tf - ti:.2f} seconds') - return res - - def _add_category(self, pkg: SoftwarePackage, category: str): - if isinstance(pkg.categories, tuple): - pkg.categories = tuple((*pkg.categories, category)) - elif isinstance(pkg.categories, list): - pkg.categories.append(category) - elif isinstance(pkg.categories, set): - pkg.categories.add(category) - - def downgrade(self, app: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher) -> bool: - man = self._get_manager_for(app) - - if man and app.can_be_downgraded(): - mti = time.time() - res = man.downgrade(app, root_password, handler) - mtf = time.time() - self.logger.info(f'Took {mtf - mti:.2f} seconds') - return res - else: - raise Exception(f"Downgrading is not possible for {app.__class__.__name__}") - - def clean_cache_for(self, app: SoftwarePackage): - man = self._get_manager_for(app) - - if man: - return man.clean_cache_for(app) - - def upgrade(self, requirements: GenericUpgradeRequirements, root_password: Optional[str], handler: ProcessWatcher) -> bool: - for man, man_reqs in requirements.sub_requirements.items(): - res = man.upgrade(man_reqs, root_password, handler) - - if not res: - return False - - return True - - def _fill_post_transaction_status(self, pkg: SoftwarePackage, installed: bool): - pkg.installed = installed - pkg.update = False - - if pkg.latest_version: - pkg.version = pkg.latest_version - - def _update_post_transaction_status(self, res: TransactionResult): - if res.success: - if res.installed: - for p in res.installed: - self._fill_post_transaction_status(p, True) - if res.removed: - for p in res.removed: - self._fill_post_transaction_status(p, False) - - def uninstall(self, pkg: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher, disk_loader: DiskCacheLoader = None) -> TransactionResult: - man = self._get_manager_for(pkg) - - if man: - ti = time.time() - disk_loader = self.disk_loader_factory.new() - disk_loader.start() - self.logger.info(f"Uninstalling {pkg.name}") - try: - res = man.uninstall(pkg, root_password, handler, disk_loader) - disk_loader.stop_working() - disk_loader.join() - self._update_post_transaction_status(res) - return res - except Exception: - traceback.print_exc() - return TransactionResult(success=False, installed=[], removed=[]) - finally: - tf = time.time() - self.logger.info(f'Uninstallation of {pkg} took {(tf - ti) / 60:.2f} minutes') - - def install(self, app: SoftwarePackage, root_password: Optional[str], disk_loader: DiskCacheLoader, handler: ProcessWatcher) -> TransactionResult: - man = self._get_manager_for(app) - - if man: - ti = time.time() - disk_loader = self.disk_loader_factory.new() - disk_loader.start() - try: - self.logger.info(f'Installing {app}') - res = man.install(app, root_password, disk_loader, handler) - disk_loader.stop_working() - disk_loader.join() - self._update_post_transaction_status(res) - return res - except Exception: - traceback.print_exc() - return TransactionResult(success=False, installed=[], removed=[]) - finally: - tf = time.time() - self.logger.info(f'Installation of {app} took {(tf - ti) / 60:.2f} minutes') - - def get_info(self, app: SoftwarePackage): - man = self._get_manager_for(app) - - if man: - return man.get_info(app) - - def get_history(self, app: SoftwarePackage) -> PackageHistory: - man = self._get_manager_for(app) - - if man: - mti = time.time() - history = man.get_history(app) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.2f} seconds') - return history - - def get_managed_types(self) -> Set[Type[SoftwarePackage]]: - available_types = set() - - for man in self.get_working_managers(): - available_types.update(man.get_managed_types()) - - return available_types - - def is_enabled(self): - return True - - def _get_manager_for(self, app: SoftwarePackage) -> SoftwareManager: - man = self.map[app.__class__] - return man if man and self._can_work(man) else None - - def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool): - if pkg.supports_disk_cache(): - man = self._get_manager_for(pkg) - - if man: - return man.cache_to_disk(pkg, icon_bytes=icon_bytes, only_icon=only_icon) - - def requires_root(self, action: SoftwareAction, app: SoftwarePackage) -> bool: - if app is None: - if self.managers: - for man in self.managers: - if self._can_work(man): - if man.requires_root(action, app): - return True - return False - else: - man = self._get_manager_for(app) - - if man: - return man.requires_root(action, app) - - def prepare(self, task_manager: TaskManager, root_password: Optional[str], internet_available: bool): - ti = time.time() - self.logger.info("Initializing") - taskman = task_manager if task_manager else TaskManager() # empty task manager to prevent null pointers - - create_config = CreateConfigFile(taskman=taskman, configman=self.configman, i18n=self.i18n, - task_icon_path=get_path('img/logo.svg'), logger=self.logger) - create_config.start() - - if self.managers: - internet_on = self.context.is_internet_available() - prepare_tasks = [] - for man in self.managers: - if man not in self._already_prepared and self._can_work(man): - t = Thread(target=man.prepare, args=(taskman, root_password, internet_on), daemon=True) - t.start() - prepare_tasks.append(t) - self._already_prepared.append(man) - - for t in prepare_tasks: - t.join() - - create_config.join() - tf = time.time() - self.logger.info(f'Finished ({tf - ti:.2f} seconds)') - - def cache_available_managers(self): - if self.managers: - for man in self.managers: - self._can_work(man) - - def list_updates(self, internet_available: bool = None) -> List[PackageUpdate]: - self._wait_to_be_ready() - - updates = [] - - if self.managers: - net_available = self.context.is_internet_available() - - for man in self.managers: - if self._can_work(man): - man_updates = man.list_updates(internet_available=net_available) - if man_updates: - updates.extend(man_updates) - - return updates - - def list_warnings(self, internet_available: bool = None) -> List[str]: - warnings = [] - - int_available = self.context.is_internet_available() - - if int_available: - updates_msg = check_for_update(self.logger, self.http_client, self.i18n) - - if updates_msg: - warnings.append(updates_msg) - - if self.managers: - for man in self.managers: - if self._can_work(man): - man_warnings = man.list_warnings(internet_available=int_available) - - if man_warnings: - warnings.extend(man_warnings) - - return warnings - - def _fill_suggestions(self, suggestions: list, man: SoftwareManager, limit: int, filter_installed: bool): - if self._can_work(man): - mti = time.time() - man_sugs = man.list_suggestions(limit=limit, filter_installed=filter_installed) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.5f} seconds') - - if man_sugs: - if 0 < limit < len(man_sugs): - man_sugs = man_sugs[0:limit] - - suggestions.extend(man_sugs) - - def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: - if self.force_suggestions or bool(self.config['suggestions']['enabled']): - if self.managers and self.context.is_internet_available(): - suggestions, threads = [], [] - for man in self.managers: - t = Thread(target=self._fill_suggestions, - args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed), - daemon=True) - t.start() - threads.append(t) - - for t in threads: - t.join() - - if suggestions: - suggestions.sort(key=lambda s: s.priority.value, reverse=True) - - return suggestions - return [] - - def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[str], watcher: ProcessWatcher): - if action.requires_internet and not self.context.is_internet_available(): - raise NoInternetException() - - man = action.manager if action.manager else self._get_manager_for(pkg) - - if man: - return eval(f"man.{action.manager_method}({'pkg=pkg, ' if pkg else ''}root_password=root_password, watcher=watcher)") - - def is_default_enabled(self) -> bool: - return True - - def launch(self, pkg: SoftwarePackage): - self._wait_to_be_ready() - - man = self._get_manager_for(pkg) - - if man: - self.logger.info(f'Launching {pkg}') - man.launch(pkg) - - def get_screenshots(self, pkg: SoftwarePackage) -> Generator[str, None, None]: - man = self._get_manager_for(pkg) - - if man: - yield from man.get_screenshots(pkg) - - def get_working_managers(self): - return [m for m in self.managers if self._can_work(m)] - - def get_settings(self) -> Optional[Generator[SettingsView, None, None]]: - if self.settings_manager is None: - self.settings_manager = GenericSettingsManager(managers=self.managers, - working_managers=self.working_managers, - configman=self.configman, - context=self.context) - else: - self.settings_manager.managers = self.managers - self.settings_manager.working_managers = self.working_managers - - yield SettingsView(self, self.settings_manager.get_settings()) - - def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[List[str]]]: - return self.settings_manager.save_settings(component) - - def _map_pkgs_by_manager(self, pkgs: List[SoftwarePackage], pkg_filters: list = None) -> Dict[SoftwareManager, List[SoftwarePackage]]: - by_manager = {} - for pkg in pkgs: - if pkg_filters and not all((1 for f in pkg_filters if f(pkg))): - continue - - man = self._get_manager_for(pkg) - - if man: - man_pkgs = by_manager.get(man) - - if man_pkgs is None: - man_pkgs = [] - by_manager[man] = man_pkgs - - man_pkgs.append(pkg) - - return by_manager - - def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: Optional[str], watcher: ProcessWatcher) -> UpgradeRequirements: - by_manager = self._map_pkgs_by_manager(pkgs) - res = GenericUpgradeRequirements([], [], [], [], {}) - - if by_manager: - for man, pkgs in by_manager.items(): - ti = time.time() - man_reqs = man.get_upgrade_requirements(pkgs, root_password, watcher) - tf = time.time() - self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds') - - if not man_reqs: - return # it means the process should be stopped - - if man_reqs: - res.sub_requirements[man] = man_reqs - if man_reqs.to_install: - res.to_install.extend(man_reqs.to_install) - - if man_reqs.to_remove: - res.to_remove.extend(man_reqs.to_remove) - - if man_reqs.to_upgrade: - res.to_upgrade.extend(man_reqs.to_upgrade) - - if man_reqs.cannot_upgrade: - res.cannot_upgrade.extend(man_reqs.cannot_upgrade) - - return res - - def reset(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool: - body = f"
{self.i18n['action.reset.body_1'].format(bold(self.context.app_name))}
" \ - f"{self.i18n['action.reset.body_2']}
" - - if watcher.request_confirmation(title=self.i18n['action.reset'], - body=body, - confirmation_label=self.i18n['proceed'].capitalize(), - deny_label=self.i18n['cancel'].capitalize()): - - try: - clean_app_files(managers=self.managers, logs=False) - restart_app() - except Exception: - return False - - return True - - def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]: - if self.managers: - working_managers = [] - - for man in self.managers: - if self._can_work(man): - working_managers.append(man) - - if working_managers: - working_managers.sort(key=lambda m: m.__class__.__name__) - - for man in working_managers: - for action in man.gen_custom_actions(): - action.manager = man - yield action - - app_config = self.configman.get_config() - - for action, available in self.dynamic_extra_actions.items(): - if available(app_config): - yield action - - yield self.action_reset - - def _fill_sizes(self, man: SoftwareManager, pkgs: List[SoftwarePackage]): - ti = time.time() - man.fill_sizes(pkgs) - tf = time.time() - self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds') - - def fill_sizes(self, pkgs: List[SoftwarePackage]): - by_manager = self._map_pkgs_by_manager(pkgs, pkg_filters=[lambda p: p.size is None]) - - if by_manager: - threads = [] - for man, man_pkgs in by_manager.items(): - if man_pkgs: - t = Thread(target=self._fill_sizes, args=(man, man_pkgs), daemon=True) - t.start() - threads.append(t) - - for t in threads: - t.join() - - def ignore_update(self, pkg: SoftwarePackage): - manager = self._get_manager_for(pkg) - - if manager: - manager.ignore_update(pkg) - - if pkg.is_update_ignored(): - if pkg.categories is None: - pkg.categories = ['updates_ignored'] - elif 'updates_ignored' not in pkg.categories: - self._add_category(pkg, 'updates_ignored') - - def revert_ignored_update(self, pkg: SoftwarePackage): - manager = self._get_manager_for(pkg) - - if manager: - manager.revert_ignored_update(pkg) - - if not pkg.is_update_ignored() and pkg.categories and 'updates_ignored' in pkg.categories: - if isinstance(pkg.categories, tuple): - pkg.categories = tuple(c for c in pkg.categories if c != 'updates_ignored') - else: - pkg.categories.remove('updates_ignored') diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py deleted file mode 100644 index 982d12e4..00000000 --- a/bauh/view/core/downloader.py +++ /dev/null @@ -1,328 +0,0 @@ -import os -import re -import shutil -import time -import traceback -from io import StringIO, BytesIO -from logging import Logger -from math import floor -from pathlib import Path -from threading import Thread -from typing import Optional, Tuple - -from bauh.api.abstract.download import FileDownloader -from bauh.api.abstract.handler import ProcessWatcher -from bauh.api.http import HttpClient -from bauh.commons.html import bold -from bauh.commons.system import ProcessHandler, SimpleProcess -from bauh.commons.view_utils import get_human_size_str -from bauh.view.util.translation import I18n - -RE_HAS_EXTENSION = re.compile(r'.+\.\w+$') - - -class SelfFileDownloader(FileDownloader): - - def __init__(self, logger: Logger, i18n: I18n, http_client: HttpClient, - check_ssl: bool): - self._logger = logger - self._i18n = i18n - self._client = http_client - self._ssl = check_ssl - - def is_multithreaded(self) -> bool: - return False - - def can_work(self) -> bool: - return True - - def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: - return tuple() - - def is_multithreaded_client_available(self, name: str) -> bool: - return False - - def list_available_multithreaded_clients(self) -> Tuple[str, ...]: - return tuple() - - def get_supported_clients(self) -> Tuple[str, ...]: - return tuple() - - def download(self, file_url: str, watcher: Optional[ProcessWatcher], output_path: str, cwd: str, - root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, - max_threads: int = None, known_size: int = None) -> bool: - try: - res = self._client.get(url=file_url, ignore_ssl=not self._ssl, stream=True) - except Exception: - return False - - try: - content_length = int(res.headers.get("content-length", 0)) - except Exception: - content_length = 0 - self._logger.warning(f"Could not retrieve the content-length for file '{file_url}'") - - file_name = file_url.split("/")[-1] - msg = StringIO() - msg.write(f"{substatus_prefix} " if substatus_prefix else "") - msg.write(f"{self._i18n['downloading']} {bold(file_name)}") - base_msg = msg.getvalue() - - byte_stream = BytesIO() - total_downloaded = 0 - known_size = content_length and content_length > 0 - total_size_str = get_human_size_str(content_length) if known_size > 0 else "?" - - try: - for data in res.iter_content(chunk_size=1024): - byte_stream.write(data) - total_downloaded += len(data) - perc = f"({(total_downloaded / content_length) * 100:.2f}%) " if known_size > 0 else "" - watcher.change_substatus(f"{perc}{base_msg} ({get_human_size_str(total_downloaded)} / {total_size_str})") - except Exception: - self._logger.error(f"Unexpected exception while downloading file from '{file_url}'") - traceback.print_exc() - return False - - self._logger.info(f"Writing downloaded file content to disk: {output_path}") - - try: - with open(output_path, "wb+") as f: - f.write(byte_stream.getvalue()) - except Exception: - self._logger.error(f"Unexpected exception when saving downloaded content to disk: {output_path}") - traceback.print_exc() - return False - - return True - - -class AdaptableFileDownloader(FileDownloader): - - def __init__(self, logger: Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient, - multithread_client: str, check_ssl: bool): - self.logger = logger - self.multithread_enabled = multithread_enabled - self.i18n = i18n - self.http_client = http_client - self.supported_multithread_clients = ("aria2", "axel") - self.multithread_client = multithread_client - self.check_ssl = check_ssl - self._self_downloader = SelfFileDownloader(logger=logger, - i18n=i18n, - http_client=http_client, - check_ssl=check_ssl) - - @staticmethod - def is_aria2c_available() -> bool: - return bool(shutil.which('aria2c')) - - @staticmethod - def is_axel_available() -> bool: - return bool(shutil.which('axel')) - - def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess: - cmd = ['aria2c', url, - '--no-conf', - '-x', '16', - '--enable-color=false', - '--stderr=true', - '--summary-interval=0', - '--disable-ipv6', - '-k', '1M', - '--allow-overwrite=true', - '-c', - '-t', '5', - '--max-file-not-found=3', - '--file-allocation=none', - '--console-log-level=error'] - - if threads > 1: - cmd.append('-s') - cmd.append(str(threads)) - - if output_path: - output_split = output_path.split('/') - cmd.append('-d') - cmd.append('/'.join(output_split[:-1])) - cmd.append('-o') - cmd.append(output_split[-1]) - - return SimpleProcess(cmd=cmd, root_password=root_password, cwd=cwd) - - def _get_axel_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess: - cmd = ['axel', url, '-n', str(threads), '-4', '-c', '-T', '5'] - - if not self.check_ssl: - cmd.append('-k') - - if output_path: - cmd.append(f'--output={output_path}') - - return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password) - - def _rm_bad_file(self, file_name: str, output_path: str, cwd, handler: ProcessHandler, root_password: Optional[str]): - to_delete = output_path if output_path else f'{cwd}/{file_name}' - - if to_delete and os.path.exists(to_delete): - self.logger.info(f'Removing downloaded file {to_delete}') - success, _ = handler.handle_simple(SimpleProcess(['rm', '-rf', to_delete], root_password=root_password)) - return success - - def _concat_file_size(self, file_url: str, base_substatus: StringIO, watcher: ProcessWatcher): - watcher.change_substatus(f'{base_substatus.getvalue()} ( ? Mb )') - - try: - size = self.http_client.get_content_length(file_url) - - if size: - base_substatus.write(f' ( {size} )') - watcher.change_substatus(base_substatus.getvalue()) - except Exception: - pass - - def _get_appropriate_threads_number(self, max_threads: int, known_size: int) -> int: - if max_threads and max_threads > 0: - threads = max_threads - elif known_size: - threads = 16 if known_size >= 16000000 else floor(known_size / 1000000) - - if threads <= 0: - threads = 1 - else: - threads = 16 - - return threads - - def _download_with_threads(self, client: str, file_url: str, output_path: str, cwd: str, - max_threads: int, known_size: int, display_file_size: bool, handler: ProcessHandler, - root_password: Optional[str] = None, substatus_prefix: Optional[str] = None) \ - -> Tuple[float, bool]: - - threads = self._get_appropriate_threads_number(max_threads, known_size) - - if client == 'aria2': - start_time = time.time() - process = self._get_aria2c_process(file_url, output_path, cwd, root_password, threads) - downloader = 'aria2' - else: - start_time = time.time() - process = self._get_axel_process(file_url, output_path, cwd, root_password, threads) - downloader = 'axel' - - name = file_url.split('/')[-1] - - if output_path and not RE_HAS_EXTENSION.match(name) and RE_HAS_EXTENSION.match(output_path): - name = output_path.split('/')[-1] - - if handler.watcher: - msg = StringIO() - msg.write(f'{substatus_prefix} ' if substatus_prefix else '') - msg.write(f"{bold('[{}]'.format(downloader))} {self.i18n['downloading']} {bold(name)}") - - if display_file_size: - if known_size: - msg.write(f' ( {get_human_size_str(known_size)} )') - handler.watcher.change_substatus(msg.getvalue()) - else: - Thread(target=self._concat_file_size, args=(file_url, msg, handler.watcher), daemon=True).start() - else: - msg.write(' ( ? Mb )') - handler.watcher.change_substatus(msg.getvalue()) - - success, _ = handler.handle_simple(process) - return start_time, success - - def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = None, cwd: str = None, root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool: - self.logger.info(f'Downloading {file_url}') - handler = ProcessHandler(watcher) - file_name = file_url.split('/')[-1] - - final_cwd = cwd if cwd else '.' - - success = False - start_time = time.time() - try: - if output_path: - if os.path.exists(output_path): - self.logger.info(f'Removing old file found before downloading: {output_path}') - os.remove(output_path) - self.logger.info(f'Old file {output_path} removed') - else: - output_dir = os.path.dirname(output_path) - - try: - Path(output_dir).mkdir(exist_ok=True, parents=True) - except OSError: - self.logger.error(f"Could not make download directory '{output_dir}'") - watcher.print(self.i18n['error.mkdir'].format(dir=output_dir)) - return False - - threaded_client = self.get_available_multithreaded_tool() - if threaded_client: - start_time, success = self._download_with_threads(client=threaded_client, file_url=file_url, - output_path=output_path, - cwd=final_cwd, max_threads=max_threads, - known_size=known_size, handler=handler, - display_file_size=display_file_size, - root_password=root_password) - else: - start_time = time.time() - success = self._self_downloader.download(file_url=file_url, watcher=watcher, output_path=output_path, - cwd=cwd, root_password=root_password, - substatus_prefix=substatus_prefix, - display_file_size=display_file_size, max_threads=max_threads, - known_size=known_size) - except Exception: - traceback.print_exc() - self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password) - - final_time = time.time() - self.logger.info(f'{file_name} download took {(final_time - start_time) / 60:.4f} minutes') - - if not success: - self.logger.error(f"Could not download '{file_name}'") - self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password) - - return success - - def is_multithreaded(self) -> bool: - return bool(self.get_available_multithreaded_tool()) - - def get_available_multithreaded_tool(self) -> str: - if self.multithread_enabled: - if self.multithread_client is None or self.multithread_client not in self.supported_multithread_clients: - for client in self.supported_multithread_clients: - if self.is_multithreaded_client_available(client): - return client - else: - possible_clients = {*self.supported_multithread_clients} - - if self.is_multithreaded_client_available(self.multithread_client): - return self.multithread_client - else: - possible_clients.remove(self.multithread_client) - - for client in possible_clients: - if self.is_multithreaded_client_available(client): - return client - - def can_work(self) -> bool: - return True - - def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: - return self.supported_multithread_clients - - def is_multithreaded_client_available(self, name: str) -> bool: - if name == 'aria2': - return self.is_aria2c_available() - elif name == 'axel': - return self.is_axel_available() - else: - return False - - def list_available_multithreaded_clients(self) -> Tuple[str, ...]: - return tuple(c for c in self.supported_multithread_clients if self.is_multithreaded_client_available(c)) - - def get_supported_clients(self) -> Tuple[str, ...]: - return "self", "aria2", "axel" diff --git a/bauh/view/core/suggestions.py b/bauh/view/core/suggestions.py deleted file mode 100644 index 280f3a88..00000000 --- a/bauh/view/core/suggestions.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -from typing import Optional, Dict -from bauh import __app_name__ - - -def read_suggestions_mapping() -> Optional[Dict[str, str]]: - file_path = f'/etc/{__app_name__}/suggestions.conf' - - if os.path.isfile(file_path): - try: - with open(file_path) as f: - file_content = f.read() - except FileNotFoundError: - return - - if not file_content: - return - - mapping = {} - for line in file_content.split('\n'): - line_strip = line.strip() - - if not line_strip.startswith('#'): - gem_file = line_strip.split('=') - - if len(gem_file) == 2: - gem_name, file_url = gem_file[0].strip(), gem_file[1].strip() - - if gem_name and file_url: - mapping[gem_name] = file_url - - return mapping if mapping else None diff --git a/bauh/view/qt/__init__.py b/bauh/view/qt/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py deleted file mode 100644 index fc80fd83..00000000 --- a/bauh/view/qt/prepare.py +++ /dev/null @@ -1,453 +0,0 @@ -import datetime -import operator -import time -from functools import reduce -from typing import Tuple, Optional - -from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication, QMutex -from PyQt5.QtGui import QIcon, QCursor, QCloseEvent, QShowEvent -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, \ - QProgressBar, QPlainTextEdit, QToolButton, QHBoxLayout - -from bauh import __app_name__ -from bauh.api.abstract.context import ApplicationContext -from bauh.api.abstract.controller import SoftwareManager, SoftwareAction -from bauh.api.abstract.handler import TaskManager -from bauh.api import user -from bauh.view.qt.components import new_spacer, QCustomToolbar -from bauh.view.qt.qt_utils import centralize, get_current_screen_geometry -from bauh.view.qt.root import RootDialog -from bauh.view.qt.thread import AnimateProgress -from bauh.view.util.translation import I18n - - -class Prepare(QThread, TaskManager): - signal_register = pyqtSignal(str, str, object) - signal_update = pyqtSignal(str, float, str) - signal_finished = pyqtSignal(str) - signal_started = pyqtSignal(int) - signal_ask_password = pyqtSignal() - signal_output = pyqtSignal(str, str) - - def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n): - super(Prepare, self).__init__() - self.manager = manager - self.i18n = i18n - self.context = context - self.waiting_password = False - self.password_response = None - self._tasks_added = set() - self._tasks_finished = set() - self._add_lock = QMutex() - self._finish_lock = QMutex() - - def ask_password(self) -> Tuple[bool, Optional[str]]: - self.waiting_password = True - self.signal_ask_password.emit() - - while self.waiting_password: - self.msleep(100) # waiting for user input - - return self.password_response - - def set_password_reply(self, valid: bool, password: str): - self.password_response = valid, password - self.waiting_password = False - - def run(self): - root_pwd = None - if not user.is_root() and self.manager.requires_root(SoftwareAction.PREPARE, None): - ok, root_pwd = self.ask_password() - - if not ok: - QCoreApplication.exit(1) - - self.manager.prepare(self, root_pwd, None) - self.signal_started.emit(len(self._tasks_added)) - - def update_progress(self, task_id: str, progress: float, substatus: str): - self._add_lock.lock() - if task_id in self._tasks_added: - self.signal_update.emit(task_id, progress, substatus) - self._add_lock.unlock() - - def update_output(self, task_id: str, output: str): - self._add_lock.lock() - if task_id in self._tasks_added: - self.signal_output.emit(task_id, output) - self._add_lock.unlock() - - def register_task(self, id_: str, label: str, icon_path: str): - self._add_lock.lock() - - if id_ not in self._tasks_added: - self._tasks_added.add(id_) - self.signal_register.emit(id_, label, icon_path) - - self._add_lock.unlock() - - def finish_task(self, task_id: str): - self._add_lock.lock() - task_registered = task_id in self._tasks_added - self._add_lock.unlock() - - if not task_registered: - return - - self._finish_lock.lock() - - if task_id not in self._tasks_finished: - self._tasks_finished.add(task_id) - self.signal_finished.emit(task_id) - - self._finish_lock.unlock() - - -class CheckFinished(QThread): - signal_finished = pyqtSignal() - - def __init__(self): - super(CheckFinished, self).__init__() - self.total = 0 - self.finished = 0 - - def run(self): - while True: - if self.finished == self.total: - break - - self.msleep(5) - - self.signal_finished.emit() - - def update(self, finished: int): - if finished is not None: - self.finished = finished - - -class EnableSkip(QThread): - - signal_timeout = pyqtSignal() - - def run(self): - ti = datetime.datetime.now() - - while True: - if datetime.datetime.now() >= ti + datetime.timedelta(seconds=10): - self.signal_timeout.emit() - break - - self.msleep(100) - - -class PreparePanel(QWidget, TaskManager): - - signal_status = pyqtSignal(int) - signal_password_response = pyqtSignal(bool, str) - - def __init__(self, context: ApplicationContext, manager: SoftwareManager, - i18n: I18n, manage_window: QWidget, app_config: dict, force_suggestions: bool = False): - super(PreparePanel, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - self.i18n = i18n - self.context = context - self.app_config = app_config - self.manage_window = manage_window - self.setWindowTitle('{} ({})'.format(__app_name__, self.i18n['prepare_panel.title.start'].lower())) - - self.setLayout(QVBoxLayout()) - self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - self.manager = manager - self.tasks = {} - self.output = {} - self.added_tasks = 0 - self.ftasks = 0 - self.started_at = None - self.self_close = False - self.force_suggestions = force_suggestions - - self.prepare_thread = Prepare(self.context, manager, self.i18n) - self.prepare_thread.signal_register.connect(self.register_task) - self.prepare_thread.signal_update.connect(self.update_progress) - self.prepare_thread.signal_finished.connect(self.finish_task) - self.prepare_thread.signal_started.connect(self.start) - self.prepare_thread.signal_ask_password.connect(self.ask_root_password) - self.prepare_thread.signal_output.connect(self.update_output) - self.signal_password_response.connect(self.prepare_thread.set_password_reply) - - self.check_thread = CheckFinished() - self.signal_status.connect(self.check_thread.update) - self.check_thread.signal_finished.connect(self.finish) - - self.skip_thread = EnableSkip() - self.skip_thread.signal_timeout.connect(self._enable_skip_button) - - self.progress_thread = AnimateProgress() - self.progress_thread.signal_change.connect(self._change_progress) - - self.label_top = QLabel() - self.label_top.setCursor(QCursor(Qt.WaitCursor)) - self.label_top.setText("{}...".format(self.i18n['prepare_panel.title.start'].capitalize())) - self.label_top.setObjectName('prepare_status') - self.label_top.setAlignment(Qt.AlignHCenter) - self.layout().addWidget(self.label_top) - self.layout().addWidget(QLabel()) - - self.table = QTableWidget() - self.table.setObjectName('tasks') - self.table.setCursor(QCursor(Qt.WaitCursor)) - self.table.setFocusPolicy(Qt.NoFocus) - self.table.setShowGrid(False) - self.table.verticalHeader().setVisible(False) - self.table.horizontalHeader().setVisible(False) - self.table.horizontalHeader().setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) - self.table.setColumnCount(4) - self.table.setHorizontalHeaderLabels(['' for _ in range(4)]) - self.table.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor)) - self.table.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor)) - self.layout().addWidget(self.table) - - self.textarea_details = QPlainTextEdit(self) - self.textarea_details.setObjectName('task_details') - self.textarea_details.setProperty('console', 'true') - self.textarea_details.resize(self.table.size()) - self.layout().addWidget(self.textarea_details) - self.textarea_details.setVisible(False) - self.textarea_details.setReadOnly(True) - self.textarea_details.setMaximumHeight(100) - self.current_output_task = None - - self.bottom_widget = QWidget() - self.bottom_widget.setLayout(QHBoxLayout()) - self.bottom_widget.layout().addStretch() - - bt_hide_details = QPushButton(self.i18n['prepare.bt_hide_details']) - bt_hide_details.setObjectName('bt_hide_details') - bt_hide_details.clicked.connect(self.hide_output) - bt_hide_details.setCursor(QCursor(Qt.PointingHandCursor)) - self.bottom_widget.layout().addWidget(bt_hide_details) - self.bottom_widget.layout().addStretch() - self.layout().addWidget(self.bottom_widget) - self.bottom_widget.setVisible(False) - - self.bt_bar = QCustomToolbar(policy_height=QSizePolicy.Fixed) - self.bt_close = QPushButton(self.i18n['close'].capitalize()) - self.bt_close.setObjectName('bt_cancel') - self.bt_close.setCursor(QCursor(Qt.PointingHandCursor)) - self.bt_close.clicked.connect(self.close) - self.bt_close.setVisible(False) - self.bt_bar.add_widget(self.bt_close) - self.bt_bar.add_widget(new_spacer()) - - self.progress_bar = QProgressBar() - self.progress_bar.setObjectName('prepare_progress') - self.progress_bar.setTextVisible(False) - self.progress_bar.setVisible(False) - self.progress_bar.setCursor(QCursor(Qt.WaitCursor)) - self.bt_bar.add_widget(self.progress_bar) - self.bt_bar.add_widget(new_spacer()) - - self.bt_skip = QPushButton(self.i18n['prepare_panel.bt_skip.label'].capitalize()) - self.bt_skip.clicked.connect(self.finish) - self.bt_skip.setEnabled(False) - self.bt_skip.setCursor(QCursor(Qt.WaitCursor)) - self.bt_bar.add_widget(self.bt_skip) - - self.layout().addWidget(self.bt_bar) - centralize(self) - - def hide_output(self): - self.current_output_task = None - self.textarea_details.setVisible(False) - self.textarea_details.clear() - self.bottom_widget.setVisible(False) - self._resize_columns() - self.setFocus(Qt.NoFocusReason) - - if not self.bt_bar.isVisible(): - self.bt_bar.setVisible(True) - - def ask_root_password(self): - valid, root_pwd = RootDialog.ask_password(self.context, self.i18n) - self.signal_password_response.emit(valid, root_pwd) - - def _enable_skip_button(self): - self.bt_skip.setEnabled(True) - self.bt_skip.setCursor(QCursor(Qt.PointingHandCursor)) - - def _change_progress(self, value: int): - self.progress_bar.setValue(value) - - def get_table_width(self) -> int: - return reduce(operator.add, [self.table.columnWidth(i) for i in range(self.table.columnCount())]) - - def _resize_columns(self): - header_horizontal = self.table.horizontalHeader() - for i in range(self.table.columnCount()): - header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents) - - self.resize(int(self.get_table_width() * 1.05), self.sizeHint().height()) - - def showEvent(self, event: Optional[QShowEvent]) -> None: - super().showEvent(event) - self.prepare_thread.start() - screen_size = get_current_screen_geometry() - self.setMinimumWidth(int(screen_size.width() * 0.25)) - self.setMinimumHeight(int(screen_size.height() * 0.35)) - self.setMaximumHeight(int(screen_size.height() * 0.95)) - centralize(self) - - def start(self, tasks: int): - self.started_at = time.time() - self.check_thread.total = tasks - self.check_thread.start() - self.skip_thread.start() - - self.progress_thread.start() - - self.bt_close.setVisible(True) - self.progress_bar.setVisible(True) - - def closeEvent(self, ev: QCloseEvent): - if not self.self_close: - QCoreApplication.exit() - - def register_task(self, id_: str, label: str, icon_path: str): - self.added_tasks += 1 - self.table.setRowCount(self.added_tasks) - task_row = self.added_tasks - 1 - - icon_widget = QWidget() - icon_widget.setProperty('container', 'true') - icon_widget.setLayout(QHBoxLayout()) - icon_widget.layout().setContentsMargins(10, 0, 10, 0) - - bt_icon = QToolButton() - bt_icon.setObjectName('bt_task') - bt_icon.setCursor(QCursor(Qt.WaitCursor)) - bt_icon.setEnabled(False) - bt_icon.setToolTip(self.i18n['prepare.bt_icon.no_output']) - - if icon_path: - bt_icon.setIcon(QIcon(icon_path)) - - def _show_output(): - lines = self.output[id_] - - if lines: - self.current_output_task = id_ - self.textarea_details.clear() - self.textarea_details.setVisible(True) - - for l in lines: - self.textarea_details.appendPlainText(l) - - self.bottom_widget.setVisible(True) - - self.setFocus(Qt.NoFocusReason) - - if self.bt_bar.isVisible(): - self.bt_bar.setVisible(False) - - bt_icon.clicked.connect(_show_output) - icon_widget.layout().addWidget(bt_icon) - - self.table.setCellWidget(task_row, 0, icon_widget) - - lb_status = QLabel(label) - lb_status.setObjectName('task_status') - lb_status.setProperty('status', 'running') - lb_status.setCursor(Qt.WaitCursor) - lb_status.setMinimumWidth(50) - lb_status.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - lb_status_col = 1 - self.table.setCellWidget(task_row, lb_status_col, lb_status) - - lb_progress = QLabel('{0:.2f}'.format(0) + '%') - lb_progress.setObjectName('task_progress') - lb_progress.setProperty('status', 'running') - lb_progress.setCursor(Qt.WaitCursor) - lb_progress.setContentsMargins(10, 0, 10, 0) - lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - lb_progress_col = 2 - - self.table.setCellWidget(task_row, lb_progress_col, lb_progress) - - lb_sub = QLabel() - lb_sub.setObjectName('task_substatus') - lb_sub.setCursor(Qt.WaitCursor) - lb_sub.setContentsMargins(10, 0, 10, 0) - lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - lb_sub.setMinimumWidth(50) - self.table.setCellWidget(task_row, 3, lb_sub) - - self.tasks[id_] = {'bt_icon': bt_icon, - 'lb_status': lb_status, - 'lb_status_col': lb_status_col, - 'lb_prog': lb_progress, - 'lb_prog_col': lb_progress_col, - 'progress': 0, - 'lb_sub': lb_sub, - 'finished': False, - 'row': task_row} - - def update_progress(self, task_id: str, progress: float, substatus: str): - task = self.tasks[task_id] - - if progress != task['progress']: - task['progress'] = progress - task['lb_prog'].setText('{0:.2f}'.format(progress) + '%') - - if substatus: - task['lb_sub'].setText('({})'.format(substatus)) - else: - task['lb_sub'].setText('') - - self._resize_columns() - - def update_output(self, task_id: str, output: str): - full_output = self.output.get(task_id) - - if full_output is None: - full_output = [] - self.output[task_id] = full_output - task = self.tasks[task_id] - task['bt_icon'].setEnabled(True) - task['bt_icon'].setCursor(QCursor(Qt.PointingHandCursor)) - task['bt_icon'].setToolTip(self.i18n['prepare.bt_icon.output']) - - full_output.append(output) - - if self.current_output_task == task_id: - self.textarea_details.appendPlainText(output) - - def finish_task(self, task_id: str): - task = self.tasks[task_id] - - for key in ('lb_prog', 'lb_status', 'lb_sub'): - label = task[key] - label.setProperty('status', 'done') - label.style().unpolish(label) - label.style().polish(label) - label.update() - - task['finished'] = True - self._resize_columns() - - self.ftasks += 1 - self.signal_status.emit(self.ftasks) - - def finish(self): - now = time.time() - self.context.logger.info("{0} tasks finished in {1:.9f} seconds".format(self.ftasks, (now - self.started_at))) - if self.isVisible(): - self.manage_window.show() - - if self.force_suggestions: - self.manage_window.begin_load_suggestions(filter_installed=True) - elif self.app_config['boot']['load_apps']: - self.manage_window.begin_refresh_packages() - else: - self.manage_window.load_without_packages() - - self.self_close = True - self.close() diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py deleted file mode 100644 index 2b1d9bdd..00000000 --- a/bauh/view/qt/settings.py +++ /dev/null @@ -1,149 +0,0 @@ -import gc -from io import StringIO -from typing import Optional - -from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal -from PyQt5.QtGui import QCursor, QShowEvent -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QPushButton, QHBoxLayout, QApplication - -from bauh import __app_name__ -from bauh.api.abstract.controller import SoftwareManager -from bauh.api.abstract.view import MessageType -from bauh.view.core.controller import GenericSoftwareManager -from bauh.view.qt import dialog -from bauh.view.qt.components import to_widget, new_spacer -from bauh.view.qt.dialog import ConfirmationDialog -from bauh.view.qt.qt_utils import centralize -from bauh.view.util import util -from bauh.view.util.translation import I18n - - -class ReloadManagePanel(QThread): - - signal_finished = pyqtSignal() - - def __init__(self, manager: SoftwareManager): - super(ReloadManagePanel, self).__init__() - self.manager = manager - - def run(self): - if isinstance(self.manager, GenericSoftwareManager): - self.manager.reset_cache() - - self.manager.prepare(task_manager=None, root_password=None, internet_available=None) - self.signal_finished.emit() - - -class SettingsWindow(QWidget): - - def __init__(self, manager: SoftwareManager, i18n: I18n, window: QWidget, parent: Optional[QWidget] = None): - super(SettingsWindow, self).__init__(parent=parent, flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - self.setWindowTitle(f"{i18n['settings'].capitalize()} ({__app_name__})") - self.setLayout(QVBoxLayout()) - self.manager = manager - self.i18n = i18n - self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - self.window = window - - self.settings_model = tuple(v for v in self.manager.get_settings())[0].component - - self.tab_group = to_widget(self.settings_model, i18n) - self.tab_group.setObjectName('settings') - self.layout().addWidget(self.tab_group) - - lower_container = QWidget() - lower_container.setObjectName('lower_container') - lower_container.setProperty('container', 'true') - lower_container.setLayout(QHBoxLayout()) - lower_container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - - self.bt_close = QPushButton() - self.bt_close.setObjectName('cancel') - self.bt_close.setAutoDefault(True) - self.bt_close.setCursor(QCursor(Qt.PointingHandCursor)) - self.bt_close.setText(self.i18n['close'].capitalize()) - self.bt_close.clicked.connect(lambda: self.close()) - lower_container.layout().addWidget(self.bt_close) - - lower_container.layout().addWidget(new_spacer()) - - self.bt_change = QPushButton() - self.bt_change.setAutoDefault(True) - self.bt_change.setObjectName('ok') - self.bt_change.setCursor(QCursor(Qt.PointingHandCursor)) - self.bt_change.setText(self.i18n['change'].capitalize()) - self.bt_change.clicked.connect(self._save_settings) - lower_container.layout().addWidget(self.bt_change) - - self.layout().addWidget(lower_container) - - self.thread_reload_panel = ReloadManagePanel(manager=manager) - self.thread_reload_panel.signal_finished.connect(self._reload_manage_panel) - centralize(self) - - def showEvent(self, event: Optional[QShowEvent]): - super(SettingsWindow, self).showEvent(event) - self.setMinimumWidth(int(self.sizeHint().width())) - centralize(self) - - def closeEvent(self, event): - if self.window and self.window.settings_window == self: - self.deleteLater() - self.window.settings_window = None - elif not self.window: - QCoreApplication.exit() - - gc.collect() - - def handle_display(self): - if self.isMinimized(): - self.setWindowState(Qt.WindowNoState) - elif self.isHidden(): - self.show() - else: - self.setWindowState(self.windowState() and Qt.WindowMinimized or Qt.WindowActive) - - def _save_settings(self): - self.tab_group.setEnabled(False) - self.bt_change.setEnabled(False) - self.bt_close.setEnabled(False) - - success, warnings = self.manager.save_settings(self.settings_model) - - if success: - if not self.window: - ConfirmationDialog(title=self.i18n['success'].capitalize(), - body=f"{self.i18n['settings.changed.success.warning']}
", - i18n=self.i18n, - confirmation_label=self.i18n['ok'], - confirmation_icon=False, - deny_button=False).ask() - QCoreApplication.exit() - elif ConfirmationDialog(title=self.i18n['warning'].capitalize(), - body=f"{self.i18n['settings.changed.success.warning']}
" - f"{self.i18n['settings.changed.success.reboot']}
", - i18n=self.i18n).ask(): - self.close() - util.restart_app() - else: - self.thread_reload_panel.start() - QApplication.setOverrideCursor(Qt.WaitCursor) - else: - msg = StringIO() - msg.write(f"{self.i18n['settings.error']}
") - - for w in warnings: - msg.write(f'* {w}
{}
'.format('{}
'.format(substatus)) - if not substatus: - self.toolbar_substatus.hide() - elif not self.toolbar_substatus.isVisible() and self.progress_bar.isVisible(): - self.toolbar_substatus.show() - - def _reorganize(self): - if not self._maximized: - self.table_apps.change_headers_policy(QHeaderView.Stretch) - self.table_apps.change_headers_policy() - self._resize(accept_lower_width=len(self.pkgs) > 0) - - def _update_table(self, pkgs_info: dict, signal: bool = False): - self.pkgs = pkgs_info["pkgs_displayed"] - - if pkgs_info["not_installed"] == 0: - update_check = sum_updates_displayed(pkgs_info) > 0 - else: - update_check = False - - self.table_apps.update_packages(self.pkgs, update_check_enabled=update_check) - - if not self._maximized: - self.label_displayed.show() - self.table_apps.change_headers_policy(QHeaderView.Stretch) - self.table_apps.change_headers_policy() - self._resize(accept_lower_width=len(self.pkgs) > 0) - - if len(self.pkgs) == 0 and len(self.pkgs_available) == 0: - self.label_displayed.setText("") - else: - self.label_displayed.setText(f"{len(self.pkgs)} / {len(self.pkgs_available)}") - else: - self.label_displayed.hide() - - if signal: - self.signal_table_update.emit() - - def update_bt_upgrade(self, pkgs_info: dict = None): - show_bt_upgrade = False - - if not any([self.suggestions_requested, self.search_performed]) and (not pkgs_info or pkgs_info['not_installed'] == 0): - for pkg in (pkgs_info['pkgs_displayed'] if pkgs_info else self.pkgs): - if not pkg.model.is_update_ignored() and pkg.update_checked: - show_bt_upgrade = True - break - - self.comp_manager.set_component_visible(BT_UPGRADE, show_bt_upgrade) - - if show_bt_upgrade: - self._reorganize() - - def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True, keep_selected: bool = False): - self.update_bt_upgrade(pkgs_info) - - if pkgs_info['updates'] > 0: - if pkgs_info['not_installed'] == 0: - if not self.comp_manager.is_visible(CHECK_UPDATES): - self.comp_manager.set_component_visible(CHECK_UPDATES, True) - - if not self.filter_updates and not keep_selected: - self._change_checkbox(self.check_updates, True, 'filter_updates', trigger_filters) - - if pkgs_info['napp_updates'] > 0 and self.filter_only_apps and not keep_selected: - self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger_filters) - else: - if not keep_selected: - self._change_checkbox(self.check_updates, False, 'filter_updates', trigger_filters) - - self.comp_manager.set_component_visible(CHECK_UPDATES, False) - - def _change_checkbox(self, checkbox: QCheckBox, checked: bool, attr: str = None, trigger: bool = True): - if not trigger: - checkbox.blockSignals(True) - - checkbox.setChecked(checked) - - if not trigger: - setattr(self, attr, checked) - checkbox.blockSignals(False) - - def _gen_filters(self, ignore_updates: bool = False) -> PackageFilters: - return PackageFilters(category=self.category_filter, - display_limit=0 if self.filter_updates else self.display_limit, - name=self.input_name.text().strip(), - only_apps=False if self.search_performed else self.filter_only_apps, - only_verified=self.filter_only_verified, - only_updates=False if ignore_updates else self.filter_updates, - only_installed=self.filter_installed, - search=self.searched_term, - type=self.type_filter) - - def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: bool, types: Optional[Set[type]] = None, ignore_updates: bool = False, keep_filters: bool = False) -> bool: - self.input_name.set_text('') - pkgs_info = commons.new_pkgs_info() - pkg_idx = new_package_index() - filters = self._gen_filters(ignore_updates=ignore_updates) - - if new_pkgs is not None: - old_installed = None - - if as_installed: - old_installed = self.pkgs_installed - self.pkgs_installed = [] - - for pkg in new_pkgs: - pkgv = PackageView(model=pkg, i18n=self.i18n) - commons.update_info(pkgv, pkgs_info) - add_to_index(pkgv, pkg_idx) - commons.apply_filters(pkgv, filters, pkgs_info) - - if old_installed and types: - for pkgv in old_installed: - if pkgv.model.__class__ not in types: - commons.update_info(pkgv, pkgs_info) - add_to_index(pkgv, pkg_idx) - commons.apply_filters(pkgv, filters, pkgs_info) - - else: # use installed - for pkgv in self.pkgs_installed: - commons.update_info(pkgv, pkgs_info) - add_to_index(pkgv, pkg_idx) - commons.apply_filters(pkgv, filters, pkgs_info) - - if pkgs_info['apps_count'] == 0 and not self.suggestions_requested: - if self.load_suggestions or self.types_changed: - if as_installed: - self.pkgs_installed = pkgs_info['pkgs'] - - self.begin_load_suggestions(filter_installed=True) - self.load_suggestions = False - return False - else: - if not keep_filters: - self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger=False) - self.check_apps.setCheckable(False) - - else: - if not keep_filters: - self.check_apps.setCheckable(True) - self._change_checkbox(self.check_apps, True, 'filter_only_apps', trigger=False) - - self._update_verified_filter(verified_available=pkgs_info['verified'] > 0, keep_state=keep_filters) - self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) - self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) - self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) - self._apply_filters(pkgs_info, ignore_updates=ignore_updates) - self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) - - self.pkgs_available = pkgs_info['pkgs'] - self.pkg_idx = pkg_idx - - if as_installed: - self.pkgs_installed = pkgs_info['pkgs'] - - self.pkgs = pkgs_info['pkgs_displayed'] - self._update_installed_filter(installed_available=pkgs_info['installed'] > 0, - keep_state=keep_filters, - hide=as_installed) - self._update_table(pkgs_info=pkgs_info) - - if new_pkgs: - self.stop_notifying_package_states() - self.thread_notify_pkgs_ready.work = True - self.thread_notify_pkgs_ready.pkgs = self.pkgs - self.thread_notify_pkgs_ready.start() - - self._resize(accept_lower_width=bool(self.pkgs_installed)) - - if not self.installed_loaded and as_installed: - self.installed_loaded = True - - return True - - def _update_installed_filter(self, keep_state: bool = True, hide: bool = False, installed_available: Optional[bool] = None): - if installed_available is not None: - has_installed = installed_available - elif self.pkgs_available == self.pkgs_installed: # it means the "installed" view is loaded - has_installed = False - else: - has_installed = False - if self.pkgs_available: - for p in self.pkgs_available: - if p.model.installed: - has_installed = True - break - - if not keep_state or not has_installed: - self._change_checkbox(self.check_installed, False, 'filter_installed', trigger=False) - - if hide: - self.comp_manager.set_component_visible(CHECK_INSTALLED, False) - else: - self.comp_manager.set_component_visible(CHECK_INSTALLED, has_installed) - - def _update_verified_filter(self, keep_state: bool = True, verified_available: Optional[bool] = None): - if verified_available is not None: - has_verified = verified_available - else: - has_verified = False - if self.pkgs_available: - has_verified = next((True for p in self.pkgs_available if p.model.is_trustable()), False) - - if not keep_state or not has_verified: - self._change_checkbox(self.check_verified, False, 'filter_only_verified', trigger=False) - - self.comp_manager.set_component_visible(CHECK_VERIFIED, has_verified) - - def _apply_filters(self, pkgs_info: dict, ignore_updates: bool): - pkgs_info['pkgs_displayed'] = [] - filters = self._gen_filters(ignore_updates=ignore_updates) - for pkgv in pkgs_info['pkgs']: - commons.apply_filters(pkgv, filters, pkgs_info) - - def _clean_combo_types(self): - if self.combo_filter_type.count() > 1: - for _ in range(self.combo_filter_type.count() - 1): - self.combo_filter_type.removeItem(1) - - def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False): - if available_types is None: - self.comp_manager.set_component_visible(COMBO_TYPES, self.combo_filter_type.count() > 2) - else: - keeping_selected = keep_selected and available_types and self.type_filter in available_types - - if not keeping_selected: - self.type_filter = self.any_type_filter - if not available_types: - self._clean_combo_types() - - if available_types: - self._clean_combo_types() - - sel_type = -1 - for idx, item in enumerate(available_types.items()): - app_type, icon_path, label = item[0], item[1]['icon'], item[1]['label'] - - icon = self.cache_type_filter_icons.get(app_type) - - if not icon: - icon = QIcon(icon_path) - self.cache_type_filter_icons[app_type] = icon - - self.combo_filter_type.addItem(icon, label, app_type) - - if keeping_selected and app_type == self.type_filter: - sel_type = idx + 1 - - self.combo_filter_type.blockSignals(True) - self.combo_filter_type.setCurrentIndex(sel_type if sel_type > -1 else 0) - self.combo_filter_type.blockSignals(False) - self.comp_manager.set_component_visible(COMBO_TYPES, len(available_types) > 1) - else: - self.comp_manager.set_component_visible(COMBO_TYPES, False) - - def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False): - if categories is None: - self.comp_manager.set_component_visible(COMBO_CATEGORIES, self.combo_categories.count() > 1) - else: - keeping_selected = keep_selected and categories and self.category_filter in categories - - if not keeping_selected: - self.category_filter = self.any_category_filter - - if categories: - if self.combo_categories.count() > 1: - for _ in range(self.combo_categories.count() - 1): - self.combo_categories.removeItem(1) - - selected_cat = -1 - cat_list = list(categories) - cat_list.sort() - - for idx, c in enumerate(cat_list): - self.__add_category(c) - - if keeping_selected and c == self.category_filter: - selected_cat = idx + 1 - - self.combo_categories.blockSignals(True) - self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0) - self.combo_categories.blockSignals(False) - self.comp_manager.set_component_visible(COMBO_CATEGORIES, True) - - else: - self.comp_manager.set_component_visible(COMBO_CATEGORIES, False) - - def __add_category(self, category: str): - i18n_cat = self.i18n.get('category.{}'.format(category), self.i18n.get(category, category)) - self.combo_categories.addItem(i18n_cat.capitalize(), category) - - def _get_current_categories(self) -> Set[str]: - if self.combo_categories.count() > 1: - return {self.combo_categories.itemData(idx) for idx in range(self.combo_categories.count()) if idx > 0} - - def _resize(self, accept_lower_width: bool = True): - table_width = self.table_apps.get_width() - toolbar_width = self.toolbar_filters.sizeHint().width() - topbar_width = self.toolbar_status.sizeHint().width() - - new_width = max(table_width, toolbar_width, topbar_width) - new_width *= 1.05 # this extra size is not because of the toolbar button, but the table upgrade buttons - new_width = int(new_width) - - if new_width >= self.maximumWidth(): - new_width = self.maximumWidth() - - if (self.pkgs and accept_lower_width) or new_width > self.width(): - self.resize(new_width, self.height()) - self.setMinimumWidth(new_width) - - def set_progress_controll(self, enabled: bool): - self.progress_controll_enabled = enabled - - def upgrade_selected(self): - body = QWidget() - body.setLayout(QHBoxLayout()) - body.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) - body.layout().addWidget(QLabel(self.i18n['manage_window.upgrade_all.popup.body'])) - body.layout().addWidget(UpgradeToggleButton(pkg=None, root=self, i18n=self.i18n, clickable=False)) - if ConfirmationDialog(title=self.i18n['manage_window.upgrade_all.popup.title'], - i18n=self.i18n, body=None, - widgets=[body]).ask(): - - self._begin_action(action_label=self.i18n['manage_window.status.upgrading'], - action_id=ACTION_UPGRADE) - self.comp_manager.set_components_visible(False) - self._handle_console_option(True) - self.thread_update.pkgs = self.pkgs - self.thread_update.start() - - def _finish_upgrade_selected(self, res: dict): - self._finish_action() - - if res.get('id'): - self._write_operation_logs('upgrade', custom_log_file=f"{UpgradeSelected.UPGRADE_LOGS_DIR}/{res['id']}.log") - sum_log_file = UpgradeSelected.SUMMARY_FILE.format(res['id']) - summ_msg = '* ' + self.i18n['console.upgrade_summary'].format(path=f'"{sum_log_file}"') - self.textarea_details.appendPlainText(summ_msg) - - if res['success']: - self.comp_manager.remove_saved_state(ACTION_UPGRADE) - self.begin_refresh_packages(pkg_types=res['types']) - self._show_console_checkbox_if_output() - - if self._can_notify_user(): - util.notify_user('{} {}'.format(res['updated'], self.i18n['notification.update_selected.success'])) - - notify_tray() - else: - self.comp_manager.restore_state(ACTION_UPGRADE) - self._show_console_errors() - - if self._can_notify_user(): - util.notify_user(self.i18n['notification.update_selected.failed']) - - self.update_custom_actions() - - def _show_console_errors(self): - if self.textarea_details.toPlainText(): - self.check_details.setChecked(True) - else: - self._handle_console_option(False) - self.comp_manager.set_component_visible(CHECK_DETAILS, False) - - def _update_action_output(self, output: str): - self.textarea_details.appendPlainText(output) - - def _begin_action(self, action_label: str, action_id: int = None): - self.thread_animate_progress.stop = False - self.thread_animate_progress.start() - self.progress_bar.setVisible(True) - - if action_id is not None: - self.comp_manager.save_states(action_id, only_visible=True) - - self._set_table_enabled(False) - self.comp_manager.set_component_visible(SEARCH_BAR, False) - self._change_status(action_label) - - def _set_lower_buttons_visible(self, visible: bool): - self.comp_manager.set_group_visible(GROUP_LOWER_BTS, visible) - - if visible: - self.comp_manager.set_component_visible(BT_CUSTOM_ACTIONS, bool(self.custom_actions)) - - def _finish_action(self, action_id: int = None): - self.thread_animate_progress.stop = True - self.thread_animate_progress.wait(msecs=1000) - - self.progress_bar.setVisible(False) - self.progress_bar.setValue(0) - self.progress_bar.setTextVisible(False) - - if action_id is not None: - self.comp_manager.restore_state(action_id) - - self.comp_manager.set_component_visible(SEARCH_BAR, True) - self._change_status() - self._change_label_substatus('') - self._set_table_enabled(True) - self.progress_controll_enabled = True - - def begin_downgrade(self, pkg: PackageView): - pwd, proceed = self._ask_root_password(SoftwareAction.DOWNGRADE, pkg) - - if not proceed: - return - - self.table_apps.stop_file_downloader() - label = f"{self.i18n['manage_window.status.downgrading']} {pkg.model.name}" - self._begin_action(action_label=label, action_id=ACTION_DOWNGRADE) - self.comp_manager.set_components_visible(False) - self._handle_console_option(True) - - self.thread_downgrade.pkg = pkg - self.thread_downgrade.root_pwd = pwd - self.thread_downgrade.start() - - def _finish_downgrade(self, res: dict): - self._finish_action() - self._write_operation_logs('downgrade', res['app']) - - if res['success']: - self.comp_manager.remove_saved_state(ACTION_DOWNGRADE) - - if self._can_notify_user(): - util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded'])) - - self.begin_refresh_packages(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None) - self._show_console_checkbox_if_output() - self.update_custom_actions() - notify_tray() - else: - self.comp_manager.restore_state(ACTION_DOWNGRADE) - self._show_console_errors() - - if self._can_notify_user(): - util.notify_user(self.i18n['notification.downgrade.failed']) - - def begin_show_info(self, pkg: dict): - self._begin_action(self.i18n['manage_window.status.info'], action_id=ACTION_INFO) - self.comp_manager.disable_visible() - - self.thread_show_info.pkg = pkg - self.thread_show_info.start() - - def _finish_show_info(self, pkg_info: dict): - self._finish_action(action_id=ACTION_INFO) - - if pkg_info: - if len(pkg_info) > 1: - dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n, - can_open_url=self.can_open_urls) - dialog_info.exec_() - else: - dialog.show_message(title=self.i18n['warning'].capitalize(), - body=self.i18n['manage_window.info.no_info'].format(bold(pkg_info['__app__'].model.name)), - type_=MessageType.WARNING) - - def begin_show_screenshots(self, pkg: PackageView): - self._begin_action(action_label=self.i18n['manage_window.status.screenshots'].format(bold(pkg.model.name)), - action_id=ACTION_SCREENSHOTS) - self.comp_manager.disable_visible() - - self.thread_screenshots.pkg = pkg - self.thread_screenshots.start() - - def _finish_show_screenshots(self, res: dict): - self._finish_action(ACTION_SCREENSHOTS) - - if res.get('screenshots'): - diag = ScreenshotsDialog(pkg=res['pkg'], - http_client=self.http_client, - icon_cache=self.icon_cache, - logger=self.logger, - i18n=self.i18n, - screenshots=res['screenshots']) - diag.exec_() - else: - dialog.show_message(title=self.i18n['error'], - body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)), - type_=MessageType.ERROR) - - def begin_show_history(self, pkg: PackageView): - self._begin_action(self.i18n['manage_window.status.history'], action_id=ACTION_HISTORY) - self.comp_manager.disable_visible() - - self.thread_show_history.pkg = pkg - self.thread_show_history.start() - - def _finish_show_history(self, res: dict): - self._finish_action(ACTION_HISTORY) - - if res.get('error'): - self._handle_console_option(True) - self.textarea_details.appendPlainText(res['error']) - self.check_details.setChecked(True) - elif not res['history'].history: - dialog.show_message(title=self.i18n['action.history.no_history.title'], - body=self.i18n['action.history.no_history.body'].format(bold(res['history'].pkg.name)), - type_=MessageType.WARNING) - else: - dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) - dialog_history.exec_() - - def search(self): - word = self.search_bar.text().strip() - if word: - self.table_apps.stop_file_downloader() - self._handle_console(False) - self.filter_updates = False - self.filter_installed = False - label = f"{self.i18n['manage_window.status.searching']} {word if word else ''}" - self._begin_action(action_label=label, action_id=ACTION_SEARCH) - self.comp_manager.set_components_visible(False) - self.searched_term = word - self.thread_search.word = word - self.thread_search.start() - - def _finish_search(self, res: dict): - self._finish_action() - self.search_performed = True - - if not res['error']: - self.comp_manager.set_group_visible(GROUP_VIEW_SEARCH, True) - self.update_pkgs(res['pkgs_found'], as_installed=False, ignore_updates=True) - self._set_lower_buttons_visible(True) - self._update_bts_installed_and_suggestions() - self._hide_filters_no_packages() - self._reorganize() - else: - self.comp_manager.restore_state(ACTION_SEARCH) - dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING) - - def _ask_root_password(self, action: SoftwareAction, pkg: PackageView) -> Tuple[Optional[str], bool]: - pwd = None - requires_root = self.manager.requires_root(action, pkg.model) - - if not user.is_root() and requires_root: - valid, pwd = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) - if not valid: - return pwd, False - - return pwd, True - - def install(self, pkg: PackageView): - pwd, proceed = self._ask_root_password(SoftwareAction.INSTALL, pkg) - - if not proceed: - return - - self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name), action_id=ACTION_INSTALL) - self.comp_manager.set_groups_visible(False, GROUP_UPPER_BAR, GROUP_LOWER_BTS) - self._handle_console_option(True) - - self.thread_install.pkg = pkg - self.thread_install.root_pwd = pwd - self.thread_install.start() - - def _write_operation_logs(self, type_: str, pkg: Optional[PackageView] = None, - custom_log_file: Optional[str] = None): - - console_output = self.textarea_details.toPlainText() - - if console_output: - if custom_log_file: - log_dir = os.path.dirname(custom_log_file) - log_file = custom_log_file - else: - log_dir = f"{LOGS_DIR}/{type_}" - if pkg: - log_dir = f"{log_dir}/{pkg.model.get_type()}/{pkg.model.name}" - - log_file = f'{log_dir}/{int(time.time())}.log' - - try: - Path(log_dir).mkdir(parents=True, exist_ok=True) - except OSError: - self.logger.error(f"Could not create the operation log directory '{log_dir}'") - return - - try: - with open(log_file, 'w+') as f: - f.write(console_output) - except OSError: - self.logger.error(f"Could not write the operation log to file '{log_file}'") - return - - log_msg = '\n* ' + self.i18n['console.operation_log'].format(path=f'"{log_file}"') - self.textarea_details.appendPlainText(log_msg) - - def _finish_install(self, res: dict): - self._finish_action(action_id=ACTION_INSTALL) - self._write_operation_logs('install', res['pkg']) - - if res['success']: - if self._can_notify_user(): - util.notify_user(msg='{} ({}) {}'.format(res['pkg'].model.name, res['pkg'].model.get_type(), self.i18n['installed'])) - - models_updated = [] - - for key in ('installed', 'removed'): - if res.get(key): - models_updated.extend(res[key]) - - if models_updated: - installed_available_idxs = [] - for idx, available in enumerate(self.pkgs_available): - for pidx, model in enumerate(models_updated): - if available.model == model: - available.update_model(model) - if model.installed: - installed_available_idxs.append((idx, pidx, available)) - - # re-indexing all installed so they always will be be displayed when no filters are applied - if installed_available_idxs: - # removing from available - installed_available_idxs.sort(key=operator.itemgetter(0)) - for decrement, data in enumerate(installed_available_idxs): - del self.pkgs_available[data[0] - decrement] - - # re-inserting into the available - installed_available_idxs.sort(key=operator.itemgetter(1)) - for new_idx, data in enumerate(installed_available_idxs): - self.pkgs_available.insert(new_idx, data[2]) - - # updating the respective table rows: - screen_width = get_current_screen_geometry(self).width() - for displayed in self.pkgs: - for model in models_updated: - if displayed.model == model: - self.table_apps.update_package(displayed, screen_width=screen_width, change_update_col=True) - - self.update_bt_upgrade() - - # updating installed packages - if res['removed'] and self.pkgs_installed: - to_remove = [] - for idx, installed in enumerate(self.pkgs_installed): - for removed in res['removed']: - if installed.model == removed: - to_remove.append(idx) - - if to_remove: - to_remove.sort() - - for decrement, idx in enumerate(to_remove): - del self.pkgs_installed[idx - decrement] - - if res['installed']: - for idx, model in enumerate(res['installed']): - self.pkgs_installed.insert(idx, PackageView(model, self.i18n)) - - self.update_custom_actions() - self._update_installed_filter(installed_available=True, keep_state=True) - self._update_index() - self.table_apps.change_headers_policy(policy=QHeaderView.Stretch, maximized=self._maximized) - self.table_apps.change_headers_policy(policy=QHeaderView.ResizeToContents, maximized=self._maximized) - self._resize(accept_lower_width=False) - else: - self._show_console_errors() - if self._can_notify_user(): - util.notify_user('{}: {}'.format(res['pkg'].model.name, self.i18n['notification.install.failed'])) - - def _update_progress(self, value: int): - self.progress_bar.setValue(value) - - def begin_execute_custom_action(self, pkg: Optional[PackageView], action: CustomSoftwareAction): - if pkg is None and action.requires_confirmation and \ - not ConfirmationDialog(title=self.i18n['confirmation'].capitalize(), - body='{}
'.format(self.i18n['custom_action.proceed_with'].capitalize().format(bold(self.i18n[action.i18n_label_key]))), - icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')), - i18n=self.i18n).ask(): - return False - - pwd = None - - if not user.is_root() and action.requires_root: - valid, pwd = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) - - if not valid: - return - - action_label = self.i18n[action.i18n_status_key] - - if pkg: - if '{}' in action_label: - action_label = action_label.format(pkg.model.name) - else: - action_label += f' {pkg.model.name}' - - if action.refresh: - self.table_apps.stop_file_downloader() - - self._begin_action(action_label=action_label, action_id=ACTION_CUSTOM_ACTION) - self.comp_manager.set_components_visible(False) - self._handle_console_option(True) - - self.thread_custom_action.pkg = pkg - self.thread_custom_action.root_pwd = pwd - self.thread_custom_action.custom_action = action - self.thread_custom_action.start() - - def _finish_execute_custom_action(self, res: dict): - self._finish_action() - - if res['success']: - if res['action'].refresh: - self.comp_manager.remove_saved_state(ACTION_CUSTOM_ACTION) - self.update_custom_actions() - self.begin_refresh_packages(pkg_types={res['pkg'].model.__class__} if res['pkg'] else None) - else: - self.comp_manager.restore_state(ACTION_CUSTOM_ACTION) - - self._show_console_checkbox_if_output() - else: - self.comp_manager.restore_state(ACTION_CUSTOM_ACTION) - self._show_console_errors() - - if res['error']: - dialog.show_message(title=self.i18n['warning' if res['error_type'] == MessageType.WARNING else 'error'].capitalize(), - body=self.i18n[res['error']], - type_=res['error_type']) - - def _show_console_checkbox_if_output(self): - if self.textarea_details.toPlainText(): - self.comp_manager.set_component_visible(CHECK_DETAILS, True) - else: - self.comp_manager.set_component_visible(CHECK_DETAILS, False) - - def show_settings(self): - if self.settings_window: - self.settings_window.handle_display() - else: - self.settings_window = SettingsWindow(manager=self.manager, i18n=self.i18n, window=self) - screen_width = get_current_screen_geometry(self).width() - self.settings_window.setMinimumWidth(int(screen_width / 4)) - self.settings_window.resize(self.size()) - self.settings_window.adjustSize() - self.settings_window.show() - - def _map_custom_action(self, action: CustomSoftwareAction, parent: QWidget) -> QCustomMenuAction: - - if action.icon_path: - try: - if action.icon_path.startswith('/'): - icon = QIcon(action.icon_path) - else: - icon = QIcon.fromTheme(action.icon_path) - except Exception: - icon = None - else: - icon = None - - tip = self.i18n[action.i18n_description_key] if action.i18n_description_key else None - return QCustomMenuAction(parent=parent, - label=self.i18n[action.i18n_label_key], - action=lambda: self.begin_execute_custom_action(None, action), - tooltip=tip, - icon=icon) - - def show_custom_actions(self): - if self.custom_actions: - menu_row = QMenu() - menu_row.setCursor(QCursor(Qt.PointingHandCursor)) - actions = [self._map_custom_action(a, menu_row) for a in self.custom_actions] - menu_row.addActions(actions) - menu_row.adjustSize() - menu_row.popup(QCursor.pos()) - menu_row.exec_() - - def begin_ignore_updates(self, pkg: PackageView): - status_key = 'ignore_updates' if not pkg.model.is_update_ignored() else 'ignore_updates_reverse' - self._begin_action(action_label=self.i18n['manage_window.status.{}'.format(status_key)].format(pkg.model.name), - action_id=ACTION_IGNORE_UPDATES) - self.comp_manager.disable_visible() - self.thread_ignore_updates.pkg = pkg - self.thread_ignore_updates.start() - - def finish_ignore_updates(self, res: dict): - self._finish_action(action_id=ACTION_IGNORE_UPDATES) - - if res['success']: - hide_package = commons.is_package_hidden(res['pkg'], self._gen_filters()) - - if hide_package: - idx_to_remove = None - for pkg in self.pkgs: - if pkg == res['pkg']: - idx_to_remove = pkg.table_index - break - - if idx_to_remove is not None: - del self.pkgs[idx_to_remove] - self.table_apps.removeRow(idx_to_remove) - self._update_table_indexes() - self.update_bt_upgrade() - else: - screen_width = get_current_screen_geometry(self).width() - for pkg in self.pkgs: - if pkg == res['pkg']: - pkg.update_model(res['pkg'].model) - self.table_apps.update_package(pkg, screen_width=screen_width, change_update_col=not any([self.search_performed, self.suggestions_requested])) - self.update_bt_upgrade() - break - - for pkg_list in (self.pkgs_available, self.pkgs_installed): - if pkg_list: - for pkg in pkg_list: - if pkg == res['pkg']: - pkg.update_model(res['pkg'].model) - break - - self._add_pkg_categories(res['pkg']) - self._update_index() - - dialog.show_message(title=self.i18n['success'].capitalize(), - body=self.i18n['action.{}.success'.format(res['action'])].format(bold(res['pkg'].model.name)), - type_=MessageType.INFO) - else: - dialog.show_message(title=self.i18n['fail'].capitalize(), - body=self.i18n['action.{}.fail'.format(res['action'])].format(bold(res['pkg'].model.name)), - type_=MessageType.ERROR) - - def _add_pkg_categories(self, pkg: PackageView): - if pkg.model.categories: - pkg_categories = {c.strip().lower() for c in pkg.model.categories if c and c.strip()} - - if pkg_categories: - current_categories = self._get_current_categories() - - if current_categories: - pkg_categories = {c.strip().lower() for c in pkg.model.categories if c} - - if pkg_categories: - categories_to_add = {c for c in pkg_categories if c and c not in current_categories} - - if categories_to_add: - for cat in categories_to_add: - self.__add_category(cat) - else: - self._update_categories(pkg_categories) - - def _map_theme_action(self, theme: ThemeMetadata, menu: QMenu) -> QCustomMenuAction: - def _change_theme(): - set_theme(theme_key=theme.key, app=QApplication.instance(), logger=self.context.logger) - self.thread_save_theme.theme_key = theme.key - self.thread_save_theme.start() - - return QCustomMenuAction(label=theme.get_i18n_name(self.i18n), - action=_change_theme, - parent=menu, - tooltip=theme.get_i18n_description(self.i18n)) - - def show_themes(self): - menu_row = QMenu() - menu_row.setCursor(QCursor(Qt.PointingHandCursor)) - menu_row.addActions(self._map_theme_actions(menu_row)) - menu_row.adjustSize() - menu_row.popup(QCursor.pos()) - menu_row.exec_() - - def _map_theme_actions(self, menu: QMenu) -> List[QCustomMenuAction]: - core_config = CoreConfigManager().get_config() - - current_theme_key, current_action = core_config['ui']['theme'], None - - actions = [] - - for t in read_all_themes_metadata(): - if not t.abstract: - action = self._map_theme_action(t, menu) - - if current_action is None and current_theme_key is not None and current_theme_key == t.key: - action.button.setProperty('current', 'true') - current_action = action - else: - actions.append(action) - - if not current_action: - invalid_action = QCustomMenuAction(label=self.i18n['manage_window.bt_themes.option.invalid'], parent=menu) - invalid_action.button.setProperty('current', 'true') - current_action = invalid_action - - actions.sort(key=lambda a: a.get_label()) - actions.insert(0, current_action) - return actions - - def reload(self): - self.thread_reload.start() - - def _reload(self): - self.update_custom_actions() - self.verify_warnings() - self.types_changed = True - self.begin_refresh_packages() - - def closeEvent(self, event: QCloseEvent) -> None: - # needs to be stopped to avoid a Qt exception/crash - self.table_apps.stop_file_downloader(wait=True) - - @property - def can_open_urls(self) -> bool: - if self._can_open_urls is None: - self._can_open_urls = shutil.which("xdg-open") - - return self._can_open_urls diff --git a/bauh/view/util/__init__.py b/bauh/view/util/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bauh/view/util/cache.py b/bauh/view/util/cache.py deleted file mode 100644 index ae119351..00000000 --- a/bauh/view/util/cache.py +++ /dev/null @@ -1,89 +0,0 @@ -import datetime -from threading import Lock -from typing import Optional - -from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory - - -class DefaultMemoryCache(MemoryCache): - """ - A synchronized cache implementation - """ - - def __init__(self, expiration_time: int): - super(DefaultMemoryCache, self).__init__() - self.expiration_time = expiration_time - self._cache = {} - self.lock = Lock() - - def is_enabled(self): - return self.expiration_time < 0 or self.expiration_time > 0 - - def add(self, key: str, val: object): - if key and self.is_enabled(): - self.lock.acquire() - self._add(key, val) - self.lock.release() - - def _add(self, key: str, val: object): - if key: - self._cache[key] = {'val': val, 'expires_at': datetime.datetime.now(UTC) + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None} - - def add_non_existing(self, key: str, val: object): - if key and self. is_enabled(): - self.lock.acquire() - cur_val = self.get(key, lock=False) - - if cur_val is None: - self._add(key, val) - - self.lock.release() - - def get(self, key: str, lock: bool = True): - if key and self.is_enabled(): - val = self._cache.get(key) - - if val: - expiration = val.get('expires_at') - - if expiration and expiration <= datetime.datetime.now(UTC): - if lock: - self.lock.acquire() - - del self._cache[key] - - if lock: - self.lock.release() - - return None - - return val['val'] - - def delete(self, key): - if key and self.is_enabled(): - if key in self._cache: - self.lock.acquire() - del self._cache[key] - self.lock.release() - - def keys(self): - return set(self._cache.keys()) if self.is_enabled() else set() - - def clean_expired(self): - if self.is_enabled(): - for key in self.keys(): - self.get(key) - - -class DefaultMemoryCacheFactory(MemoryCacheFactory): - - def __init__(self, expiration_time: int): - """ - :param expiration_time: default expiration time for all instantiated caches - """ - super(DefaultMemoryCacheFactory, self).__init__() - self.expiration_time = expiration_time - - def new(self, expiration: Optional[int] = None) -> MemoryCache: - return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) -UTC = datetime.timezone.utc diff --git a/bauh/view/util/disk.py b/bauh/view/util/disk.py deleted file mode 100644 index f460f45f..00000000 --- a/bauh/view/util/disk.py +++ /dev/null @@ -1,115 +0,0 @@ -import json -import logging -import os -import time -from threading import Thread, Lock -from typing import Type, Dict, Any, Optional - -import yaml - -from bauh.api.abstract.cache import MemoryCache -from bauh.api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory -from bauh.api.abstract.model import SoftwarePackage - - -class AsyncDiskCacheLoader(Thread, DiskCacheLoader): - - def __init__(self, cache_map: Dict[Type[SoftwarePackage], MemoryCache], logger: logging.Logger): - super(AsyncDiskCacheLoader, self).__init__(daemon=True) - self.pkgs = [] - self._work = True - self.lock = Lock() - self.cache_map = cache_map - self.logger = logger - self.processed = 0 - self._working = False - - def fill(self, pkg: SoftwarePackage, sync: bool = False): - """ - Adds a package which data must be read from the disk to a queue (if not sync) - :param pkg: - :param sync: - :return: - """ - if pkg and pkg.supports_disk_cache(): - if sync or not self._working: - self._fill_cached_data(pkg) - else: - self.pkgs.append(pkg) - - def read(self, pkg: SoftwarePackage) -> Optional[Dict[str, Any]]: - if pkg and pkg.supports_disk_cache(): - data_path = pkg.get_disk_data_path() - - if data_path and os.path.isfile(data_path): - ext = data_path.split('.')[-1] - - try: - with open(data_path) as f: - file_content = f.read() - except FileNotFoundError: - return - - if file_content: - if ext == 'json': - cached_data = json.loads(file_content) - elif ext in {'yml', 'yaml'}: - cached_data = yaml.load(file_content) - else: - raise Exception(f'The cached data file {data_path} has an unsupported format') - - if cached_data: - return cached_data - - else: - self.logger.warning(f"No cached content in file {data_path}") - - def stop_working(self): - self._work = False - - def run(self): - self._working = True - last = 0 - - while True: - time.sleep(0.00001) - if len(self.pkgs) > self.processed: - pkg = self.pkgs[last] - - self._fill_cached_data(pkg) - self.processed += 1 - last += 1 - elif not self._work: - break - - self._working = False - - def _fill_cached_data(self, pkg: SoftwarePackage) -> bool: - cached_data = self.read(pkg) - - if cached_data: - pkg.fill_cached_data(cached_data) - cache = self.cache_map.get(pkg.__class__) - - if cache: - cache.add_non_existing(str(pkg.id), cached_data) - - return True - - return False - - -class DefaultDiskCacheLoaderFactory(DiskCacheLoaderFactory): - - def __init__(self, logger: logging.Logger): - super(DefaultDiskCacheLoaderFactory, self).__init__() - self.logger = logger - self.cache_map = {} - - def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache): - if pkg_type: - if pkg_type not in self.cache_map: - self.cache_map[pkg_type] = cache - - def new(self) -> AsyncDiskCacheLoader: - return AsyncDiskCacheLoader(cache_map=self.cache_map, logger=self.logger) diff --git a/bauh/view/util/logs.py b/bauh/view/util/logs.py deleted file mode 100644 index d17e3fec..00000000 --- a/bauh/view/util/logs.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging -from logging import INFO - -FORMAT = '%(asctime)s %(levelname)s [%(module_path)s:%(lineno)s - %(funcName)s()] - %(message)s' - - -class FilePathFilter(logging.Filter): - - def filter(self, record): - record.module_path = record.pathname.split('site-packages/')[1] if 'site-packages' in record.pathname else str(record.pathname) - return True - - -def new_logger(name: str, enabled: bool) -> logging.Logger: - instance = logging.Logger(name, level=INFO) - instance.addFilter(FilePathFilter()) - stream_handler = logging.StreamHandler() - stream_handler.setFormatter(logging.Formatter(FORMAT)) - instance.addHandler(stream_handler) - instance.disabled = not enabled - - return instance diff --git a/bauh/view/util/resource.py b/bauh/view/util/resource.py deleted file mode 100755 index a0c0f0c1..00000000 --- a/bauh/view/util/resource.py +++ /dev/null @@ -1,5 +0,0 @@ -from bauh import ROOT_DIR - - -def get_path(resource_path): - return ROOT_DIR + '/view/resources/' + resource_path diff --git a/bauh/view/util/translation.py b/bauh/view/util/translation.py deleted file mode 100644 index 807c2cdf..00000000 --- a/bauh/view/util/translation.py +++ /dev/null @@ -1,89 +0,0 @@ -import glob -import locale -import os -from typing import Tuple, Set - -from bauh.view.util import resource - - -class I18n(dict): - - def __init__(self, current_key: str, current_locale: dict, default_key: str, default_locale: dict): - super(I18n, self).__init__() - self.current_key = current_key - self.current = current_locale - self.default_key = default_key - self.default = default_locale - - def __getitem__(self, item): - try: - return self.current.__getitem__(item) - except KeyError: - if self.default: - try: - return self.default.__getitem__(item) - except KeyError: - return item - else: - return item - - def get(self, *args, **kwargs): - res = self.current.get(args[0]) - - if res is None: - if self.default: - return self.default.get(*args, **kwargs) - else: - return self.current.get(*args, **kwargs) - - return res - - -def get_available_keys() -> Set[str]: - locale_dir = resource.get_path('locale') - return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*') if os.path.isfile(file)} - - -def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]: - - locale_path = None - - if key is None: - try: - current_locale = locale.getlocale() - - if current_locale is None or current_locale[0] is None: - current_locale = ('en', 'UTF-8') - except Exception: - current_locale = ('en', 'UTF-8') - - else: - current_locale = [key.strip().lower()] - - if current_locale: - current_locale = current_locale[0] - - for locale_file in glob.glob(locale_dir + '/*'): - name = locale_file.split('/')[-1] - - if current_locale == name or current_locale.startswith(name + '_'): - locale_path = locale_file - break - - if not locale_path: - return current_locale if current_locale else key, {} - - with open(locale_path, 'r') as f: - locale_keys = f.readlines() - - locale_obj = {} - for line in locale_keys: - line_strip = line.strip() - if line_strip: - try: - keyval = line_strip.split('=') - locale_obj[keyval[0].strip()] = keyval[1].strip() - except Exception: - print("Error decoding i18n line '{}'".format(line)) - - return locale_path.split('/')[-1], locale_obj diff --git a/bauh/view/util/util.py b/bauh/view/util/util.py deleted file mode 100644 index 1611d20e..00000000 --- a/bauh/view/util/util.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import shutil -import subprocess -import sys -import traceback -from typing import List, Tuple - -from PyQt5.QtCore import QCoreApplication -from PyQt5.QtGui import QIcon -from colorama import Fore - -from bauh import __app_name__ -from bauh.api.abstract.controller import SoftwareManager -from bauh.api.paths import CONFIG_DIR, CACHE_DIR, TEMP_DIR -from bauh.commons.system import run_cmd -from bauh.view.util import resource - - -def notify_user(msg: str, icon_path: str = None): - icon_id = icon_path - - if not icon_id: - icon_id = get_default_icon()[0] - - os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_id) if icon_id else '', msg)) - - -def get_default_icon(system: bool = True) -> Tuple[str, QIcon]: - if system: - system_icon = QIcon.fromTheme(__app_name__) - if not system_icon.isNull(): - return system_icon.name(), system_icon - - path = resource.get_path('img/logo.svg') - return path, QIcon(path) - - -def restart_app(): - appimage_path = os.getenv('APPIMAGE') - - restart_cmd = [appimage_path] if appimage_path else [sys.executable, *sys.argv] - - subprocess.Popen(restart_cmd) - QCoreApplication.exit() - - -def get_distro(): - if os.path.exists('/etc/arch-release'): - return 'arch' - - if os.path.exists('/etc/os-release'): - with open('/etc/os-release', 'r') as os_release_file: - for line in os_release_file: - if 'ID_LIKE=arch' in line: - return 'arch' - - if os.path.exists('/proc/version'): - if 'ubuntu' in run_cmd('cat /proc/version').lower(): - return 'ubuntu' - - return 'unknown' - - -def clean_app_files(managers: List[SoftwareManager], logs: bool = True): - - if logs: - print('[{}] Cleaning configuration and cache files'.format(__app_name__)) - - for path in (CACHE_DIR, CONFIG_DIR, TEMP_DIR): - if logs: - print('[{}] Deleting directory {}'.format(__app_name__, path)) - - if os.path.exists(path): - try: - shutil.rmtree(path) - if logs: - print('{}[{}] Directory {} deleted{}'.format(Fore.YELLOW, __app_name__, path, Fore.RESET)) - except Exception: - if logs: - print('{}[{}] An exception has happened when deleting {}{}'.format(Fore.RED, __app_name__, path, Fore.RESET)) - traceback.print_exc() - - if managers: - for m in managers: - m.clear_data() - - if logs: - print('[{}] Cleaning finished'.format(__app_name__)) diff --git a/bearhub/api/__init__.py b/bearhub/api/__init__.py index 00f0f1e7..e69de29b 100644 --- a/bearhub/api/__init__.py +++ b/bearhub/api/__init__.py @@ -1,2 +0,0 @@ -from bauh.api import * # noqa: F401,F403 - diff --git a/bearhub/api/abstract/__init__.py b/bearhub/api/abstract/__init__.py index d5df5008..e69de29b 100644 --- a/bearhub/api/abstract/__init__.py +++ b/bearhub/api/abstract/__init__.py @@ -1,2 +0,0 @@ -from bauh.api.abstract import * # noqa: F401,F403 - diff --git a/bearhub/api/abstract/context.py b/bearhub/api/abstract/context.py index fcadf4a9..3fe3f901 100644 --- a/bearhub/api/abstract/context.py +++ b/bearhub/api/abstract/context.py @@ -1,2 +1,75 @@ -from bauh.api.abstract.context import * # noqa: F401,F403 +import logging +import sys +from typing import Optional, Dict +from bauh.api.abstract.cache import MemoryCacheFactory +from bauh.api.abstract.disk import DiskCacheLoaderFactory +from bauh.api.abstract.download import FileDownloader +from bauh.api.http import HttpClient +from bauh.commons.internet import InternetChecker +from bauh.view.util.translation import I18n + + +class ApplicationContext: + + def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n, + cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory, + logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str, + app_version: str, internet_checker: InternetChecker, root_user: bool, screen_width: int = -1, + screen_height: int = -1, suggestions_mapping: Optional[Dict[str, str]] = None): + """ + :param download_icons: if packages icons should be downloaded + :param http_client: a shared instance of http client + :param app_root_dir: GUI root dir + :param i18n: the translation keys + :param cache_factory: + :param disk_loader_factory: + :param logger: a logger instance + :param file_downloader + :param distro + :param app_name + :param app_version + :param internet_checker + :param screen_width + :param screen_height + :param suggestions_mapping + """ + self.download_icons = download_icons + self.http_client = http_client + self.app_root_dir = app_root_dir + self.i18n = i18n + self.cache_factory = cache_factory + self.disk_loader_factory = disk_loader_factory + self.logger = logger + self.file_downloader = file_downloader + self.arch_x86_64 = sys.maxsize > 2**32 + self.distro = distro + self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game', + 'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility') + self.app_name = app_name + self.app_version = app_version + self.root_user = root_user + self.root_password = None + self.internet_checker = internet_checker + self.screen_width = screen_width + self.screen_height = screen_height + self._suggestions_mapping = suggestions_mapping + + def is_system_x86_64(self): + return self.arch_x86_64 + + def get_view_path(self): + return self.app_root_dir + '/view' + + def is_internet_available(self) -> bool: + return self.internet_checker.is_available() + + def get_suggestion_url(self, module: str, default: Optional[str] = None) -> Optional[str]: + if self._suggestions_mapping: + module_split = module.split(f'{self.app_name}.gems.') + + if len(module_split) > 1: + gem_name = module_split[1].split('.')[0] + return self._suggestions_mapping.get(gem_name, default) + + return default diff --git a/bauh/api/abstract/controller.py b/bearhub/api/abstract/controller.py similarity index 100% rename from bauh/api/abstract/controller.py rename to bearhub/api/abstract/controller.py diff --git a/bauh/api/abstract/disk.py b/bearhub/api/abstract/disk.py similarity index 100% rename from bauh/api/abstract/disk.py rename to bearhub/api/abstract/disk.py diff --git a/bauh/api/abstract/download.py b/bearhub/api/abstract/download.py similarity index 100% rename from bauh/api/abstract/download.py rename to bearhub/api/abstract/download.py diff --git a/bauh/api/abstract/handler.py b/bearhub/api/abstract/handler.py similarity index 100% rename from bauh/api/abstract/handler.py rename to bearhub/api/abstract/handler.py diff --git a/bauh/api/abstract/model.py b/bearhub/api/abstract/model.py similarity index 100% rename from bauh/api/abstract/model.py rename to bearhub/api/abstract/model.py diff --git a/bauh/api/abstract/view.py b/bearhub/api/abstract/view.py similarity index 100% rename from bauh/api/abstract/view.py rename to bearhub/api/abstract/view.py diff --git a/bauh/api/exception.py b/bearhub/api/exception.py similarity index 100% rename from bauh/api/exception.py rename to bearhub/api/exception.py diff --git a/bearhub/api/http.py b/bearhub/api/http.py index c339bdcb..f325acb0 100644 --- a/bearhub/api/http.py +++ b/bearhub/api/http.py @@ -1,2 +1,121 @@ -from bauh.api.http import * # noqa: F401,F403 +import logging +import time +import traceback +from typing import Optional +import requests +import yaml + +from bauh.commons import system +from bauh.commons.view_utils import get_human_size_str + + +class HttpClient: + + def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = 30, sleep: float = 0.5): + self.max_attempts = max_attempts + self.session = requests.Session() + self.timeout = timeout + self.sleep = sleep + self.logger = logger + + def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False, + session: bool = True, stream: bool = False) -> Optional[requests.Response]: + cur_attempts = 1 + + while cur_attempts <= self.max_attempts: + cur_attempts += 1 + + try: + args = {'timeout': self.timeout, 'allow_redirects': allow_redirects, 'stream': stream} + + if params: + args['params'] = params + + if headers: + args['headers'] = headers + + if ignore_ssl: + args['verify'] = False + + if session: + res = self.session.get(url, **args) + else: + res = requests.get(url, **args) + + if 200 <= res.status_code < 300: + return res + + if single_call: + return res + + if self.sleep > 0: + time.sleep(self.sleep) + except Exception as e: + if isinstance(e, requests.exceptions.ConnectionError): + self.logger.error('Internet seems to be off') + raise e + elif isinstance(e, requests.exceptions.TooManyRedirects): + self.logger.warning(f"Too many redirects for GET -> {url}") + raise e + elif e.__class__ in (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema): + self.logger.warning(f"The URL '{url}' has an invalid schema") + raise e + + self.logger.error(f"Could not retrieve data from '{url}'") + traceback.print_exc() + continue + + self.logger.warning(f"Could not retrieve data from '{url}'") + + def get_json(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True): + res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session) + return res.json() if res else None + + def get_yaml(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True): + res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session) + return yaml.safe_load(res.text) if res else None + + def get_content_length_in_bytes(self, url: str, session: bool = True) -> Optional[int]: + if not url: + return + + params = {'url': url, 'allow_redirects': True, 'stream': True} + + try: + if session: + res = self.session.get(**params) + else: + res = requests.get(**params) + except requests.exceptions.ConnectionError: + self.logger.info(f"Internet seems to be off. Could not reach '{url}'") + return + + if res.status_code == 200: + size = res.headers.get('Content-Length') + + if size: + try: + return int(size) + except Exception: + pass + + def get_content_length(self, url: str, session: bool = True) -> Optional[str]: + size = self.get_content_length_in_bytes(url, session) + + if size: + return get_human_size_str(size) + + def exists(self, url: str, session: bool = True, timeout: int = 5) -> bool: + params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': timeout} + + try: + if session: + res = self.session.head(**params) + else: + res = self.session.get(**params) + except requests.exceptions.TooManyRedirects: + self.logger.warning(f"{url} seems to exist, but too many redirects have happened") + return True + + return res.status_code in (200, 403) diff --git a/bearhub/api/paths.py b/bearhub/api/paths.py index 1af81d11..8c388360 100644 --- a/bearhub/api/paths.py +++ b/bearhub/api/paths.py @@ -1,2 +1,70 @@ -from bauh.api.paths import * # noqa: F401,F403 +import os +import shutil +from getpass import getuser +from pathlib import Path +from bauh import __app_name__ +from bauh.api import user + + +def get_temp_dir(username: str) -> str: + return f'/tmp/{__app_name__}@{username}' + + +CACHE_DIR = f'/var/cache/{__app_name__}' if user.is_root() else f'{Path.home()}/.cache/{__app_name__}' +CONFIG_DIR = f'/etc/{__app_name__}' if user.is_root() else f'{Path.home()}/.config/{__app_name__}' +USER_THEMES_DIR = f'/usr/share/{__app_name__}/themes' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}/themes' +DESKTOP_ENTRIES_DIR = '/usr/share/applications' if user.is_root() else f'{Path.home()}/.local/share/applications' +TEMP_DIR = get_temp_dir(getuser()) +LOGS_DIR = f'{TEMP_DIR}/logs' +AUTOSTART_DIR = f'/etc/xdg/autostart' if user.is_root() else f'{Path.home()}/.config/autostart' +BINARIES_DIR = f'/usr/local/bin' if user.is_root() else f'{Path.home()}/.local/bin' +SHARED_FILES_DIR = f'/usr/local/share/{__app_name__}' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}' + + +def _migrate_dir_if_possible(old_path: str, new_path: str): + if old_path == new_path: + return + + if not os.path.exists(old_path): + return + + if os.path.exists(new_path): + return + + try: + parent = os.path.dirname(new_path) + if parent: + Path(parent).mkdir(parents=True, exist_ok=True) + + shutil.move(old_path, new_path) + except Exception: + # The migration is best-effort. If it fails, the app can still start with empty defaults. + pass + + +def migrate_legacy_paths(): + old_app_name = 'bauh' + + if old_app_name == __app_name__: + return + + if user.is_root(): + old_cache = f'/var/cache/{old_app_name}' + old_config = f'/etc/{old_app_name}' + old_shared = f'/usr/local/share/{old_app_name}' + old_temp = f'/tmp/{old_app_name}@{getuser()}' + else: + home = Path.home() + old_cache = f'{home}/.cache/{old_app_name}' + old_config = f'{home}/.config/{old_app_name}' + old_shared = f'{home}/.local/share/{old_app_name}' + old_temp = f'/tmp/{old_app_name}@{getuser()}' + + _migrate_dir_if_possible(old_cache, CACHE_DIR) + _migrate_dir_if_possible(old_config, CONFIG_DIR) + _migrate_dir_if_possible(old_shared, SHARED_FILES_DIR) + _migrate_dir_if_possible(old_temp, TEMP_DIR) + + +migrate_legacy_paths() diff --git a/bearhub/api/user.py b/bearhub/api/user.py index 9e2b9bf6..cd407ac2 100644 --- a/bearhub/api/user.py +++ b/bearhub/api/user.py @@ -1,2 +1,6 @@ -from bauh.api.user import * # noqa: F401,F403 +import os +from typing import Optional + +def is_root(user_id: Optional[int] = None): + return user_id == 0 if user_id is not None else os.getuid() == 0 diff --git a/bearhub/commons/__init__.py b/bearhub/commons/__init__.py index e0841f4d..e69de29b 100644 --- a/bearhub/commons/__init__.py +++ b/bearhub/commons/__init__.py @@ -1,2 +0,0 @@ -from bauh.commons import * # noqa: F401,F403 - diff --git a/bauh/commons/boot.py b/bearhub/commons/boot.py similarity index 100% rename from bauh/commons/boot.py rename to bearhub/commons/boot.py diff --git a/bauh/commons/category.py b/bearhub/commons/category.py similarity index 100% rename from bauh/commons/category.py rename to bearhub/commons/category.py diff --git a/bearhub/commons/config.py b/bearhub/commons/config.py index 64a391fc..f2e6cb40 100644 --- a/bearhub/commons/config.py +++ b/bearhub/commons/config.py @@ -1,2 +1,102 @@ -from bauh.commons.config import * # noqa: F401,F403 +import os +import traceback +from abc import abstractmethod, ABC +from pathlib import Path +from threading import Thread +from typing import Optional +import yaml + +from bauh.api.paths import CONFIG_DIR +from bauh.commons import util + + +def read_config(file_path: str, template: dict, update_file: bool = False, update_async: bool = False) -> dict: + if not os.path.exists(file_path): + Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True) + save_config(template, file_path) + else: + with open(file_path) as f: + local_config = yaml.safe_load(f.read()) + + if local_config: + util.deep_update(template, local_config) + + if update_file: + if update_async: + Thread(target=save_config, args=(template, file_path), daemon=True).start() + else: + save_config(template, file_path) + + return template + + +def save_config(config: dict, file_path: str): + with open(file_path, 'w+') as f: + f.write(yaml.dump(config)) + + +class ConfigManager(ABC): + + @abstractmethod + def read_config(self) -> Optional[dict]: + pass + + @abstractmethod + def get_default_config(self) -> dict: + pass + + @abstractmethod + def is_config_cached(self) -> bool: + pass + + def get_config(self) -> dict: + default_config = self.get_default_config() + + if default_config: + cached_config = self.read_config() + + if cached_config: + self.merge_config(default_config, cached_config) + + return default_config + + @staticmethod + def merge_config(base_config: dict, current_config: dict): + util.deep_update(base_config, current_config) + + @abstractmethod + def save_config(self, config_obj: dict): + pass + + +class YAMLConfigManager(ConfigManager, ABC): + + def __init__(self, config_file_path: str): + self.file_path = config_file_path + + def is_config_cached(self) -> bool: + return os.path.exists(self.file_path) + + def read_config(self) -> Optional[dict]: + if self.is_config_cached(): + with open(self.file_path) as f: + local_config = yaml.safe_load(f.read()) + + if local_config is not None: + return local_config + + def save_config(self, config_obj: dict): + if config_obj: + config_dir = os.path.dirname(self.file_path) + try: + Path(config_dir).mkdir(parents=True, exist_ok=True) + except OSError: + traceback.print_exc() + return + + try: + with open(self.file_path, 'w+') as f: + f.write(yaml.dump(config_obj)) + except Exception: + traceback.print_exc() diff --git a/bauh/commons/custom_types.py b/bearhub/commons/custom_types.py similarity index 100% rename from bauh/commons/custom_types.py rename to bearhub/commons/custom_types.py diff --git a/bauh/commons/html.py b/bearhub/commons/html.py similarity index 100% rename from bauh/commons/html.py rename to bearhub/commons/html.py diff --git a/bearhub/commons/internet.py b/bearhub/commons/internet.py index 9b0f375d..e52f1702 100644 --- a/bearhub/commons/internet.py +++ b/bearhub/commons/internet.py @@ -1,2 +1,17 @@ -from bauh.commons.internet import * # noqa: F401,F403 +import socket + +class InternetChecker: + + def __init__(self, offline: bool): + self.offline = offline + + def is_available(self) -> bool: + if self.offline: + return False + + try: + socket.gethostbyname("w3.org") + return True + except Exception: + return False diff --git a/bauh/commons/regex.py b/bearhub/commons/regex.py similarity index 100% rename from bauh/commons/regex.py rename to bearhub/commons/regex.py diff --git a/bauh/commons/resource.py b/bearhub/commons/resource.py similarity index 100% rename from bauh/commons/resource.py rename to bearhub/commons/resource.py diff --git a/bauh/commons/singleton.py b/bearhub/commons/singleton.py similarity index 100% rename from bauh/commons/singleton.py rename to bearhub/commons/singleton.py diff --git a/bauh/commons/suggestions.py b/bearhub/commons/suggestions.py similarity index 100% rename from bauh/commons/suggestions.py rename to bearhub/commons/suggestions.py diff --git a/bauh/commons/system.py b/bearhub/commons/system.py similarity index 100% rename from bauh/commons/system.py rename to bearhub/commons/system.py diff --git a/bauh/commons/util.py b/bearhub/commons/util.py similarity index 100% rename from bauh/commons/util.py rename to bearhub/commons/util.py diff --git a/bauh/commons/version_util.py b/bearhub/commons/version_util.py similarity index 100% rename from bauh/commons/version_util.py rename to bearhub/commons/version_util.py diff --git a/bauh/commons/view_utils.py b/bearhub/commons/view_utils.py similarity index 100% rename from bauh/commons/view_utils.py rename to bearhub/commons/view_utils.py diff --git a/bauh/gems/appimage/__init__.py b/bearhub/gems/appimage/__init__.py similarity index 100% rename from bauh/gems/appimage/__init__.py rename to bearhub/gems/appimage/__init__.py diff --git a/bauh/gems/appimage/config.py b/bearhub/gems/appimage/config.py similarity index 100% rename from bauh/gems/appimage/config.py rename to bearhub/gems/appimage/config.py diff --git a/bauh/gems/appimage/controller.py b/bearhub/gems/appimage/controller.py similarity index 100% rename from bauh/gems/appimage/controller.py rename to bearhub/gems/appimage/controller.py diff --git a/bauh/gems/appimage/model.py b/bearhub/gems/appimage/model.py similarity index 100% rename from bauh/gems/appimage/model.py rename to bearhub/gems/appimage/model.py diff --git a/bauh/gems/appimage/query.py b/bearhub/gems/appimage/query.py similarity index 100% rename from bauh/gems/appimage/query.py rename to bearhub/gems/appimage/query.py diff --git a/bauh/api/__init__.py b/bearhub/gems/appimage/resources/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from bauh/api/__init__.py rename to bearhub/gems/appimage/resources/__init__.py diff --git a/bauh/api/abstract/__init__.py b/bearhub/gems/appimage/resources/img/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from bauh/api/abstract/__init__.py rename to bearhub/gems/appimage/resources/img/__init__.py diff --git a/bauh/gems/appimage/resources/img/appimage.svg b/bearhub/gems/appimage/resources/img/appimage.svg similarity index 100% rename from bauh/gems/appimage/resources/img/appimage.svg rename to bearhub/gems/appimage/resources/img/appimage.svg diff --git a/bauh/gems/appimage/resources/img/refresh.svg b/bearhub/gems/appimage/resources/img/refresh.svg similarity index 100% rename from bauh/gems/appimage/resources/img/refresh.svg rename to bearhub/gems/appimage/resources/img/refresh.svg diff --git a/bauh/commons/__init__.py b/bearhub/gems/appimage/resources/locale/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from bauh/commons/__init__.py rename to bearhub/gems/appimage/resources/locale/__init__.py diff --git a/bauh/gems/appimage/resources/locale/ca b/bearhub/gems/appimage/resources/locale/ca similarity index 100% rename from bauh/gems/appimage/resources/locale/ca rename to bearhub/gems/appimage/resources/locale/ca diff --git a/bauh/gems/appimage/resources/locale/de b/bearhub/gems/appimage/resources/locale/de similarity index 100% rename from bauh/gems/appimage/resources/locale/de rename to bearhub/gems/appimage/resources/locale/de diff --git a/bauh/gems/appimage/resources/locale/en b/bearhub/gems/appimage/resources/locale/en similarity index 100% rename from bauh/gems/appimage/resources/locale/en rename to bearhub/gems/appimage/resources/locale/en diff --git a/bauh/gems/appimage/resources/locale/es b/bearhub/gems/appimage/resources/locale/es similarity index 100% rename from bauh/gems/appimage/resources/locale/es rename to bearhub/gems/appimage/resources/locale/es diff --git a/bauh/gems/appimage/resources/locale/fr b/bearhub/gems/appimage/resources/locale/fr similarity index 100% rename from bauh/gems/appimage/resources/locale/fr rename to bearhub/gems/appimage/resources/locale/fr diff --git a/bauh/gems/appimage/resources/locale/it b/bearhub/gems/appimage/resources/locale/it similarity index 100% rename from bauh/gems/appimage/resources/locale/it rename to bearhub/gems/appimage/resources/locale/it diff --git a/bauh/gems/appimage/resources/locale/pt b/bearhub/gems/appimage/resources/locale/pt similarity index 100% rename from bauh/gems/appimage/resources/locale/pt rename to bearhub/gems/appimage/resources/locale/pt diff --git a/bauh/gems/appimage/resources/locale/ru b/bearhub/gems/appimage/resources/locale/ru similarity index 100% rename from bauh/gems/appimage/resources/locale/ru rename to bearhub/gems/appimage/resources/locale/ru diff --git a/bauh/gems/appimage/resources/locale/tr b/bearhub/gems/appimage/resources/locale/tr similarity index 100% rename from bauh/gems/appimage/resources/locale/tr rename to bearhub/gems/appimage/resources/locale/tr diff --git a/bauh/gems/appimage/resources/locale/zh b/bearhub/gems/appimage/resources/locale/zh similarity index 100% rename from bauh/gems/appimage/resources/locale/zh rename to bearhub/gems/appimage/resources/locale/zh diff --git a/bauh/gems/appimage/util.py b/bearhub/gems/appimage/util.py similarity index 100% rename from bauh/gems/appimage/util.py rename to bearhub/gems/appimage/util.py diff --git a/bauh/gems/appimage/worker.py b/bearhub/gems/appimage/worker.py similarity index 100% rename from bauh/gems/appimage/worker.py rename to bearhub/gems/appimage/worker.py diff --git a/bauh/gems/arch/__init__.py b/bearhub/gems/arch/__init__.py similarity index 100% rename from bauh/gems/arch/__init__.py rename to bearhub/gems/arch/__init__.py diff --git a/bauh/gems/arch/aur.py b/bearhub/gems/arch/aur.py similarity index 100% rename from bauh/gems/arch/aur.py rename to bearhub/gems/arch/aur.py diff --git a/bauh/gems/arch/config.py b/bearhub/gems/arch/config.py similarity index 100% rename from bauh/gems/arch/config.py rename to bearhub/gems/arch/config.py diff --git a/bauh/gems/arch/confirmation.py b/bearhub/gems/arch/confirmation.py similarity index 100% rename from bauh/gems/arch/confirmation.py rename to bearhub/gems/arch/confirmation.py diff --git a/bauh/gems/arch/controller.py b/bearhub/gems/arch/controller.py similarity index 100% rename from bauh/gems/arch/controller.py rename to bearhub/gems/arch/controller.py diff --git a/bauh/gems/arch/cpu_manager.py b/bearhub/gems/arch/cpu_manager.py similarity index 100% rename from bauh/gems/arch/cpu_manager.py rename to bearhub/gems/arch/cpu_manager.py diff --git a/bauh/gems/arch/database.py b/bearhub/gems/arch/database.py similarity index 100% rename from bauh/gems/arch/database.py rename to bearhub/gems/arch/database.py diff --git a/bauh/gems/arch/dependencies.py b/bearhub/gems/arch/dependencies.py similarity index 100% rename from bauh/gems/arch/dependencies.py rename to bearhub/gems/arch/dependencies.py diff --git a/bauh/gems/arch/disk.py b/bearhub/gems/arch/disk.py similarity index 100% rename from bauh/gems/arch/disk.py rename to bearhub/gems/arch/disk.py diff --git a/bauh/gems/arch/download.py b/bearhub/gems/arch/download.py similarity index 100% rename from bauh/gems/arch/download.py rename to bearhub/gems/arch/download.py diff --git a/bauh/gems/arch/exceptions.py b/bearhub/gems/arch/exceptions.py similarity index 100% rename from bauh/gems/arch/exceptions.py rename to bearhub/gems/arch/exceptions.py diff --git a/bauh/gems/arch/git.py b/bearhub/gems/arch/git.py similarity index 100% rename from bauh/gems/arch/git.py rename to bearhub/gems/arch/git.py diff --git a/bauh/gems/arch/gpg.py b/bearhub/gems/arch/gpg.py similarity index 100% rename from bauh/gems/arch/gpg.py rename to bearhub/gems/arch/gpg.py diff --git a/bauh/gems/arch/makepkg.py b/bearhub/gems/arch/makepkg.py similarity index 100% rename from bauh/gems/arch/makepkg.py rename to bearhub/gems/arch/makepkg.py diff --git a/bauh/gems/arch/mapper.py b/bearhub/gems/arch/mapper.py similarity index 100% rename from bauh/gems/arch/mapper.py rename to bearhub/gems/arch/mapper.py diff --git a/bauh/gems/arch/message.py b/bearhub/gems/arch/message.py similarity index 100% rename from bauh/gems/arch/message.py rename to bearhub/gems/arch/message.py diff --git a/bauh/gems/arch/mirrors.py b/bearhub/gems/arch/mirrors.py similarity index 100% rename from bauh/gems/arch/mirrors.py rename to bearhub/gems/arch/mirrors.py diff --git a/bauh/gems/arch/model.py b/bearhub/gems/arch/model.py similarity index 100% rename from bauh/gems/arch/model.py rename to bearhub/gems/arch/model.py diff --git a/bauh/gems/arch/output.py b/bearhub/gems/arch/output.py similarity index 100% rename from bauh/gems/arch/output.py rename to bearhub/gems/arch/output.py diff --git a/bauh/gems/arch/pacman.py b/bearhub/gems/arch/pacman.py similarity index 100% rename from bauh/gems/arch/pacman.py rename to bearhub/gems/arch/pacman.py diff --git a/bauh/gems/arch/pkgbuild.py b/bearhub/gems/arch/pkgbuild.py similarity index 100% rename from bauh/gems/arch/pkgbuild.py rename to bearhub/gems/arch/pkgbuild.py diff --git a/bauh/gems/arch/proc_util.py b/bearhub/gems/arch/proc_util.py similarity index 100% rename from bauh/gems/arch/proc_util.py rename to bearhub/gems/arch/proc_util.py diff --git a/bauh/gems/arch/rebuild_detector.py b/bearhub/gems/arch/rebuild_detector.py similarity index 100% rename from bauh/gems/arch/rebuild_detector.py rename to bearhub/gems/arch/rebuild_detector.py diff --git a/bauh/gems/appimage/resources/__init__.py b/bearhub/gems/arch/resources/__init__.py similarity index 100% rename from bauh/gems/appimage/resources/__init__.py rename to bearhub/gems/arch/resources/__init__.py diff --git a/bauh/gems/appimage/resources/img/__init__.py b/bearhub/gems/arch/resources/img/__init__.py similarity index 100% rename from bauh/gems/appimage/resources/img/__init__.py rename to bearhub/gems/arch/resources/img/__init__.py diff --git a/bauh/gems/arch/resources/img/arch.svg b/bearhub/gems/arch/resources/img/arch.svg similarity index 100% rename from bauh/gems/arch/resources/img/arch.svg rename to bearhub/gems/arch/resources/img/arch.svg diff --git a/bauh/gems/arch/resources/img/build.svg b/bearhub/gems/arch/resources/img/build.svg similarity index 100% rename from bauh/gems/arch/resources/img/build.svg rename to bearhub/gems/arch/resources/img/build.svg diff --git a/bauh/gems/arch/resources/img/check.svg b/bearhub/gems/arch/resources/img/check.svg similarity index 100% rename from bauh/gems/arch/resources/img/check.svg rename to bearhub/gems/arch/resources/img/check.svg diff --git a/bauh/gems/arch/resources/img/check_disabled.svg b/bearhub/gems/arch/resources/img/check_disabled.svg similarity index 100% rename from bauh/gems/arch/resources/img/check_disabled.svg rename to bearhub/gems/arch/resources/img/check_disabled.svg diff --git a/bauh/gems/arch/resources/img/mark_pkgbuild.svg b/bearhub/gems/arch/resources/img/mark_pkgbuild.svg similarity index 100% rename from bauh/gems/arch/resources/img/mark_pkgbuild.svg rename to bearhub/gems/arch/resources/img/mark_pkgbuild.svg diff --git a/bauh/gems/arch/resources/img/repo.svg b/bearhub/gems/arch/resources/img/repo.svg similarity index 100% rename from bauh/gems/arch/resources/img/repo.svg rename to bearhub/gems/arch/resources/img/repo.svg diff --git a/bauh/gems/arch/resources/img/unmark_pkgbuild.svg b/bearhub/gems/arch/resources/img/unmark_pkgbuild.svg similarity index 100% rename from bauh/gems/arch/resources/img/unmark_pkgbuild.svg rename to bearhub/gems/arch/resources/img/unmark_pkgbuild.svg diff --git a/bauh/gems/appimage/resources/locale/__init__.py b/bearhub/gems/arch/resources/locale/__init__.py similarity index 100% rename from bauh/gems/appimage/resources/locale/__init__.py rename to bearhub/gems/arch/resources/locale/__init__.py diff --git a/bauh/gems/arch/resources/locale/ca b/bearhub/gems/arch/resources/locale/ca similarity index 100% rename from bauh/gems/arch/resources/locale/ca rename to bearhub/gems/arch/resources/locale/ca diff --git a/bauh/gems/arch/resources/locale/de b/bearhub/gems/arch/resources/locale/de similarity index 100% rename from bauh/gems/arch/resources/locale/de rename to bearhub/gems/arch/resources/locale/de diff --git a/bauh/gems/arch/resources/locale/en b/bearhub/gems/arch/resources/locale/en similarity index 100% rename from bauh/gems/arch/resources/locale/en rename to bearhub/gems/arch/resources/locale/en diff --git a/bauh/gems/arch/resources/locale/es b/bearhub/gems/arch/resources/locale/es similarity index 100% rename from bauh/gems/arch/resources/locale/es rename to bearhub/gems/arch/resources/locale/es diff --git a/bauh/gems/arch/resources/locale/fr b/bearhub/gems/arch/resources/locale/fr similarity index 100% rename from bauh/gems/arch/resources/locale/fr rename to bearhub/gems/arch/resources/locale/fr diff --git a/bauh/gems/arch/resources/locale/it b/bearhub/gems/arch/resources/locale/it similarity index 100% rename from bauh/gems/arch/resources/locale/it rename to bearhub/gems/arch/resources/locale/it diff --git a/bauh/gems/arch/resources/locale/pt b/bearhub/gems/arch/resources/locale/pt similarity index 100% rename from bauh/gems/arch/resources/locale/pt rename to bearhub/gems/arch/resources/locale/pt diff --git a/bauh/gems/arch/resources/locale/ru b/bearhub/gems/arch/resources/locale/ru similarity index 100% rename from bauh/gems/arch/resources/locale/ru rename to bearhub/gems/arch/resources/locale/ru diff --git a/bauh/gems/arch/resources/locale/tr b/bearhub/gems/arch/resources/locale/tr similarity index 100% rename from bauh/gems/arch/resources/locale/tr rename to bearhub/gems/arch/resources/locale/tr diff --git a/bauh/gems/arch/resources/locale/zh b/bearhub/gems/arch/resources/locale/zh similarity index 100% rename from bauh/gems/arch/resources/locale/zh rename to bearhub/gems/arch/resources/locale/zh diff --git a/bauh/gems/arch/sorting.py b/bearhub/gems/arch/sorting.py similarity index 100% rename from bauh/gems/arch/sorting.py rename to bearhub/gems/arch/sorting.py diff --git a/bauh/gems/arch/sshell.py b/bearhub/gems/arch/sshell.py similarity index 100% rename from bauh/gems/arch/sshell.py rename to bearhub/gems/arch/sshell.py diff --git a/bauh/gems/arch/suggestions.py b/bearhub/gems/arch/suggestions.py similarity index 100% rename from bauh/gems/arch/suggestions.py rename to bearhub/gems/arch/suggestions.py diff --git a/bauh/gems/arch/updates.py b/bearhub/gems/arch/updates.py similarity index 100% rename from bauh/gems/arch/updates.py rename to bearhub/gems/arch/updates.py diff --git a/bauh/gems/arch/worker.py b/bearhub/gems/arch/worker.py similarity index 100% rename from bauh/gems/arch/worker.py rename to bearhub/gems/arch/worker.py diff --git a/bauh/gems/flatpak/__init__.py b/bearhub/gems/flatpak/__init__.py similarity index 100% rename from bauh/gems/flatpak/__init__.py rename to bearhub/gems/flatpak/__init__.py diff --git a/bauh/gems/flatpak/config.py b/bearhub/gems/flatpak/config.py similarity index 100% rename from bauh/gems/flatpak/config.py rename to bearhub/gems/flatpak/config.py diff --git a/bauh/gems/flatpak/constants.py b/bearhub/gems/flatpak/constants.py similarity index 100% rename from bauh/gems/flatpak/constants.py rename to bearhub/gems/flatpak/constants.py diff --git a/bauh/gems/flatpak/controller.py b/bearhub/gems/flatpak/controller.py similarity index 100% rename from bauh/gems/flatpak/controller.py rename to bearhub/gems/flatpak/controller.py diff --git a/bauh/gems/flatpak/flatpak.py b/bearhub/gems/flatpak/flatpak.py similarity index 100% rename from bauh/gems/flatpak/flatpak.py rename to bearhub/gems/flatpak/flatpak.py diff --git a/bauh/gems/flatpak/model.py b/bearhub/gems/flatpak/model.py similarity index 100% rename from bauh/gems/flatpak/model.py rename to bearhub/gems/flatpak/model.py diff --git a/bauh/gems/arch/resources/__init__.py b/bearhub/gems/flatpak/resources/__init__.py similarity index 100% rename from bauh/gems/arch/resources/__init__.py rename to bearhub/gems/flatpak/resources/__init__.py diff --git a/bauh/gems/arch/resources/img/__init__.py b/bearhub/gems/flatpak/resources/img/__init__.py similarity index 100% rename from bauh/gems/arch/resources/img/__init__.py rename to bearhub/gems/flatpak/resources/img/__init__.py diff --git a/bauh/gems/flatpak/resources/img/flatpak.svg b/bearhub/gems/flatpak/resources/img/flatpak.svg similarity index 100% rename from bauh/gems/flatpak/resources/img/flatpak.svg rename to bearhub/gems/flatpak/resources/img/flatpak.svg diff --git a/bauh/gems/arch/resources/locale/__init__.py b/bearhub/gems/flatpak/resources/locale/__init__.py similarity index 100% rename from bauh/gems/arch/resources/locale/__init__.py rename to bearhub/gems/flatpak/resources/locale/__init__.py diff --git a/bauh/gems/flatpak/resources/locale/ca b/bearhub/gems/flatpak/resources/locale/ca similarity index 100% rename from bauh/gems/flatpak/resources/locale/ca rename to bearhub/gems/flatpak/resources/locale/ca diff --git a/bauh/gems/flatpak/resources/locale/de b/bearhub/gems/flatpak/resources/locale/de similarity index 100% rename from bauh/gems/flatpak/resources/locale/de rename to bearhub/gems/flatpak/resources/locale/de diff --git a/bauh/gems/flatpak/resources/locale/en b/bearhub/gems/flatpak/resources/locale/en similarity index 100% rename from bauh/gems/flatpak/resources/locale/en rename to bearhub/gems/flatpak/resources/locale/en diff --git a/bauh/gems/flatpak/resources/locale/es b/bearhub/gems/flatpak/resources/locale/es similarity index 100% rename from bauh/gems/flatpak/resources/locale/es rename to bearhub/gems/flatpak/resources/locale/es diff --git a/bauh/gems/flatpak/resources/locale/fr b/bearhub/gems/flatpak/resources/locale/fr similarity index 100% rename from bauh/gems/flatpak/resources/locale/fr rename to bearhub/gems/flatpak/resources/locale/fr diff --git a/bauh/gems/flatpak/resources/locale/it b/bearhub/gems/flatpak/resources/locale/it similarity index 100% rename from bauh/gems/flatpak/resources/locale/it rename to bearhub/gems/flatpak/resources/locale/it diff --git a/bauh/gems/flatpak/resources/locale/pt b/bearhub/gems/flatpak/resources/locale/pt similarity index 100% rename from bauh/gems/flatpak/resources/locale/pt rename to bearhub/gems/flatpak/resources/locale/pt diff --git a/bauh/gems/flatpak/resources/locale/ru b/bearhub/gems/flatpak/resources/locale/ru similarity index 100% rename from bauh/gems/flatpak/resources/locale/ru rename to bearhub/gems/flatpak/resources/locale/ru diff --git a/bauh/gems/flatpak/resources/locale/tr b/bearhub/gems/flatpak/resources/locale/tr similarity index 100% rename from bauh/gems/flatpak/resources/locale/tr rename to bearhub/gems/flatpak/resources/locale/tr diff --git a/bauh/gems/flatpak/resources/locale/zh b/bearhub/gems/flatpak/resources/locale/zh similarity index 100% rename from bauh/gems/flatpak/resources/locale/zh rename to bearhub/gems/flatpak/resources/locale/zh diff --git a/bauh/gems/flatpak/worker.py b/bearhub/gems/flatpak/worker.py similarity index 100% rename from bauh/gems/flatpak/worker.py rename to bearhub/gems/flatpak/worker.py diff --git a/bauh/gems/web/__init__.py b/bearhub/gems/web/__init__.py similarity index 100% rename from bauh/gems/web/__init__.py rename to bearhub/gems/web/__init__.py diff --git a/bauh/gems/web/config.py b/bearhub/gems/web/config.py similarity index 100% rename from bauh/gems/web/config.py rename to bearhub/gems/web/config.py diff --git a/bauh/gems/web/controller.py b/bearhub/gems/web/controller.py similarity index 100% rename from bauh/gems/web/controller.py rename to bearhub/gems/web/controller.py diff --git a/bauh/gems/web/environment.py b/bearhub/gems/web/environment.py similarity index 100% rename from bauh/gems/web/environment.py rename to bearhub/gems/web/environment.py diff --git a/bauh/gems/web/model.py b/bearhub/gems/web/model.py similarity index 100% rename from bauh/gems/web/model.py rename to bearhub/gems/web/model.py diff --git a/bauh/gems/web/nativefier.py b/bearhub/gems/web/nativefier.py similarity index 100% rename from bauh/gems/web/nativefier.py rename to bearhub/gems/web/nativefier.py diff --git a/bauh/gems/flatpak/resources/__init__.py b/bearhub/gems/web/resources/__init__.py similarity index 100% rename from bauh/gems/flatpak/resources/__init__.py rename to bearhub/gems/web/resources/__init__.py diff --git a/bauh/gems/flatpak/resources/img/__init__.py b/bearhub/gems/web/resources/img/__init__.py similarity index 100% rename from bauh/gems/flatpak/resources/img/__init__.py rename to bearhub/gems/web/resources/img/__init__.py diff --git a/bauh/gems/web/resources/img/web.svg b/bearhub/gems/web/resources/img/web.svg similarity index 100% rename from bauh/gems/web/resources/img/web.svg rename to bearhub/gems/web/resources/img/web.svg diff --git a/bauh/gems/flatpak/resources/locale/__init__.py b/bearhub/gems/web/resources/locale/__init__.py similarity index 100% rename from bauh/gems/flatpak/resources/locale/__init__.py rename to bearhub/gems/web/resources/locale/__init__.py diff --git a/bauh/gems/web/resources/locale/ca b/bearhub/gems/web/resources/locale/ca similarity index 100% rename from bauh/gems/web/resources/locale/ca rename to bearhub/gems/web/resources/locale/ca diff --git a/bauh/gems/web/resources/locale/de b/bearhub/gems/web/resources/locale/de similarity index 100% rename from bauh/gems/web/resources/locale/de rename to bearhub/gems/web/resources/locale/de diff --git a/bauh/gems/web/resources/locale/en b/bearhub/gems/web/resources/locale/en similarity index 100% rename from bauh/gems/web/resources/locale/en rename to bearhub/gems/web/resources/locale/en diff --git a/bauh/gems/web/resources/locale/es b/bearhub/gems/web/resources/locale/es similarity index 100% rename from bauh/gems/web/resources/locale/es rename to bearhub/gems/web/resources/locale/es diff --git a/bauh/gems/web/resources/locale/fr b/bearhub/gems/web/resources/locale/fr similarity index 100% rename from bauh/gems/web/resources/locale/fr rename to bearhub/gems/web/resources/locale/fr diff --git a/bauh/gems/web/resources/locale/it b/bearhub/gems/web/resources/locale/it similarity index 100% rename from bauh/gems/web/resources/locale/it rename to bearhub/gems/web/resources/locale/it diff --git a/bauh/gems/web/resources/locale/pt b/bearhub/gems/web/resources/locale/pt similarity index 100% rename from bauh/gems/web/resources/locale/pt rename to bearhub/gems/web/resources/locale/pt diff --git a/bauh/gems/web/resources/locale/ru b/bearhub/gems/web/resources/locale/ru similarity index 100% rename from bauh/gems/web/resources/locale/ru rename to bearhub/gems/web/resources/locale/ru diff --git a/bauh/gems/web/resources/locale/tr b/bearhub/gems/web/resources/locale/tr similarity index 100% rename from bauh/gems/web/resources/locale/tr rename to bearhub/gems/web/resources/locale/tr diff --git a/bauh/gems/web/resources/locale/zh b/bearhub/gems/web/resources/locale/zh similarity index 100% rename from bauh/gems/web/resources/locale/zh rename to bearhub/gems/web/resources/locale/zh diff --git a/bauh/gems/web/search.py b/bearhub/gems/web/search.py similarity index 100% rename from bauh/gems/web/search.py rename to bearhub/gems/web/search.py diff --git a/bauh/gems/web/suggestions.py b/bearhub/gems/web/suggestions.py similarity index 100% rename from bauh/gems/web/suggestions.py rename to bearhub/gems/web/suggestions.py diff --git a/bauh/gems/web/worker.py b/bearhub/gems/web/worker.py similarity index 100% rename from bauh/gems/web/worker.py rename to bearhub/gems/web/worker.py diff --git a/bearhub/view/core/controller.py b/bearhub/view/core/controller.py old mode 100644 new mode 100755 index df532ad5..4da5652b --- a/bearhub/view/core/controller.py +++ b/bearhub/view/core/controller.py @@ -1,2 +1,702 @@ -from bauh.view.core.controller import * # noqa: F401,F403 +import shutil +import time +import traceback +from subprocess import Popen, STDOUT +from threading import Thread +from typing import List, Set, Type, Tuple, Dict, Optional, Generator, Callable +from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ + UpgradeRequirement, TransactionResult, SoftwareAction, SettingsView, SettingsController +from bauh.api.abstract.disk import DiskCacheLoader +from bauh.api.abstract.handler import ProcessWatcher, TaskManager +from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \ + CustomSoftwareAction +from bauh.api.abstract.view import TabGroupComponent, MessageType +from bauh.api.exception import NoInternetException +from bauh.commons.boot import CreateConfigFile +from bauh.commons.html import bold +from bauh.commons.regex import RE_URL +from bauh.commons.util import sanitize_command_input +from bauh.view.core.config import CoreConfigManager +from bauh.view.core.settings import GenericSettingsManager +from bauh.view.core.update import check_for_update +from bauh.view.util import resource +from bauh.view.util.resource import get_path +from bauh.view.util.util import clean_app_files, restart_app + + +class GenericUpgradeRequirements(UpgradeRequirements): + + def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement], + to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[SoftwarePackage], + sub_requirements: Dict[SoftwareManager, UpgradeRequirements]): + super(GenericUpgradeRequirements, self).__init__(to_install=to_install, to_upgrade=to_upgrade, + to_remove=to_remove, cannot_upgrade=cannot_upgrade) + self.sub_requirements = sub_requirements + + +class GenericSoftwareManager(SoftwareManager, SettingsController): + + def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict, + force_suggestions: bool = False): + + super(GenericSoftwareManager, self).__init__(context=context) + self.managers = managers + self.map = {t: m for m in self.managers for t in m.get_managed_types()} + self._available_cache = {} if config['system']['single_dependency_checking'] else None + self.thread_prepare = None + self.i18n = context.i18n + self.disk_loader_factory = context.disk_loader_factory + self.logger = context.logger + self._already_prepared = [] + self.working_managers = [] + self.config = config + self.settings_manager: Optional[GenericSettingsManager] = None + self.http_client = context.http_client + self.configman = CoreConfigManager() + self._action_reset: Optional[CustomSoftwareAction] = None + self._dynamic_extra_actions: Optional[Dict[CustomSoftwareAction, Callable[[dict], bool]]] = None + self.force_suggestions = force_suggestions + + @property + def dynamic_extra_actions(self) -> Dict[CustomSoftwareAction, Callable[[dict], bool]]: + if self._dynamic_extra_actions is None: + self._dynamic_extra_actions = { + CustomSoftwareAction(i18n_label_key='action.backups', + i18n_status_key='action.backups.status', + i18n_description_key='action.backups.desc', + manager_method='launch_timeshift', + manager=self, + icon_path='timeshift', + requires_root=False, + refresh=False): self.is_backups_action_available + } + + return self._dynamic_extra_actions + + @property + def action_reset(self) -> CustomSoftwareAction: + if self._action_reset is None: + self._action_reset = CustomSoftwareAction(i18n_label_key='action.reset', + i18n_status_key='action.reset.status', + i18n_description_key='action.reset.desc', + manager_method='reset', + icon_path=resource.get_path('img/logo.svg'), + requires_root=False, + manager=self, + refresh=False) + + return self._action_reset + + def _is_timeshift_launcher_available(self) -> bool: + return bool(shutil.which('timeshift-launcher')) + + def is_backups_action_available(self, app_config: dict) -> bool: + return bool(app_config['backup']['enabled']) and self._is_timeshift_launcher_available() + + def reset_cache(self): + if self._available_cache is not None: + self._available_cache = {} + self.working_managers.clear() + + def launch_timeshift(self, root_password: Optional[str], watcher: ProcessWatcher): + if self._is_timeshift_launcher_available(): + try: + Popen(['timeshift-launcher'], stderr=STDOUT) + return True + except Exception: + traceback.print_exc() + watcher.show_message(title=self.i18n["error"].capitalize(), + body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')), + type_=MessageType.ERROR) + return False + else: + watcher.show_message(title=self.i18n["error"].capitalize(), + body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')), + type_=MessageType.ERROR) + return False + + def _can_work(self, man: SoftwareManager): + if self._available_cache is not None: + available = False + for t in man.get_managed_types(): + available = self._available_cache.get(t) + + if available is None: + available = man.is_enabled() and man.can_work()[0] + self._available_cache[t] = available + + if available: + available = True + else: + available = man.is_enabled() and man.can_work()[0] + + if available: + if man not in self.working_managers: + self.working_managers.append(man) + else: + if man in self.working_managers: + self.working_managers.remove(man) + + return available + + def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult): + mti = time.time() + apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1) + mtf = time.time() + self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds') + + res.installed.extend(apps_found.installed) + res.new.extend(apps_found.new) + + def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = -1, is_url: bool = False) -> SearchResult: + ti = time.time() + self._wait_to_be_ready() + + res = SearchResult.empty() + + if self.context.is_internet_available(): + norm_query = sanitize_command_input(words).lower() + self.logger.info(f"Search query: {norm_query}") + + if norm_query: + is_url = bool(RE_URL.match(norm_query)) + disk_loader = self.disk_loader_factory.new() + disk_loader.start() + + threads = [] + + for man in self.managers: + if self._can_work(man): + t = Thread(target=self._search, args=(norm_query, is_url, man, disk_loader, res), daemon=True) + t.start() + threads.append(t) + + for t in threads: + t.join() + + if disk_loader: + disk_loader.stop_working() + disk_loader.join() + else: + raise NoInternetException() + + res.update_total() + tf = time.time() + self.logger.info(f'Took {tf - ti:.8f} seconds') + return res + + def _wait_to_be_ready(self): + if self.thread_prepare: + self.thread_prepare.join() + self.thread_prepare = None + + def set_enabled(self, enabled: bool): + pass + + def can_work(self) -> Tuple[bool, Optional[str]]: + return True, None + + def _get_package_lower_name(self, pkg: SoftwarePackage): + return pkg.name.lower() + + def _fill_read_installed(self, man: SoftwareManager, disk_loader: DiskCacheLoader, internet_available: bool, + output: List[SearchResult]): + mti = time.time() + man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available, + limit=-1, only_apps=False) + mtf = time.time() + self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.4f} seconds') + output.append(man_res) + + def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: + ti = time.time() + self._wait_to_be_ready() + + res = SearchResult([], None, 0) + + disk_loader = None + + net_available = self.context.is_internet_available() + read_threads = list() + results = list() + + if not pkg_types: # any type + for man in self.managers: + if self._can_work(man): + if not disk_loader: + disk_loader = self.disk_loader_factory.new() + disk_loader.start() + + t = Thread(target=self._fill_read_installed, + args=(man, disk_loader, net_available, results), + daemon=True) + t.start() + read_threads.append(t) + else: + man_already_used = [] + + for t in pkg_types: + man = self.map.get(t) + if man and (man not in man_already_used) and self._can_work(man): + + if not disk_loader: + disk_loader = self.disk_loader_factory.new() + disk_loader.start() + + t = Thread(target=self._fill_read_installed, + args=(man, disk_loader, net_available, results), + daemon=True) + t.start() + read_threads.append(t) + + for t in read_threads: + t.join() + + if disk_loader: + disk_loader.stop_working() + disk_loader.join() + + for result in results: + if result.installed: + res.installed.extend(result.installed) + res.total += result.total + + if res.installed: + for p in res.installed: + if p.is_update_ignored(): + if p.categories is None: + p.categories = ['updates_ignored'] + elif 'updates_ignored' not in p.categories: + self._add_category(p, 'updates_ignored') + + res.installed.sort(key=self._get_package_lower_name) + + tf = time.time() + self.logger.info(f'Took {tf - ti:.2f} seconds') + return res + + def _add_category(self, pkg: SoftwarePackage, category: str): + if isinstance(pkg.categories, tuple): + pkg.categories = tuple((*pkg.categories, category)) + elif isinstance(pkg.categories, list): + pkg.categories.append(category) + elif isinstance(pkg.categories, set): + pkg.categories.add(category) + + def downgrade(self, app: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher) -> bool: + man = self._get_manager_for(app) + + if man and app.can_be_downgraded(): + mti = time.time() + res = man.downgrade(app, root_password, handler) + mtf = time.time() + self.logger.info(f'Took {mtf - mti:.2f} seconds') + return res + else: + raise Exception(f"Downgrading is not possible for {app.__class__.__name__}") + + def clean_cache_for(self, app: SoftwarePackage): + man = self._get_manager_for(app) + + if man: + return man.clean_cache_for(app) + + def upgrade(self, requirements: GenericUpgradeRequirements, root_password: Optional[str], handler: ProcessWatcher) -> bool: + for man, man_reqs in requirements.sub_requirements.items(): + res = man.upgrade(man_reqs, root_password, handler) + + if not res: + return False + + return True + + def _fill_post_transaction_status(self, pkg: SoftwarePackage, installed: bool): + pkg.installed = installed + pkg.update = False + + if pkg.latest_version: + pkg.version = pkg.latest_version + + def _update_post_transaction_status(self, res: TransactionResult): + if res.success: + if res.installed: + for p in res.installed: + self._fill_post_transaction_status(p, True) + if res.removed: + for p in res.removed: + self._fill_post_transaction_status(p, False) + + def uninstall(self, pkg: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher, disk_loader: DiskCacheLoader = None) -> TransactionResult: + man = self._get_manager_for(pkg) + + if man: + ti = time.time() + disk_loader = self.disk_loader_factory.new() + disk_loader.start() + self.logger.info(f"Uninstalling {pkg.name}") + try: + res = man.uninstall(pkg, root_password, handler, disk_loader) + disk_loader.stop_working() + disk_loader.join() + self._update_post_transaction_status(res) + return res + except Exception: + traceback.print_exc() + return TransactionResult(success=False, installed=[], removed=[]) + finally: + tf = time.time() + self.logger.info(f'Uninstallation of {pkg} took {(tf - ti) / 60:.2f} minutes') + + def install(self, app: SoftwarePackage, root_password: Optional[str], disk_loader: DiskCacheLoader, handler: ProcessWatcher) -> TransactionResult: + man = self._get_manager_for(app) + + if man: + ti = time.time() + disk_loader = self.disk_loader_factory.new() + disk_loader.start() + try: + self.logger.info(f'Installing {app}') + res = man.install(app, root_password, disk_loader, handler) + disk_loader.stop_working() + disk_loader.join() + self._update_post_transaction_status(res) + return res + except Exception: + traceback.print_exc() + return TransactionResult(success=False, installed=[], removed=[]) + finally: + tf = time.time() + self.logger.info(f'Installation of {app} took {(tf - ti) / 60:.2f} minutes') + + def get_info(self, app: SoftwarePackage): + man = self._get_manager_for(app) + + if man: + return man.get_info(app) + + def get_history(self, app: SoftwarePackage) -> PackageHistory: + man = self._get_manager_for(app) + + if man: + mti = time.time() + history = man.get_history(app) + mtf = time.time() + self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.2f} seconds') + return history + + def get_managed_types(self) -> Set[Type[SoftwarePackage]]: + available_types = set() + + for man in self.get_working_managers(): + available_types.update(man.get_managed_types()) + + return available_types + + def is_enabled(self): + return True + + def _get_manager_for(self, app: SoftwarePackage) -> SoftwareManager: + man = self.map[app.__class__] + return man if man and self._can_work(man) else None + + def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool): + if pkg.supports_disk_cache(): + man = self._get_manager_for(pkg) + + if man: + return man.cache_to_disk(pkg, icon_bytes=icon_bytes, only_icon=only_icon) + + def requires_root(self, action: SoftwareAction, app: SoftwarePackage) -> bool: + if app is None: + if self.managers: + for man in self.managers: + if self._can_work(man): + if man.requires_root(action, app): + return True + return False + else: + man = self._get_manager_for(app) + + if man: + return man.requires_root(action, app) + + def prepare(self, task_manager: TaskManager, root_password: Optional[str], internet_available: bool): + ti = time.time() + self.logger.info("Initializing") + taskman = task_manager if task_manager else TaskManager() # empty task manager to prevent null pointers + + create_config = CreateConfigFile(taskman=taskman, configman=self.configman, i18n=self.i18n, + task_icon_path=get_path('img/logo.svg'), logger=self.logger) + create_config.start() + + if self.managers: + internet_on = self.context.is_internet_available() + prepare_tasks = [] + for man in self.managers: + if man not in self._already_prepared and self._can_work(man): + t = Thread(target=man.prepare, args=(taskman, root_password, internet_on), daemon=True) + t.start() + prepare_tasks.append(t) + self._already_prepared.append(man) + + for t in prepare_tasks: + t.join() + + create_config.join() + tf = time.time() + self.logger.info(f'Finished ({tf - ti:.2f} seconds)') + + def cache_available_managers(self): + if self.managers: + for man in self.managers: + self._can_work(man) + + def list_updates(self, internet_available: bool = None) -> List[PackageUpdate]: + self._wait_to_be_ready() + + updates = [] + + if self.managers: + net_available = self.context.is_internet_available() + + for man in self.managers: + if self._can_work(man): + man_updates = man.list_updates(internet_available=net_available) + if man_updates: + updates.extend(man_updates) + + return updates + + def list_warnings(self, internet_available: bool = None) -> List[str]: + warnings = [] + + int_available = self.context.is_internet_available() + + if int_available: + updates_msg = check_for_update(self.logger, self.http_client, self.i18n) + + if updates_msg: + warnings.append(updates_msg) + + if self.managers: + for man in self.managers: + if self._can_work(man): + man_warnings = man.list_warnings(internet_available=int_available) + + if man_warnings: + warnings.extend(man_warnings) + + return warnings + + def _fill_suggestions(self, suggestions: list, man: SoftwareManager, limit: int, filter_installed: bool): + if self._can_work(man): + mti = time.time() + man_sugs = man.list_suggestions(limit=limit, filter_installed=filter_installed) + mtf = time.time() + self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.5f} seconds') + + if man_sugs: + if 0 < limit < len(man_sugs): + man_sugs = man_sugs[0:limit] + + suggestions.extend(man_sugs) + + def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: + if self.force_suggestions or bool(self.config['suggestions']['enabled']): + if self.managers and self.context.is_internet_available(): + suggestions, threads = [], [] + for man in self.managers: + t = Thread(target=self._fill_suggestions, + args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed), + daemon=True) + t.start() + threads.append(t) + + for t in threads: + t.join() + + if suggestions: + suggestions.sort(key=lambda s: s.priority.value, reverse=True) + + return suggestions + return [] + + def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[str], watcher: ProcessWatcher): + if action.requires_internet and not self.context.is_internet_available(): + raise NoInternetException() + + man = action.manager if action.manager else self._get_manager_for(pkg) + + if man: + return eval(f"man.{action.manager_method}({'pkg=pkg, ' if pkg else ''}root_password=root_password, watcher=watcher)") + + def is_default_enabled(self) -> bool: + return True + + def launch(self, pkg: SoftwarePackage): + self._wait_to_be_ready() + + man = self._get_manager_for(pkg) + + if man: + self.logger.info(f'Launching {pkg}') + man.launch(pkg) + + def get_screenshots(self, pkg: SoftwarePackage) -> Generator[str, None, None]: + man = self._get_manager_for(pkg) + + if man: + yield from man.get_screenshots(pkg) + + def get_working_managers(self): + return [m for m in self.managers if self._can_work(m)] + + def get_settings(self) -> Optional[Generator[SettingsView, None, None]]: + if self.settings_manager is None: + self.settings_manager = GenericSettingsManager(managers=self.managers, + working_managers=self.working_managers, + configman=self.configman, + context=self.context) + else: + self.settings_manager.managers = self.managers + self.settings_manager.working_managers = self.working_managers + + yield SettingsView(self, self.settings_manager.get_settings()) + + def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[List[str]]]: + return self.settings_manager.save_settings(component) + + def _map_pkgs_by_manager(self, pkgs: List[SoftwarePackage], pkg_filters: list = None) -> Dict[SoftwareManager, List[SoftwarePackage]]: + by_manager = {} + for pkg in pkgs: + if pkg_filters and not all((1 for f in pkg_filters if f(pkg))): + continue + + man = self._get_manager_for(pkg) + + if man: + man_pkgs = by_manager.get(man) + + if man_pkgs is None: + man_pkgs = [] + by_manager[man] = man_pkgs + + man_pkgs.append(pkg) + + return by_manager + + def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: Optional[str], watcher: ProcessWatcher) -> UpgradeRequirements: + by_manager = self._map_pkgs_by_manager(pkgs) + res = GenericUpgradeRequirements([], [], [], [], {}) + + if by_manager: + for man, pkgs in by_manager.items(): + ti = time.time() + man_reqs = man.get_upgrade_requirements(pkgs, root_password, watcher) + tf = time.time() + self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds') + + if not man_reqs: + return # it means the process should be stopped + + if man_reqs: + res.sub_requirements[man] = man_reqs + if man_reqs.to_install: + res.to_install.extend(man_reqs.to_install) + + if man_reqs.to_remove: + res.to_remove.extend(man_reqs.to_remove) + + if man_reqs.to_upgrade: + res.to_upgrade.extend(man_reqs.to_upgrade) + + if man_reqs.cannot_upgrade: + res.cannot_upgrade.extend(man_reqs.cannot_upgrade) + + return res + + def reset(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool: + body = f"{self.i18n['action.reset.body_1'].format(bold(self.context.app_name))}
" \ + f"{self.i18n['action.reset.body_2']}
" + + if watcher.request_confirmation(title=self.i18n['action.reset'], + body=body, + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + + try: + clean_app_files(managers=self.managers, logs=False) + restart_app() + except Exception: + return False + + return True + + def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]: + if self.managers: + working_managers = [] + + for man in self.managers: + if self._can_work(man): + working_managers.append(man) + + if working_managers: + working_managers.sort(key=lambda m: m.__class__.__name__) + + for man in working_managers: + for action in man.gen_custom_actions(): + action.manager = man + yield action + + app_config = self.configman.get_config() + + for action, available in self.dynamic_extra_actions.items(): + if available(app_config): + yield action + + yield self.action_reset + + def _fill_sizes(self, man: SoftwareManager, pkgs: List[SoftwarePackage]): + ti = time.time() + man.fill_sizes(pkgs) + tf = time.time() + self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds') + + def fill_sizes(self, pkgs: List[SoftwarePackage]): + by_manager = self._map_pkgs_by_manager(pkgs, pkg_filters=[lambda p: p.size is None]) + + if by_manager: + threads = [] + for man, man_pkgs in by_manager.items(): + if man_pkgs: + t = Thread(target=self._fill_sizes, args=(man, man_pkgs), daemon=True) + t.start() + threads.append(t) + + for t in threads: + t.join() + + def ignore_update(self, pkg: SoftwarePackage): + manager = self._get_manager_for(pkg) + + if manager: + manager.ignore_update(pkg) + + if pkg.is_update_ignored(): + if pkg.categories is None: + pkg.categories = ['updates_ignored'] + elif 'updates_ignored' not in pkg.categories: + self._add_category(pkg, 'updates_ignored') + + def revert_ignored_update(self, pkg: SoftwarePackage): + manager = self._get_manager_for(pkg) + + if manager: + manager.revert_ignored_update(pkg) + + if not pkg.is_update_ignored() and pkg.categories and 'updates_ignored' in pkg.categories: + if isinstance(pkg.categories, tuple): + pkg.categories = tuple(c for c in pkg.categories if c != 'updates_ignored') + else: + pkg.categories.remove('updates_ignored') diff --git a/bearhub/view/core/downloader.py b/bearhub/view/core/downloader.py index d6817973..982d12e4 100644 --- a/bearhub/view/core/downloader.py +++ b/bearhub/view/core/downloader.py @@ -1,2 +1,328 @@ -from bauh.view.core.downloader import * # noqa: F401,F403 +import os +import re +import shutil +import time +import traceback +from io import StringIO, BytesIO +from logging import Logger +from math import floor +from pathlib import Path +from threading import Thread +from typing import Optional, Tuple +from bauh.api.abstract.download import FileDownloader +from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.http import HttpClient +from bauh.commons.html import bold +from bauh.commons.system import ProcessHandler, SimpleProcess +from bauh.commons.view_utils import get_human_size_str +from bauh.view.util.translation import I18n + +RE_HAS_EXTENSION = re.compile(r'.+\.\w+$') + + +class SelfFileDownloader(FileDownloader): + + def __init__(self, logger: Logger, i18n: I18n, http_client: HttpClient, + check_ssl: bool): + self._logger = logger + self._i18n = i18n + self._client = http_client + self._ssl = check_ssl + + def is_multithreaded(self) -> bool: + return False + + def can_work(self) -> bool: + return True + + def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: + return tuple() + + def is_multithreaded_client_available(self, name: str) -> bool: + return False + + def list_available_multithreaded_clients(self) -> Tuple[str, ...]: + return tuple() + + def get_supported_clients(self) -> Tuple[str, ...]: + return tuple() + + def download(self, file_url: str, watcher: Optional[ProcessWatcher], output_path: str, cwd: str, + root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, + max_threads: int = None, known_size: int = None) -> bool: + try: + res = self._client.get(url=file_url, ignore_ssl=not self._ssl, stream=True) + except Exception: + return False + + try: + content_length = int(res.headers.get("content-length", 0)) + except Exception: + content_length = 0 + self._logger.warning(f"Could not retrieve the content-length for file '{file_url}'") + + file_name = file_url.split("/")[-1] + msg = StringIO() + msg.write(f"{substatus_prefix} " if substatus_prefix else "") + msg.write(f"{self._i18n['downloading']} {bold(file_name)}") + base_msg = msg.getvalue() + + byte_stream = BytesIO() + total_downloaded = 0 + known_size = content_length and content_length > 0 + total_size_str = get_human_size_str(content_length) if known_size > 0 else "?" + + try: + for data in res.iter_content(chunk_size=1024): + byte_stream.write(data) + total_downloaded += len(data) + perc = f"({(total_downloaded / content_length) * 100:.2f}%) " if known_size > 0 else "" + watcher.change_substatus(f"{perc}{base_msg} ({get_human_size_str(total_downloaded)} / {total_size_str})") + except Exception: + self._logger.error(f"Unexpected exception while downloading file from '{file_url}'") + traceback.print_exc() + return False + + self._logger.info(f"Writing downloaded file content to disk: {output_path}") + + try: + with open(output_path, "wb+") as f: + f.write(byte_stream.getvalue()) + except Exception: + self._logger.error(f"Unexpected exception when saving downloaded content to disk: {output_path}") + traceback.print_exc() + return False + + return True + + +class AdaptableFileDownloader(FileDownloader): + + def __init__(self, logger: Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient, + multithread_client: str, check_ssl: bool): + self.logger = logger + self.multithread_enabled = multithread_enabled + self.i18n = i18n + self.http_client = http_client + self.supported_multithread_clients = ("aria2", "axel") + self.multithread_client = multithread_client + self.check_ssl = check_ssl + self._self_downloader = SelfFileDownloader(logger=logger, + i18n=i18n, + http_client=http_client, + check_ssl=check_ssl) + + @staticmethod + def is_aria2c_available() -> bool: + return bool(shutil.which('aria2c')) + + @staticmethod + def is_axel_available() -> bool: + return bool(shutil.which('axel')) + + def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess: + cmd = ['aria2c', url, + '--no-conf', + '-x', '16', + '--enable-color=false', + '--stderr=true', + '--summary-interval=0', + '--disable-ipv6', + '-k', '1M', + '--allow-overwrite=true', + '-c', + '-t', '5', + '--max-file-not-found=3', + '--file-allocation=none', + '--console-log-level=error'] + + if threads > 1: + cmd.append('-s') + cmd.append(str(threads)) + + if output_path: + output_split = output_path.split('/') + cmd.append('-d') + cmd.append('/'.join(output_split[:-1])) + cmd.append('-o') + cmd.append(output_split[-1]) + + return SimpleProcess(cmd=cmd, root_password=root_password, cwd=cwd) + + def _get_axel_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess: + cmd = ['axel', url, '-n', str(threads), '-4', '-c', '-T', '5'] + + if not self.check_ssl: + cmd.append('-k') + + if output_path: + cmd.append(f'--output={output_path}') + + return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password) + + def _rm_bad_file(self, file_name: str, output_path: str, cwd, handler: ProcessHandler, root_password: Optional[str]): + to_delete = output_path if output_path else f'{cwd}/{file_name}' + + if to_delete and os.path.exists(to_delete): + self.logger.info(f'Removing downloaded file {to_delete}') + success, _ = handler.handle_simple(SimpleProcess(['rm', '-rf', to_delete], root_password=root_password)) + return success + + def _concat_file_size(self, file_url: str, base_substatus: StringIO, watcher: ProcessWatcher): + watcher.change_substatus(f'{base_substatus.getvalue()} ( ? Mb )') + + try: + size = self.http_client.get_content_length(file_url) + + if size: + base_substatus.write(f' ( {size} )') + watcher.change_substatus(base_substatus.getvalue()) + except Exception: + pass + + def _get_appropriate_threads_number(self, max_threads: int, known_size: int) -> int: + if max_threads and max_threads > 0: + threads = max_threads + elif known_size: + threads = 16 if known_size >= 16000000 else floor(known_size / 1000000) + + if threads <= 0: + threads = 1 + else: + threads = 16 + + return threads + + def _download_with_threads(self, client: str, file_url: str, output_path: str, cwd: str, + max_threads: int, known_size: int, display_file_size: bool, handler: ProcessHandler, + root_password: Optional[str] = None, substatus_prefix: Optional[str] = None) \ + -> Tuple[float, bool]: + + threads = self._get_appropriate_threads_number(max_threads, known_size) + + if client == 'aria2': + start_time = time.time() + process = self._get_aria2c_process(file_url, output_path, cwd, root_password, threads) + downloader = 'aria2' + else: + start_time = time.time() + process = self._get_axel_process(file_url, output_path, cwd, root_password, threads) + downloader = 'axel' + + name = file_url.split('/')[-1] + + if output_path and not RE_HAS_EXTENSION.match(name) and RE_HAS_EXTENSION.match(output_path): + name = output_path.split('/')[-1] + + if handler.watcher: + msg = StringIO() + msg.write(f'{substatus_prefix} ' if substatus_prefix else '') + msg.write(f"{bold('[{}]'.format(downloader))} {self.i18n['downloading']} {bold(name)}") + + if display_file_size: + if known_size: + msg.write(f' ( {get_human_size_str(known_size)} )') + handler.watcher.change_substatus(msg.getvalue()) + else: + Thread(target=self._concat_file_size, args=(file_url, msg, handler.watcher), daemon=True).start() + else: + msg.write(' ( ? Mb )') + handler.watcher.change_substatus(msg.getvalue()) + + success, _ = handler.handle_simple(process) + return start_time, success + + def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = None, cwd: str = None, root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool: + self.logger.info(f'Downloading {file_url}') + handler = ProcessHandler(watcher) + file_name = file_url.split('/')[-1] + + final_cwd = cwd if cwd else '.' + + success = False + start_time = time.time() + try: + if output_path: + if os.path.exists(output_path): + self.logger.info(f'Removing old file found before downloading: {output_path}') + os.remove(output_path) + self.logger.info(f'Old file {output_path} removed') + else: + output_dir = os.path.dirname(output_path) + + try: + Path(output_dir).mkdir(exist_ok=True, parents=True) + except OSError: + self.logger.error(f"Could not make download directory '{output_dir}'") + watcher.print(self.i18n['error.mkdir'].format(dir=output_dir)) + return False + + threaded_client = self.get_available_multithreaded_tool() + if threaded_client: + start_time, success = self._download_with_threads(client=threaded_client, file_url=file_url, + output_path=output_path, + cwd=final_cwd, max_threads=max_threads, + known_size=known_size, handler=handler, + display_file_size=display_file_size, + root_password=root_password) + else: + start_time = time.time() + success = self._self_downloader.download(file_url=file_url, watcher=watcher, output_path=output_path, + cwd=cwd, root_password=root_password, + substatus_prefix=substatus_prefix, + display_file_size=display_file_size, max_threads=max_threads, + known_size=known_size) + except Exception: + traceback.print_exc() + self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password) + + final_time = time.time() + self.logger.info(f'{file_name} download took {(final_time - start_time) / 60:.4f} minutes') + + if not success: + self.logger.error(f"Could not download '{file_name}'") + self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password) + + return success + + def is_multithreaded(self) -> bool: + return bool(self.get_available_multithreaded_tool()) + + def get_available_multithreaded_tool(self) -> str: + if self.multithread_enabled: + if self.multithread_client is None or self.multithread_client not in self.supported_multithread_clients: + for client in self.supported_multithread_clients: + if self.is_multithreaded_client_available(client): + return client + else: + possible_clients = {*self.supported_multithread_clients} + + if self.is_multithreaded_client_available(self.multithread_client): + return self.multithread_client + else: + possible_clients.remove(self.multithread_client) + + for client in possible_clients: + if self.is_multithreaded_client_available(client): + return client + + def can_work(self) -> bool: + return True + + def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: + return self.supported_multithread_clients + + def is_multithreaded_client_available(self, name: str) -> bool: + if name == 'aria2': + return self.is_aria2c_available() + elif name == 'axel': + return self.is_axel_available() + else: + return False + + def list_available_multithreaded_clients(self) -> Tuple[str, ...]: + return tuple(c for c in self.supported_multithread_clients if self.is_multithreaded_client_available(c)) + + def get_supported_clients(self) -> Tuple[str, ...]: + return "self", "aria2", "axel" diff --git a/bauh/view/core/settings.py b/bearhub/view/core/settings.py similarity index 100% rename from bauh/view/core/settings.py rename to bearhub/view/core/settings.py diff --git a/bearhub/view/core/suggestions.py b/bearhub/view/core/suggestions.py index 1e48e384..280f3a88 100644 --- a/bearhub/view/core/suggestions.py +++ b/bearhub/view/core/suggestions.py @@ -1,2 +1,32 @@ -from bauh.view.core.suggestions import * # noqa: F401,F403 +import os +from typing import Optional, Dict +from bauh import __app_name__ + +def read_suggestions_mapping() -> Optional[Dict[str, str]]: + file_path = f'/etc/{__app_name__}/suggestions.conf' + + if os.path.isfile(file_path): + try: + with open(file_path) as f: + file_content = f.read() + except FileNotFoundError: + return + + if not file_content: + return + + mapping = {} + for line in file_content.split('\n'): + line_strip = line.strip() + + if not line_strip.startswith('#'): + gem_file = line_strip.split('=') + + if len(gem_file) == 2: + gem_name, file_url = gem_file[0].strip(), gem_file[1].strip() + + if gem_name and file_url: + mapping[gem_name] = file_url + + return mapping if mapping else None diff --git a/bauh/view/core/timeshift.py b/bearhub/view/core/timeshift.py similarity index 100% rename from bauh/view/core/timeshift.py rename to bearhub/view/core/timeshift.py diff --git a/bauh/view/core/tray_client.py b/bearhub/view/core/tray_client.py similarity index 100% rename from bauh/view/core/tray_client.py rename to bearhub/view/core/tray_client.py diff --git a/bauh/view/core/update.py b/bearhub/view/core/update.py similarity index 100% rename from bauh/view/core/update.py rename to bearhub/view/core/update.py diff --git a/bearhub/view/qt/__init__.py b/bearhub/view/qt/__init__.py old mode 100644 new mode 100755 index 43353c68..e69de29b --- a/bearhub/view/qt/__init__.py +++ b/bearhub/view/qt/__init__.py @@ -1,2 +0,0 @@ -from bauh.view.qt import * # noqa: F401,F403 - diff --git a/bauh/view/qt/about.py b/bearhub/view/qt/about.py similarity index 100% rename from bauh/view/qt/about.py rename to bearhub/view/qt/about.py diff --git a/bauh/view/qt/apps_table.py b/bearhub/view/qt/apps_table.py similarity index 100% rename from bauh/view/qt/apps_table.py rename to bearhub/view/qt/apps_table.py diff --git a/bauh/view/qt/commons.py b/bearhub/view/qt/commons.py similarity index 100% rename from bauh/view/qt/commons.py rename to bearhub/view/qt/commons.py diff --git a/bauh/view/qt/components.py b/bearhub/view/qt/components.py similarity index 100% rename from bauh/view/qt/components.py rename to bearhub/view/qt/components.py diff --git a/bauh/view/qt/dialog.py b/bearhub/view/qt/dialog.py similarity index 100% rename from bauh/view/qt/dialog.py rename to bearhub/view/qt/dialog.py diff --git a/bauh/view/qt/history.py b/bearhub/view/qt/history.py similarity index 100% rename from bauh/view/qt/history.py rename to bearhub/view/qt/history.py diff --git a/bauh/view/qt/info.py b/bearhub/view/qt/info.py similarity index 100% rename from bauh/view/qt/info.py rename to bearhub/view/qt/info.py diff --git a/bearhub/view/qt/prepare.py b/bearhub/view/qt/prepare.py index 342e431e..fc80fd83 100644 --- a/bearhub/view/qt/prepare.py +++ b/bearhub/view/qt/prepare.py @@ -1,2 +1,453 @@ -from bauh.view.qt.prepare import * # noqa: F401,F403 +import datetime +import operator +import time +from functools import reduce +from typing import Tuple, Optional +from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication, QMutex +from PyQt5.QtGui import QIcon, QCursor, QCloseEvent, QShowEvent +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, \ + QProgressBar, QPlainTextEdit, QToolButton, QHBoxLayout + +from bauh import __app_name__ +from bauh.api.abstract.context import ApplicationContext +from bauh.api.abstract.controller import SoftwareManager, SoftwareAction +from bauh.api.abstract.handler import TaskManager +from bauh.api import user +from bauh.view.qt.components import new_spacer, QCustomToolbar +from bauh.view.qt.qt_utils import centralize, get_current_screen_geometry +from bauh.view.qt.root import RootDialog +from bauh.view.qt.thread import AnimateProgress +from bauh.view.util.translation import I18n + + +class Prepare(QThread, TaskManager): + signal_register = pyqtSignal(str, str, object) + signal_update = pyqtSignal(str, float, str) + signal_finished = pyqtSignal(str) + signal_started = pyqtSignal(int) + signal_ask_password = pyqtSignal() + signal_output = pyqtSignal(str, str) + + def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n): + super(Prepare, self).__init__() + self.manager = manager + self.i18n = i18n + self.context = context + self.waiting_password = False + self.password_response = None + self._tasks_added = set() + self._tasks_finished = set() + self._add_lock = QMutex() + self._finish_lock = QMutex() + + def ask_password(self) -> Tuple[bool, Optional[str]]: + self.waiting_password = True + self.signal_ask_password.emit() + + while self.waiting_password: + self.msleep(100) # waiting for user input + + return self.password_response + + def set_password_reply(self, valid: bool, password: str): + self.password_response = valid, password + self.waiting_password = False + + def run(self): + root_pwd = None + if not user.is_root() and self.manager.requires_root(SoftwareAction.PREPARE, None): + ok, root_pwd = self.ask_password() + + if not ok: + QCoreApplication.exit(1) + + self.manager.prepare(self, root_pwd, None) + self.signal_started.emit(len(self._tasks_added)) + + def update_progress(self, task_id: str, progress: float, substatus: str): + self._add_lock.lock() + if task_id in self._tasks_added: + self.signal_update.emit(task_id, progress, substatus) + self._add_lock.unlock() + + def update_output(self, task_id: str, output: str): + self._add_lock.lock() + if task_id in self._tasks_added: + self.signal_output.emit(task_id, output) + self._add_lock.unlock() + + def register_task(self, id_: str, label: str, icon_path: str): + self._add_lock.lock() + + if id_ not in self._tasks_added: + self._tasks_added.add(id_) + self.signal_register.emit(id_, label, icon_path) + + self._add_lock.unlock() + + def finish_task(self, task_id: str): + self._add_lock.lock() + task_registered = task_id in self._tasks_added + self._add_lock.unlock() + + if not task_registered: + return + + self._finish_lock.lock() + + if task_id not in self._tasks_finished: + self._tasks_finished.add(task_id) + self.signal_finished.emit(task_id) + + self._finish_lock.unlock() + + +class CheckFinished(QThread): + signal_finished = pyqtSignal() + + def __init__(self): + super(CheckFinished, self).__init__() + self.total = 0 + self.finished = 0 + + def run(self): + while True: + if self.finished == self.total: + break + + self.msleep(5) + + self.signal_finished.emit() + + def update(self, finished: int): + if finished is not None: + self.finished = finished + + +class EnableSkip(QThread): + + signal_timeout = pyqtSignal() + + def run(self): + ti = datetime.datetime.now() + + while True: + if datetime.datetime.now() >= ti + datetime.timedelta(seconds=10): + self.signal_timeout.emit() + break + + self.msleep(100) + + +class PreparePanel(QWidget, TaskManager): + + signal_status = pyqtSignal(int) + signal_password_response = pyqtSignal(bool, str) + + def __init__(self, context: ApplicationContext, manager: SoftwareManager, + i18n: I18n, manage_window: QWidget, app_config: dict, force_suggestions: bool = False): + super(PreparePanel, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) + self.i18n = i18n + self.context = context + self.app_config = app_config + self.manage_window = manage_window + self.setWindowTitle('{} ({})'.format(__app_name__, self.i18n['prepare_panel.title.start'].lower())) + + self.setLayout(QVBoxLayout()) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + self.manager = manager + self.tasks = {} + self.output = {} + self.added_tasks = 0 + self.ftasks = 0 + self.started_at = None + self.self_close = False + self.force_suggestions = force_suggestions + + self.prepare_thread = Prepare(self.context, manager, self.i18n) + self.prepare_thread.signal_register.connect(self.register_task) + self.prepare_thread.signal_update.connect(self.update_progress) + self.prepare_thread.signal_finished.connect(self.finish_task) + self.prepare_thread.signal_started.connect(self.start) + self.prepare_thread.signal_ask_password.connect(self.ask_root_password) + self.prepare_thread.signal_output.connect(self.update_output) + self.signal_password_response.connect(self.prepare_thread.set_password_reply) + + self.check_thread = CheckFinished() + self.signal_status.connect(self.check_thread.update) + self.check_thread.signal_finished.connect(self.finish) + + self.skip_thread = EnableSkip() + self.skip_thread.signal_timeout.connect(self._enable_skip_button) + + self.progress_thread = AnimateProgress() + self.progress_thread.signal_change.connect(self._change_progress) + + self.label_top = QLabel() + self.label_top.setCursor(QCursor(Qt.WaitCursor)) + self.label_top.setText("{}...".format(self.i18n['prepare_panel.title.start'].capitalize())) + self.label_top.setObjectName('prepare_status') + self.label_top.setAlignment(Qt.AlignHCenter) + self.layout().addWidget(self.label_top) + self.layout().addWidget(QLabel()) + + self.table = QTableWidget() + self.table.setObjectName('tasks') + self.table.setCursor(QCursor(Qt.WaitCursor)) + self.table.setFocusPolicy(Qt.NoFocus) + self.table.setShowGrid(False) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setVisible(False) + self.table.horizontalHeader().setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + self.table.setColumnCount(4) + self.table.setHorizontalHeaderLabels(['' for _ in range(4)]) + self.table.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor)) + self.table.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor)) + self.layout().addWidget(self.table) + + self.textarea_details = QPlainTextEdit(self) + self.textarea_details.setObjectName('task_details') + self.textarea_details.setProperty('console', 'true') + self.textarea_details.resize(self.table.size()) + self.layout().addWidget(self.textarea_details) + self.textarea_details.setVisible(False) + self.textarea_details.setReadOnly(True) + self.textarea_details.setMaximumHeight(100) + self.current_output_task = None + + self.bottom_widget = QWidget() + self.bottom_widget.setLayout(QHBoxLayout()) + self.bottom_widget.layout().addStretch() + + bt_hide_details = QPushButton(self.i18n['prepare.bt_hide_details']) + bt_hide_details.setObjectName('bt_hide_details') + bt_hide_details.clicked.connect(self.hide_output) + bt_hide_details.setCursor(QCursor(Qt.PointingHandCursor)) + self.bottom_widget.layout().addWidget(bt_hide_details) + self.bottom_widget.layout().addStretch() + self.layout().addWidget(self.bottom_widget) + self.bottom_widget.setVisible(False) + + self.bt_bar = QCustomToolbar(policy_height=QSizePolicy.Fixed) + self.bt_close = QPushButton(self.i18n['close'].capitalize()) + self.bt_close.setObjectName('bt_cancel') + self.bt_close.setCursor(QCursor(Qt.PointingHandCursor)) + self.bt_close.clicked.connect(self.close) + self.bt_close.setVisible(False) + self.bt_bar.add_widget(self.bt_close) + self.bt_bar.add_widget(new_spacer()) + + self.progress_bar = QProgressBar() + self.progress_bar.setObjectName('prepare_progress') + self.progress_bar.setTextVisible(False) + self.progress_bar.setVisible(False) + self.progress_bar.setCursor(QCursor(Qt.WaitCursor)) + self.bt_bar.add_widget(self.progress_bar) + self.bt_bar.add_widget(new_spacer()) + + self.bt_skip = QPushButton(self.i18n['prepare_panel.bt_skip.label'].capitalize()) + self.bt_skip.clicked.connect(self.finish) + self.bt_skip.setEnabled(False) + self.bt_skip.setCursor(QCursor(Qt.WaitCursor)) + self.bt_bar.add_widget(self.bt_skip) + + self.layout().addWidget(self.bt_bar) + centralize(self) + + def hide_output(self): + self.current_output_task = None + self.textarea_details.setVisible(False) + self.textarea_details.clear() + self.bottom_widget.setVisible(False) + self._resize_columns() + self.setFocus(Qt.NoFocusReason) + + if not self.bt_bar.isVisible(): + self.bt_bar.setVisible(True) + + def ask_root_password(self): + valid, root_pwd = RootDialog.ask_password(self.context, self.i18n) + self.signal_password_response.emit(valid, root_pwd) + + def _enable_skip_button(self): + self.bt_skip.setEnabled(True) + self.bt_skip.setCursor(QCursor(Qt.PointingHandCursor)) + + def _change_progress(self, value: int): + self.progress_bar.setValue(value) + + def get_table_width(self) -> int: + return reduce(operator.add, [self.table.columnWidth(i) for i in range(self.table.columnCount())]) + + def _resize_columns(self): + header_horizontal = self.table.horizontalHeader() + for i in range(self.table.columnCount()): + header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents) + + self.resize(int(self.get_table_width() * 1.05), self.sizeHint().height()) + + def showEvent(self, event: Optional[QShowEvent]) -> None: + super().showEvent(event) + self.prepare_thread.start() + screen_size = get_current_screen_geometry() + self.setMinimumWidth(int(screen_size.width() * 0.25)) + self.setMinimumHeight(int(screen_size.height() * 0.35)) + self.setMaximumHeight(int(screen_size.height() * 0.95)) + centralize(self) + + def start(self, tasks: int): + self.started_at = time.time() + self.check_thread.total = tasks + self.check_thread.start() + self.skip_thread.start() + + self.progress_thread.start() + + self.bt_close.setVisible(True) + self.progress_bar.setVisible(True) + + def closeEvent(self, ev: QCloseEvent): + if not self.self_close: + QCoreApplication.exit() + + def register_task(self, id_: str, label: str, icon_path: str): + self.added_tasks += 1 + self.table.setRowCount(self.added_tasks) + task_row = self.added_tasks - 1 + + icon_widget = QWidget() + icon_widget.setProperty('container', 'true') + icon_widget.setLayout(QHBoxLayout()) + icon_widget.layout().setContentsMargins(10, 0, 10, 0) + + bt_icon = QToolButton() + bt_icon.setObjectName('bt_task') + bt_icon.setCursor(QCursor(Qt.WaitCursor)) + bt_icon.setEnabled(False) + bt_icon.setToolTip(self.i18n['prepare.bt_icon.no_output']) + + if icon_path: + bt_icon.setIcon(QIcon(icon_path)) + + def _show_output(): + lines = self.output[id_] + + if lines: + self.current_output_task = id_ + self.textarea_details.clear() + self.textarea_details.setVisible(True) + + for l in lines: + self.textarea_details.appendPlainText(l) + + self.bottom_widget.setVisible(True) + + self.setFocus(Qt.NoFocusReason) + + if self.bt_bar.isVisible(): + self.bt_bar.setVisible(False) + + bt_icon.clicked.connect(_show_output) + icon_widget.layout().addWidget(bt_icon) + + self.table.setCellWidget(task_row, 0, icon_widget) + + lb_status = QLabel(label) + lb_status.setObjectName('task_status') + lb_status.setProperty('status', 'running') + lb_status.setCursor(Qt.WaitCursor) + lb_status.setMinimumWidth(50) + lb_status.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + lb_status_col = 1 + self.table.setCellWidget(task_row, lb_status_col, lb_status) + + lb_progress = QLabel('{0:.2f}'.format(0) + '%') + lb_progress.setObjectName('task_progress') + lb_progress.setProperty('status', 'running') + lb_progress.setCursor(Qt.WaitCursor) + lb_progress.setContentsMargins(10, 0, 10, 0) + lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + lb_progress_col = 2 + + self.table.setCellWidget(task_row, lb_progress_col, lb_progress) + + lb_sub = QLabel() + lb_sub.setObjectName('task_substatus') + lb_sub.setCursor(Qt.WaitCursor) + lb_sub.setContentsMargins(10, 0, 10, 0) + lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + lb_sub.setMinimumWidth(50) + self.table.setCellWidget(task_row, 3, lb_sub) + + self.tasks[id_] = {'bt_icon': bt_icon, + 'lb_status': lb_status, + 'lb_status_col': lb_status_col, + 'lb_prog': lb_progress, + 'lb_prog_col': lb_progress_col, + 'progress': 0, + 'lb_sub': lb_sub, + 'finished': False, + 'row': task_row} + + def update_progress(self, task_id: str, progress: float, substatus: str): + task = self.tasks[task_id] + + if progress != task['progress']: + task['progress'] = progress + task['lb_prog'].setText('{0:.2f}'.format(progress) + '%') + + if substatus: + task['lb_sub'].setText('({})'.format(substatus)) + else: + task['lb_sub'].setText('') + + self._resize_columns() + + def update_output(self, task_id: str, output: str): + full_output = self.output.get(task_id) + + if full_output is None: + full_output = [] + self.output[task_id] = full_output + task = self.tasks[task_id] + task['bt_icon'].setEnabled(True) + task['bt_icon'].setCursor(QCursor(Qt.PointingHandCursor)) + task['bt_icon'].setToolTip(self.i18n['prepare.bt_icon.output']) + + full_output.append(output) + + if self.current_output_task == task_id: + self.textarea_details.appendPlainText(output) + + def finish_task(self, task_id: str): + task = self.tasks[task_id] + + for key in ('lb_prog', 'lb_status', 'lb_sub'): + label = task[key] + label.setProperty('status', 'done') + label.style().unpolish(label) + label.style().polish(label) + label.update() + + task['finished'] = True + self._resize_columns() + + self.ftasks += 1 + self.signal_status.emit(self.ftasks) + + def finish(self): + now = time.time() + self.context.logger.info("{0} tasks finished in {1:.9f} seconds".format(self.ftasks, (now - self.started_at))) + if self.isVisible(): + self.manage_window.show() + + if self.force_suggestions: + self.manage_window.begin_load_suggestions(filter_installed=True) + elif self.app_config['boot']['load_apps']: + self.manage_window.begin_refresh_packages() + else: + self.manage_window.load_without_packages() + + self.self_close = True + self.close() diff --git a/bauh/view/qt/qt_utils.py b/bearhub/view/qt/qt_utils.py similarity index 100% rename from bauh/view/qt/qt_utils.py rename to bearhub/view/qt/qt_utils.py diff --git a/bauh/view/qt/root.py b/bearhub/view/qt/root.py similarity index 100% rename from bauh/view/qt/root.py rename to bearhub/view/qt/root.py diff --git a/bauh/view/qt/screenshots.py b/bearhub/view/qt/screenshots.py similarity index 100% rename from bauh/view/qt/screenshots.py rename to bearhub/view/qt/screenshots.py diff --git a/bearhub/view/qt/settings.py b/bearhub/view/qt/settings.py index afb5f98a..2b1d9bdd 100644 --- a/bearhub/view/qt/settings.py +++ b/bearhub/view/qt/settings.py @@ -1,2 +1,149 @@ -from bauh.view.qt.settings import * # noqa: F401,F403 +import gc +from io import StringIO +from typing import Optional +from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal +from PyQt5.QtGui import QCursor, QShowEvent +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QPushButton, QHBoxLayout, QApplication + +from bauh import __app_name__ +from bauh.api.abstract.controller import SoftwareManager +from bauh.api.abstract.view import MessageType +from bauh.view.core.controller import GenericSoftwareManager +from bauh.view.qt import dialog +from bauh.view.qt.components import to_widget, new_spacer +from bauh.view.qt.dialog import ConfirmationDialog +from bauh.view.qt.qt_utils import centralize +from bauh.view.util import util +from bauh.view.util.translation import I18n + + +class ReloadManagePanel(QThread): + + signal_finished = pyqtSignal() + + def __init__(self, manager: SoftwareManager): + super(ReloadManagePanel, self).__init__() + self.manager = manager + + def run(self): + if isinstance(self.manager, GenericSoftwareManager): + self.manager.reset_cache() + + self.manager.prepare(task_manager=None, root_password=None, internet_available=None) + self.signal_finished.emit() + + +class SettingsWindow(QWidget): + + def __init__(self, manager: SoftwareManager, i18n: I18n, window: QWidget, parent: Optional[QWidget] = None): + super(SettingsWindow, self).__init__(parent=parent, flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) + self.setWindowTitle(f"{i18n['settings'].capitalize()} ({__app_name__})") + self.setLayout(QVBoxLayout()) + self.manager = manager + self.i18n = i18n + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + self.window = window + + self.settings_model = tuple(v for v in self.manager.get_settings())[0].component + + self.tab_group = to_widget(self.settings_model, i18n) + self.tab_group.setObjectName('settings') + self.layout().addWidget(self.tab_group) + + lower_container = QWidget() + lower_container.setObjectName('lower_container') + lower_container.setProperty('container', 'true') + lower_container.setLayout(QHBoxLayout()) + lower_container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + + self.bt_close = QPushButton() + self.bt_close.setObjectName('cancel') + self.bt_close.setAutoDefault(True) + self.bt_close.setCursor(QCursor(Qt.PointingHandCursor)) + self.bt_close.setText(self.i18n['close'].capitalize()) + self.bt_close.clicked.connect(lambda: self.close()) + lower_container.layout().addWidget(self.bt_close) + + lower_container.layout().addWidget(new_spacer()) + + self.bt_change = QPushButton() + self.bt_change.setAutoDefault(True) + self.bt_change.setObjectName('ok') + self.bt_change.setCursor(QCursor(Qt.PointingHandCursor)) + self.bt_change.setText(self.i18n['change'].capitalize()) + self.bt_change.clicked.connect(self._save_settings) + lower_container.layout().addWidget(self.bt_change) + + self.layout().addWidget(lower_container) + + self.thread_reload_panel = ReloadManagePanel(manager=manager) + self.thread_reload_panel.signal_finished.connect(self._reload_manage_panel) + centralize(self) + + def showEvent(self, event: Optional[QShowEvent]): + super(SettingsWindow, self).showEvent(event) + self.setMinimumWidth(int(self.sizeHint().width())) + centralize(self) + + def closeEvent(self, event): + if self.window and self.window.settings_window == self: + self.deleteLater() + self.window.settings_window = None + elif not self.window: + QCoreApplication.exit() + + gc.collect() + + def handle_display(self): + if self.isMinimized(): + self.setWindowState(Qt.WindowNoState) + elif self.isHidden(): + self.show() + else: + self.setWindowState(self.windowState() and Qt.WindowMinimized or Qt.WindowActive) + + def _save_settings(self): + self.tab_group.setEnabled(False) + self.bt_change.setEnabled(False) + self.bt_close.setEnabled(False) + + success, warnings = self.manager.save_settings(self.settings_model) + + if success: + if not self.window: + ConfirmationDialog(title=self.i18n['success'].capitalize(), + body=f"{self.i18n['settings.changed.success.warning']}
", + i18n=self.i18n, + confirmation_label=self.i18n['ok'], + confirmation_icon=False, + deny_button=False).ask() + QCoreApplication.exit() + elif ConfirmationDialog(title=self.i18n['warning'].capitalize(), + body=f"{self.i18n['settings.changed.success.warning']}
" + f"{self.i18n['settings.changed.success.reboot']}
", + i18n=self.i18n).ask(): + self.close() + util.restart_app() + else: + self.thread_reload_panel.start() + QApplication.setOverrideCursor(Qt.WaitCursor) + else: + msg = StringIO() + msg.write(f"{self.i18n['settings.error']}
") + + for w in warnings: + msg.write(f'* {w}
{}
'.format('{}
'.format(substatus)) + if not substatus: + self.toolbar_substatus.hide() + elif not self.toolbar_substatus.isVisible() and self.progress_bar.isVisible(): + self.toolbar_substatus.show() + + def _reorganize(self): + if not self._maximized: + self.table_apps.change_headers_policy(QHeaderView.Stretch) + self.table_apps.change_headers_policy() + self._resize(accept_lower_width=len(self.pkgs) > 0) + + def _update_table(self, pkgs_info: dict, signal: bool = False): + self.pkgs = pkgs_info["pkgs_displayed"] + + if pkgs_info["not_installed"] == 0: + update_check = sum_updates_displayed(pkgs_info) > 0 + else: + update_check = False + + self.table_apps.update_packages(self.pkgs, update_check_enabled=update_check) + + if not self._maximized: + self.label_displayed.show() + self.table_apps.change_headers_policy(QHeaderView.Stretch) + self.table_apps.change_headers_policy() + self._resize(accept_lower_width=len(self.pkgs) > 0) + + if len(self.pkgs) == 0 and len(self.pkgs_available) == 0: + self.label_displayed.setText("") + else: + self.label_displayed.setText(f"{len(self.pkgs)} / {len(self.pkgs_available)}") + else: + self.label_displayed.hide() + + if signal: + self.signal_table_update.emit() + + def update_bt_upgrade(self, pkgs_info: dict = None): + show_bt_upgrade = False + + if not any([self.suggestions_requested, self.search_performed]) and (not pkgs_info or pkgs_info['not_installed'] == 0): + for pkg in (pkgs_info['pkgs_displayed'] if pkgs_info else self.pkgs): + if not pkg.model.is_update_ignored() and pkg.update_checked: + show_bt_upgrade = True + break + + self.comp_manager.set_component_visible(BT_UPGRADE, show_bt_upgrade) + + if show_bt_upgrade: + self._reorganize() + + def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True, keep_selected: bool = False): + self.update_bt_upgrade(pkgs_info) + + if pkgs_info['updates'] > 0: + if pkgs_info['not_installed'] == 0: + if not self.comp_manager.is_visible(CHECK_UPDATES): + self.comp_manager.set_component_visible(CHECK_UPDATES, True) + + if not self.filter_updates and not keep_selected: + self._change_checkbox(self.check_updates, True, 'filter_updates', trigger_filters) + + if pkgs_info['napp_updates'] > 0 and self.filter_only_apps and not keep_selected: + self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger_filters) + else: + if not keep_selected: + self._change_checkbox(self.check_updates, False, 'filter_updates', trigger_filters) + + self.comp_manager.set_component_visible(CHECK_UPDATES, False) + + def _change_checkbox(self, checkbox: QCheckBox, checked: bool, attr: str = None, trigger: bool = True): + if not trigger: + checkbox.blockSignals(True) + + checkbox.setChecked(checked) + + if not trigger: + setattr(self, attr, checked) + checkbox.blockSignals(False) + + def _gen_filters(self, ignore_updates: bool = False) -> PackageFilters: + return PackageFilters(category=self.category_filter, + display_limit=0 if self.filter_updates else self.display_limit, + name=self.input_name.text().strip(), + only_apps=False if self.search_performed else self.filter_only_apps, + only_verified=self.filter_only_verified, + only_updates=False if ignore_updates else self.filter_updates, + only_installed=self.filter_installed, + search=self.searched_term, + type=self.type_filter) + + def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: bool, types: Optional[Set[type]] = None, ignore_updates: bool = False, keep_filters: bool = False) -> bool: + self.input_name.set_text('') + pkgs_info = commons.new_pkgs_info() + pkg_idx = new_package_index() + filters = self._gen_filters(ignore_updates=ignore_updates) + + if new_pkgs is not None: + old_installed = None + + if as_installed: + old_installed = self.pkgs_installed + self.pkgs_installed = [] + + for pkg in new_pkgs: + pkgv = PackageView(model=pkg, i18n=self.i18n) + commons.update_info(pkgv, pkgs_info) + add_to_index(pkgv, pkg_idx) + commons.apply_filters(pkgv, filters, pkgs_info) + + if old_installed and types: + for pkgv in old_installed: + if pkgv.model.__class__ not in types: + commons.update_info(pkgv, pkgs_info) + add_to_index(pkgv, pkg_idx) + commons.apply_filters(pkgv, filters, pkgs_info) + + else: # use installed + for pkgv in self.pkgs_installed: + commons.update_info(pkgv, pkgs_info) + add_to_index(pkgv, pkg_idx) + commons.apply_filters(pkgv, filters, pkgs_info) + + if pkgs_info['apps_count'] == 0 and not self.suggestions_requested: + if self.load_suggestions or self.types_changed: + if as_installed: + self.pkgs_installed = pkgs_info['pkgs'] + + self.begin_load_suggestions(filter_installed=True) + self.load_suggestions = False + return False + else: + if not keep_filters: + self._change_checkbox(self.check_apps, False, 'filter_only_apps', trigger=False) + self.check_apps.setCheckable(False) + + else: + if not keep_filters: + self.check_apps.setCheckable(True) + self._change_checkbox(self.check_apps, True, 'filter_only_apps', trigger=False) + + self._update_verified_filter(verified_available=pkgs_info['verified'] > 0, keep_state=keep_filters) + self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) + self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) + self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) + self._apply_filters(pkgs_info, ignore_updates=ignore_updates) + self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) + + self.pkgs_available = pkgs_info['pkgs'] + self.pkg_idx = pkg_idx + + if as_installed: + self.pkgs_installed = pkgs_info['pkgs'] + + self.pkgs = pkgs_info['pkgs_displayed'] + self._update_installed_filter(installed_available=pkgs_info['installed'] > 0, + keep_state=keep_filters, + hide=as_installed) + self._update_table(pkgs_info=pkgs_info) + + if new_pkgs: + self.stop_notifying_package_states() + self.thread_notify_pkgs_ready.work = True + self.thread_notify_pkgs_ready.pkgs = self.pkgs + self.thread_notify_pkgs_ready.start() + + self._resize(accept_lower_width=bool(self.pkgs_installed)) + + if not self.installed_loaded and as_installed: + self.installed_loaded = True + + return True + + def _update_installed_filter(self, keep_state: bool = True, hide: bool = False, installed_available: Optional[bool] = None): + if installed_available is not None: + has_installed = installed_available + elif self.pkgs_available == self.pkgs_installed: # it means the "installed" view is loaded + has_installed = False + else: + has_installed = False + if self.pkgs_available: + for p in self.pkgs_available: + if p.model.installed: + has_installed = True + break + + if not keep_state or not has_installed: + self._change_checkbox(self.check_installed, False, 'filter_installed', trigger=False) + + if hide: + self.comp_manager.set_component_visible(CHECK_INSTALLED, False) + else: + self.comp_manager.set_component_visible(CHECK_INSTALLED, has_installed) + + def _update_verified_filter(self, keep_state: bool = True, verified_available: Optional[bool] = None): + if verified_available is not None: + has_verified = verified_available + else: + has_verified = False + if self.pkgs_available: + has_verified = next((True for p in self.pkgs_available if p.model.is_trustable()), False) + + if not keep_state or not has_verified: + self._change_checkbox(self.check_verified, False, 'filter_only_verified', trigger=False) + + self.comp_manager.set_component_visible(CHECK_VERIFIED, has_verified) + + def _apply_filters(self, pkgs_info: dict, ignore_updates: bool): + pkgs_info['pkgs_displayed'] = [] + filters = self._gen_filters(ignore_updates=ignore_updates) + for pkgv in pkgs_info['pkgs']: + commons.apply_filters(pkgv, filters, pkgs_info) + + def _clean_combo_types(self): + if self.combo_filter_type.count() > 1: + for _ in range(self.combo_filter_type.count() - 1): + self.combo_filter_type.removeItem(1) + + def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False): + if available_types is None: + self.comp_manager.set_component_visible(COMBO_TYPES, self.combo_filter_type.count() > 2) + else: + keeping_selected = keep_selected and available_types and self.type_filter in available_types + + if not keeping_selected: + self.type_filter = self.any_type_filter + if not available_types: + self._clean_combo_types() + + if available_types: + self._clean_combo_types() + + sel_type = -1 + for idx, item in enumerate(available_types.items()): + app_type, icon_path, label = item[0], item[1]['icon'], item[1]['label'] + + icon = self.cache_type_filter_icons.get(app_type) + + if not icon: + icon = QIcon(icon_path) + self.cache_type_filter_icons[app_type] = icon + + self.combo_filter_type.addItem(icon, label, app_type) + + if keeping_selected and app_type == self.type_filter: + sel_type = idx + 1 + + self.combo_filter_type.blockSignals(True) + self.combo_filter_type.setCurrentIndex(sel_type if sel_type > -1 else 0) + self.combo_filter_type.blockSignals(False) + self.comp_manager.set_component_visible(COMBO_TYPES, len(available_types) > 1) + else: + self.comp_manager.set_component_visible(COMBO_TYPES, False) + + def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False): + if categories is None: + self.comp_manager.set_component_visible(COMBO_CATEGORIES, self.combo_categories.count() > 1) + else: + keeping_selected = keep_selected and categories and self.category_filter in categories + + if not keeping_selected: + self.category_filter = self.any_category_filter + + if categories: + if self.combo_categories.count() > 1: + for _ in range(self.combo_categories.count() - 1): + self.combo_categories.removeItem(1) + + selected_cat = -1 + cat_list = list(categories) + cat_list.sort() + + for idx, c in enumerate(cat_list): + self.__add_category(c) + + if keeping_selected and c == self.category_filter: + selected_cat = idx + 1 + + self.combo_categories.blockSignals(True) + self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0) + self.combo_categories.blockSignals(False) + self.comp_manager.set_component_visible(COMBO_CATEGORIES, True) + + else: + self.comp_manager.set_component_visible(COMBO_CATEGORIES, False) + + def __add_category(self, category: str): + i18n_cat = self.i18n.get('category.{}'.format(category), self.i18n.get(category, category)) + self.combo_categories.addItem(i18n_cat.capitalize(), category) + + def _get_current_categories(self) -> Set[str]: + if self.combo_categories.count() > 1: + return {self.combo_categories.itemData(idx) for idx in range(self.combo_categories.count()) if idx > 0} + + def _resize(self, accept_lower_width: bool = True): + table_width = self.table_apps.get_width() + toolbar_width = self.toolbar_filters.sizeHint().width() + topbar_width = self.toolbar_status.sizeHint().width() + + new_width = max(table_width, toolbar_width, topbar_width) + new_width *= 1.05 # this extra size is not because of the toolbar button, but the table upgrade buttons + new_width = int(new_width) + + if new_width >= self.maximumWidth(): + new_width = self.maximumWidth() + + if (self.pkgs and accept_lower_width) or new_width > self.width(): + self.resize(new_width, self.height()) + self.setMinimumWidth(new_width) + + def set_progress_controll(self, enabled: bool): + self.progress_controll_enabled = enabled + + def upgrade_selected(self): + body = QWidget() + body.setLayout(QHBoxLayout()) + body.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + body.layout().addWidget(QLabel(self.i18n['manage_window.upgrade_all.popup.body'])) + body.layout().addWidget(UpgradeToggleButton(pkg=None, root=self, i18n=self.i18n, clickable=False)) + if ConfirmationDialog(title=self.i18n['manage_window.upgrade_all.popup.title'], + i18n=self.i18n, body=None, + widgets=[body]).ask(): + + self._begin_action(action_label=self.i18n['manage_window.status.upgrading'], + action_id=ACTION_UPGRADE) + self.comp_manager.set_components_visible(False) + self._handle_console_option(True) + self.thread_update.pkgs = self.pkgs + self.thread_update.start() + + def _finish_upgrade_selected(self, res: dict): + self._finish_action() + + if res.get('id'): + self._write_operation_logs('upgrade', custom_log_file=f"{UpgradeSelected.UPGRADE_LOGS_DIR}/{res['id']}.log") + sum_log_file = UpgradeSelected.SUMMARY_FILE.format(res['id']) + summ_msg = '* ' + self.i18n['console.upgrade_summary'].format(path=f'"{sum_log_file}"') + self.textarea_details.appendPlainText(summ_msg) + + if res['success']: + self.comp_manager.remove_saved_state(ACTION_UPGRADE) + self.begin_refresh_packages(pkg_types=res['types']) + self._show_console_checkbox_if_output() + + if self._can_notify_user(): + util.notify_user('{} {}'.format(res['updated'], self.i18n['notification.update_selected.success'])) + + notify_tray() + else: + self.comp_manager.restore_state(ACTION_UPGRADE) + self._show_console_errors() + + if self._can_notify_user(): + util.notify_user(self.i18n['notification.update_selected.failed']) + + self.update_custom_actions() + + def _show_console_errors(self): + if self.textarea_details.toPlainText(): + self.check_details.setChecked(True) + else: + self._handle_console_option(False) + self.comp_manager.set_component_visible(CHECK_DETAILS, False) + + def _update_action_output(self, output: str): + self.textarea_details.appendPlainText(output) + + def _begin_action(self, action_label: str, action_id: int = None): + self.thread_animate_progress.stop = False + self.thread_animate_progress.start() + self.progress_bar.setVisible(True) + + if action_id is not None: + self.comp_manager.save_states(action_id, only_visible=True) + + self._set_table_enabled(False) + self.comp_manager.set_component_visible(SEARCH_BAR, False) + self._change_status(action_label) + + def _set_lower_buttons_visible(self, visible: bool): + self.comp_manager.set_group_visible(GROUP_LOWER_BTS, visible) + + if visible: + self.comp_manager.set_component_visible(BT_CUSTOM_ACTIONS, bool(self.custom_actions)) + + def _finish_action(self, action_id: int = None): + self.thread_animate_progress.stop = True + self.thread_animate_progress.wait(msecs=1000) + + self.progress_bar.setVisible(False) + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(False) + + if action_id is not None: + self.comp_manager.restore_state(action_id) + + self.comp_manager.set_component_visible(SEARCH_BAR, True) + self._change_status() + self._change_label_substatus('') + self._set_table_enabled(True) + self.progress_controll_enabled = True + + def begin_downgrade(self, pkg: PackageView): + pwd, proceed = self._ask_root_password(SoftwareAction.DOWNGRADE, pkg) + + if not proceed: + return + + self.table_apps.stop_file_downloader() + label = f"{self.i18n['manage_window.status.downgrading']} {pkg.model.name}" + self._begin_action(action_label=label, action_id=ACTION_DOWNGRADE) + self.comp_manager.set_components_visible(False) + self._handle_console_option(True) + + self.thread_downgrade.pkg = pkg + self.thread_downgrade.root_pwd = pwd + self.thread_downgrade.start() + + def _finish_downgrade(self, res: dict): + self._finish_action() + self._write_operation_logs('downgrade', res['app']) + + if res['success']: + self.comp_manager.remove_saved_state(ACTION_DOWNGRADE) + + if self._can_notify_user(): + util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded'])) + + self.begin_refresh_packages(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None) + self._show_console_checkbox_if_output() + self.update_custom_actions() + notify_tray() + else: + self.comp_manager.restore_state(ACTION_DOWNGRADE) + self._show_console_errors() + + if self._can_notify_user(): + util.notify_user(self.i18n['notification.downgrade.failed']) + + def begin_show_info(self, pkg: dict): + self._begin_action(self.i18n['manage_window.status.info'], action_id=ACTION_INFO) + self.comp_manager.disable_visible() + + self.thread_show_info.pkg = pkg + self.thread_show_info.start() + + def _finish_show_info(self, pkg_info: dict): + self._finish_action(action_id=ACTION_INFO) + + if pkg_info: + if len(pkg_info) > 1: + dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n, + can_open_url=self.can_open_urls) + dialog_info.exec_() + else: + dialog.show_message(title=self.i18n['warning'].capitalize(), + body=self.i18n['manage_window.info.no_info'].format(bold(pkg_info['__app__'].model.name)), + type_=MessageType.WARNING) + + def begin_show_screenshots(self, pkg: PackageView): + self._begin_action(action_label=self.i18n['manage_window.status.screenshots'].format(bold(pkg.model.name)), + action_id=ACTION_SCREENSHOTS) + self.comp_manager.disable_visible() + + self.thread_screenshots.pkg = pkg + self.thread_screenshots.start() + + def _finish_show_screenshots(self, res: dict): + self._finish_action(ACTION_SCREENSHOTS) + + if res.get('screenshots'): + diag = ScreenshotsDialog(pkg=res['pkg'], + http_client=self.http_client, + icon_cache=self.icon_cache, + logger=self.logger, + i18n=self.i18n, + screenshots=res['screenshots']) + diag.exec_() + else: + dialog.show_message(title=self.i18n['error'], + body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)), + type_=MessageType.ERROR) + + def begin_show_history(self, pkg: PackageView): + self._begin_action(self.i18n['manage_window.status.history'], action_id=ACTION_HISTORY) + self.comp_manager.disable_visible() + + self.thread_show_history.pkg = pkg + self.thread_show_history.start() + + def _finish_show_history(self, res: dict): + self._finish_action(ACTION_HISTORY) + + if res.get('error'): + self._handle_console_option(True) + self.textarea_details.appendPlainText(res['error']) + self.check_details.setChecked(True) + elif not res['history'].history: + dialog.show_message(title=self.i18n['action.history.no_history.title'], + body=self.i18n['action.history.no_history.body'].format(bold(res['history'].pkg.name)), + type_=MessageType.WARNING) + else: + dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) + dialog_history.exec_() + + def search(self): + word = self.search_bar.text().strip() + if word: + self.table_apps.stop_file_downloader() + self._handle_console(False) + self.filter_updates = False + self.filter_installed = False + label = f"{self.i18n['manage_window.status.searching']} {word if word else ''}" + self._begin_action(action_label=label, action_id=ACTION_SEARCH) + self.comp_manager.set_components_visible(False) + self.searched_term = word + self.thread_search.word = word + self.thread_search.start() + + def _finish_search(self, res: dict): + self._finish_action() + self.search_performed = True + + if not res['error']: + self.comp_manager.set_group_visible(GROUP_VIEW_SEARCH, True) + self.update_pkgs(res['pkgs_found'], as_installed=False, ignore_updates=True) + self._set_lower_buttons_visible(True) + self._update_bts_installed_and_suggestions() + self._hide_filters_no_packages() + self._reorganize() + else: + self.comp_manager.restore_state(ACTION_SEARCH) + dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING) + + def _ask_root_password(self, action: SoftwareAction, pkg: PackageView) -> Tuple[Optional[str], bool]: + pwd = None + requires_root = self.manager.requires_root(action, pkg.model) + + if not user.is_root() and requires_root: + valid, pwd = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) + if not valid: + return pwd, False + + return pwd, True + + def install(self, pkg: PackageView): + pwd, proceed = self._ask_root_password(SoftwareAction.INSTALL, pkg) + + if not proceed: + return + + self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name), action_id=ACTION_INSTALL) + self.comp_manager.set_groups_visible(False, GROUP_UPPER_BAR, GROUP_LOWER_BTS) + self._handle_console_option(True) + + self.thread_install.pkg = pkg + self.thread_install.root_pwd = pwd + self.thread_install.start() + + def _write_operation_logs(self, type_: str, pkg: Optional[PackageView] = None, + custom_log_file: Optional[str] = None): + + console_output = self.textarea_details.toPlainText() + + if console_output: + if custom_log_file: + log_dir = os.path.dirname(custom_log_file) + log_file = custom_log_file + else: + log_dir = f"{LOGS_DIR}/{type_}" + if pkg: + log_dir = f"{log_dir}/{pkg.model.get_type()}/{pkg.model.name}" + + log_file = f'{log_dir}/{int(time.time())}.log' + + try: + Path(log_dir).mkdir(parents=True, exist_ok=True) + except OSError: + self.logger.error(f"Could not create the operation log directory '{log_dir}'") + return + + try: + with open(log_file, 'w+') as f: + f.write(console_output) + except OSError: + self.logger.error(f"Could not write the operation log to file '{log_file}'") + return + + log_msg = '\n* ' + self.i18n['console.operation_log'].format(path=f'"{log_file}"') + self.textarea_details.appendPlainText(log_msg) + + def _finish_install(self, res: dict): + self._finish_action(action_id=ACTION_INSTALL) + self._write_operation_logs('install', res['pkg']) + + if res['success']: + if self._can_notify_user(): + util.notify_user(msg='{} ({}) {}'.format(res['pkg'].model.name, res['pkg'].model.get_type(), self.i18n['installed'])) + + models_updated = [] + + for key in ('installed', 'removed'): + if res.get(key): + models_updated.extend(res[key]) + + if models_updated: + installed_available_idxs = [] + for idx, available in enumerate(self.pkgs_available): + for pidx, model in enumerate(models_updated): + if available.model == model: + available.update_model(model) + if model.installed: + installed_available_idxs.append((idx, pidx, available)) + + # re-indexing all installed so they always will be be displayed when no filters are applied + if installed_available_idxs: + # removing from available + installed_available_idxs.sort(key=operator.itemgetter(0)) + for decrement, data in enumerate(installed_available_idxs): + del self.pkgs_available[data[0] - decrement] + + # re-inserting into the available + installed_available_idxs.sort(key=operator.itemgetter(1)) + for new_idx, data in enumerate(installed_available_idxs): + self.pkgs_available.insert(new_idx, data[2]) + + # updating the respective table rows: + screen_width = get_current_screen_geometry(self).width() + for displayed in self.pkgs: + for model in models_updated: + if displayed.model == model: + self.table_apps.update_package(displayed, screen_width=screen_width, change_update_col=True) + + self.update_bt_upgrade() + + # updating installed packages + if res['removed'] and self.pkgs_installed: + to_remove = [] + for idx, installed in enumerate(self.pkgs_installed): + for removed in res['removed']: + if installed.model == removed: + to_remove.append(idx) + + if to_remove: + to_remove.sort() + + for decrement, idx in enumerate(to_remove): + del self.pkgs_installed[idx - decrement] + + if res['installed']: + for idx, model in enumerate(res['installed']): + self.pkgs_installed.insert(idx, PackageView(model, self.i18n)) + + self.update_custom_actions() + self._update_installed_filter(installed_available=True, keep_state=True) + self._update_index() + self.table_apps.change_headers_policy(policy=QHeaderView.Stretch, maximized=self._maximized) + self.table_apps.change_headers_policy(policy=QHeaderView.ResizeToContents, maximized=self._maximized) + self._resize(accept_lower_width=False) + else: + self._show_console_errors() + if self._can_notify_user(): + util.notify_user('{}: {}'.format(res['pkg'].model.name, self.i18n['notification.install.failed'])) + + def _update_progress(self, value: int): + self.progress_bar.setValue(value) + + def begin_execute_custom_action(self, pkg: Optional[PackageView], action: CustomSoftwareAction): + if pkg is None and action.requires_confirmation and \ + not ConfirmationDialog(title=self.i18n['confirmation'].capitalize(), + body='{}
'.format(self.i18n['custom_action.proceed_with'].capitalize().format(bold(self.i18n[action.i18n_label_key]))), + icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')), + i18n=self.i18n).ask(): + return False + + pwd = None + + if not user.is_root() and action.requires_root: + valid, pwd = RootDialog.ask_password(self.context, i18n=self.i18n, comp_manager=self.comp_manager) + + if not valid: + return + + action_label = self.i18n[action.i18n_status_key] + + if pkg: + if '{}' in action_label: + action_label = action_label.format(pkg.model.name) + else: + action_label += f' {pkg.model.name}' + + if action.refresh: + self.table_apps.stop_file_downloader() + + self._begin_action(action_label=action_label, action_id=ACTION_CUSTOM_ACTION) + self.comp_manager.set_components_visible(False) + self._handle_console_option(True) + + self.thread_custom_action.pkg = pkg + self.thread_custom_action.root_pwd = pwd + self.thread_custom_action.custom_action = action + self.thread_custom_action.start() + + def _finish_execute_custom_action(self, res: dict): + self._finish_action() + + if res['success']: + if res['action'].refresh: + self.comp_manager.remove_saved_state(ACTION_CUSTOM_ACTION) + self.update_custom_actions() + self.begin_refresh_packages(pkg_types={res['pkg'].model.__class__} if res['pkg'] else None) + else: + self.comp_manager.restore_state(ACTION_CUSTOM_ACTION) + + self._show_console_checkbox_if_output() + else: + self.comp_manager.restore_state(ACTION_CUSTOM_ACTION) + self._show_console_errors() + + if res['error']: + dialog.show_message(title=self.i18n['warning' if res['error_type'] == MessageType.WARNING else 'error'].capitalize(), + body=self.i18n[res['error']], + type_=res['error_type']) + + def _show_console_checkbox_if_output(self): + if self.textarea_details.toPlainText(): + self.comp_manager.set_component_visible(CHECK_DETAILS, True) + else: + self.comp_manager.set_component_visible(CHECK_DETAILS, False) + + def show_settings(self): + if self.settings_window: + self.settings_window.handle_display() + else: + self.settings_window = SettingsWindow(manager=self.manager, i18n=self.i18n, window=self) + screen_width = get_current_screen_geometry(self).width() + self.settings_window.setMinimumWidth(int(screen_width / 4)) + self.settings_window.resize(self.size()) + self.settings_window.adjustSize() + self.settings_window.show() + + def _map_custom_action(self, action: CustomSoftwareAction, parent: QWidget) -> QCustomMenuAction: + + if action.icon_path: + try: + if action.icon_path.startswith('/'): + icon = QIcon(action.icon_path) + else: + icon = QIcon.fromTheme(action.icon_path) + except Exception: + icon = None + else: + icon = None + + tip = self.i18n[action.i18n_description_key] if action.i18n_description_key else None + return QCustomMenuAction(parent=parent, + label=self.i18n[action.i18n_label_key], + action=lambda: self.begin_execute_custom_action(None, action), + tooltip=tip, + icon=icon) + + def show_custom_actions(self): + if self.custom_actions: + menu_row = QMenu() + menu_row.setCursor(QCursor(Qt.PointingHandCursor)) + actions = [self._map_custom_action(a, menu_row) for a in self.custom_actions] + menu_row.addActions(actions) + menu_row.adjustSize() + menu_row.popup(QCursor.pos()) + menu_row.exec_() + + def begin_ignore_updates(self, pkg: PackageView): + status_key = 'ignore_updates' if not pkg.model.is_update_ignored() else 'ignore_updates_reverse' + self._begin_action(action_label=self.i18n['manage_window.status.{}'.format(status_key)].format(pkg.model.name), + action_id=ACTION_IGNORE_UPDATES) + self.comp_manager.disable_visible() + self.thread_ignore_updates.pkg = pkg + self.thread_ignore_updates.start() + + def finish_ignore_updates(self, res: dict): + self._finish_action(action_id=ACTION_IGNORE_UPDATES) + + if res['success']: + hide_package = commons.is_package_hidden(res['pkg'], self._gen_filters()) + + if hide_package: + idx_to_remove = None + for pkg in self.pkgs: + if pkg == res['pkg']: + idx_to_remove = pkg.table_index + break + + if idx_to_remove is not None: + del self.pkgs[idx_to_remove] + self.table_apps.removeRow(idx_to_remove) + self._update_table_indexes() + self.update_bt_upgrade() + else: + screen_width = get_current_screen_geometry(self).width() + for pkg in self.pkgs: + if pkg == res['pkg']: + pkg.update_model(res['pkg'].model) + self.table_apps.update_package(pkg, screen_width=screen_width, change_update_col=not any([self.search_performed, self.suggestions_requested])) + self.update_bt_upgrade() + break + + for pkg_list in (self.pkgs_available, self.pkgs_installed): + if pkg_list: + for pkg in pkg_list: + if pkg == res['pkg']: + pkg.update_model(res['pkg'].model) + break + + self._add_pkg_categories(res['pkg']) + self._update_index() + + dialog.show_message(title=self.i18n['success'].capitalize(), + body=self.i18n['action.{}.success'.format(res['action'])].format(bold(res['pkg'].model.name)), + type_=MessageType.INFO) + else: + dialog.show_message(title=self.i18n['fail'].capitalize(), + body=self.i18n['action.{}.fail'.format(res['action'])].format(bold(res['pkg'].model.name)), + type_=MessageType.ERROR) + + def _add_pkg_categories(self, pkg: PackageView): + if pkg.model.categories: + pkg_categories = {c.strip().lower() for c in pkg.model.categories if c and c.strip()} + + if pkg_categories: + current_categories = self._get_current_categories() + + if current_categories: + pkg_categories = {c.strip().lower() for c in pkg.model.categories if c} + + if pkg_categories: + categories_to_add = {c for c in pkg_categories if c and c not in current_categories} + + if categories_to_add: + for cat in categories_to_add: + self.__add_category(cat) + else: + self._update_categories(pkg_categories) + + def _map_theme_action(self, theme: ThemeMetadata, menu: QMenu) -> QCustomMenuAction: + def _change_theme(): + set_theme(theme_key=theme.key, app=QApplication.instance(), logger=self.context.logger) + self.thread_save_theme.theme_key = theme.key + self.thread_save_theme.start() + + return QCustomMenuAction(label=theme.get_i18n_name(self.i18n), + action=_change_theme, + parent=menu, + tooltip=theme.get_i18n_description(self.i18n)) + + def show_themes(self): + menu_row = QMenu() + menu_row.setCursor(QCursor(Qt.PointingHandCursor)) + menu_row.addActions(self._map_theme_actions(menu_row)) + menu_row.adjustSize() + menu_row.popup(QCursor.pos()) + menu_row.exec_() + + def _map_theme_actions(self, menu: QMenu) -> List[QCustomMenuAction]: + core_config = CoreConfigManager().get_config() + + current_theme_key, current_action = core_config['ui']['theme'], None + + actions = [] + + for t in read_all_themes_metadata(): + if not t.abstract: + action = self._map_theme_action(t, menu) + + if current_action is None and current_theme_key is not None and current_theme_key == t.key: + action.button.setProperty('current', 'true') + current_action = action + else: + actions.append(action) + + if not current_action: + invalid_action = QCustomMenuAction(label=self.i18n['manage_window.bt_themes.option.invalid'], parent=menu) + invalid_action.button.setProperty('current', 'true') + current_action = invalid_action + + actions.sort(key=lambda a: a.get_label()) + actions.insert(0, current_action) + return actions + + def reload(self): + self.thread_reload.start() + + def _reload(self): + self.update_custom_actions() + self.verify_warnings() + self.types_changed = True + self.begin_refresh_packages() + + def closeEvent(self, event: QCloseEvent) -> None: + # needs to be stopped to avoid a Qt exception/crash + self.table_apps.stop_file_downloader(wait=True) + + @property + def can_open_urls(self) -> bool: + if self._can_open_urls is None: + self._can_open_urls = shutil.which("xdg-open") + + return self._can_open_urls diff --git a/bearhub/view/util/__init__.py b/bearhub/view/util/__init__.py index f7b4cf15..e69de29b 100644 --- a/bearhub/view/util/__init__.py +++ b/bearhub/view/util/__init__.py @@ -1,2 +0,0 @@ -from bauh.view.util import * # noqa: F401,F403 - diff --git a/bearhub/view/util/cache.py b/bearhub/view/util/cache.py index fbf4080e..ae119351 100644 --- a/bearhub/view/util/cache.py +++ b/bearhub/view/util/cache.py @@ -1,2 +1,89 @@ -from bauh.view.util.cache import * # noqa: F401,F403 +import datetime +from threading import Lock +from typing import Optional +from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory + + +class DefaultMemoryCache(MemoryCache): + """ + A synchronized cache implementation + """ + + def __init__(self, expiration_time: int): + super(DefaultMemoryCache, self).__init__() + self.expiration_time = expiration_time + self._cache = {} + self.lock = Lock() + + def is_enabled(self): + return self.expiration_time < 0 or self.expiration_time > 0 + + def add(self, key: str, val: object): + if key and self.is_enabled(): + self.lock.acquire() + self._add(key, val) + self.lock.release() + + def _add(self, key: str, val: object): + if key: + self._cache[key] = {'val': val, 'expires_at': datetime.datetime.now(UTC) + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None} + + def add_non_existing(self, key: str, val: object): + if key and self. is_enabled(): + self.lock.acquire() + cur_val = self.get(key, lock=False) + + if cur_val is None: + self._add(key, val) + + self.lock.release() + + def get(self, key: str, lock: bool = True): + if key and self.is_enabled(): + val = self._cache.get(key) + + if val: + expiration = val.get('expires_at') + + if expiration and expiration <= datetime.datetime.now(UTC): + if lock: + self.lock.acquire() + + del self._cache[key] + + if lock: + self.lock.release() + + return None + + return val['val'] + + def delete(self, key): + if key and self.is_enabled(): + if key in self._cache: + self.lock.acquire() + del self._cache[key] + self.lock.release() + + def keys(self): + return set(self._cache.keys()) if self.is_enabled() else set() + + def clean_expired(self): + if self.is_enabled(): + for key in self.keys(): + self.get(key) + + +class DefaultMemoryCacheFactory(MemoryCacheFactory): + + def __init__(self, expiration_time: int): + """ + :param expiration_time: default expiration time for all instantiated caches + """ + super(DefaultMemoryCacheFactory, self).__init__() + self.expiration_time = expiration_time + + def new(self, expiration: Optional[int] = None) -> MemoryCache: + return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) +UTC = datetime.timezone.utc diff --git a/bearhub/view/util/disk.py b/bearhub/view/util/disk.py index 9833f506..f460f45f 100644 --- a/bearhub/view/util/disk.py +++ b/bearhub/view/util/disk.py @@ -1,2 +1,115 @@ -from bauh.view.util.disk import * # noqa: F401,F403 +import json +import logging +import os +import time +from threading import Thread, Lock +from typing import Type, Dict, Any, Optional +import yaml + +from bauh.api.abstract.cache import MemoryCache +from bauh.api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory +from bauh.api.abstract.model import SoftwarePackage + + +class AsyncDiskCacheLoader(Thread, DiskCacheLoader): + + def __init__(self, cache_map: Dict[Type[SoftwarePackage], MemoryCache], logger: logging.Logger): + super(AsyncDiskCacheLoader, self).__init__(daemon=True) + self.pkgs = [] + self._work = True + self.lock = Lock() + self.cache_map = cache_map + self.logger = logger + self.processed = 0 + self._working = False + + def fill(self, pkg: SoftwarePackage, sync: bool = False): + """ + Adds a package which data must be read from the disk to a queue (if not sync) + :param pkg: + :param sync: + :return: + """ + if pkg and pkg.supports_disk_cache(): + if sync or not self._working: + self._fill_cached_data(pkg) + else: + self.pkgs.append(pkg) + + def read(self, pkg: SoftwarePackage) -> Optional[Dict[str, Any]]: + if pkg and pkg.supports_disk_cache(): + data_path = pkg.get_disk_data_path() + + if data_path and os.path.isfile(data_path): + ext = data_path.split('.')[-1] + + try: + with open(data_path) as f: + file_content = f.read() + except FileNotFoundError: + return + + if file_content: + if ext == 'json': + cached_data = json.loads(file_content) + elif ext in {'yml', 'yaml'}: + cached_data = yaml.load(file_content) + else: + raise Exception(f'The cached data file {data_path} has an unsupported format') + + if cached_data: + return cached_data + + else: + self.logger.warning(f"No cached content in file {data_path}") + + def stop_working(self): + self._work = False + + def run(self): + self._working = True + last = 0 + + while True: + time.sleep(0.00001) + if len(self.pkgs) > self.processed: + pkg = self.pkgs[last] + + self._fill_cached_data(pkg) + self.processed += 1 + last += 1 + elif not self._work: + break + + self._working = False + + def _fill_cached_data(self, pkg: SoftwarePackage) -> bool: + cached_data = self.read(pkg) + + if cached_data: + pkg.fill_cached_data(cached_data) + cache = self.cache_map.get(pkg.__class__) + + if cache: + cache.add_non_existing(str(pkg.id), cached_data) + + return True + + return False + + +class DefaultDiskCacheLoaderFactory(DiskCacheLoaderFactory): + + def __init__(self, logger: logging.Logger): + super(DefaultDiskCacheLoaderFactory, self).__init__() + self.logger = logger + self.cache_map = {} + + def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache): + if pkg_type: + if pkg_type not in self.cache_map: + self.cache_map[pkg_type] = cache + + def new(self) -> AsyncDiskCacheLoader: + return AsyncDiskCacheLoader(cache_map=self.cache_map, logger=self.logger) diff --git a/bearhub/view/util/logs.py b/bearhub/view/util/logs.py index 2fe96436..d17e3fec 100644 --- a/bearhub/view/util/logs.py +++ b/bearhub/view/util/logs.py @@ -1,2 +1,22 @@ -from bauh.view.util.logs import * # noqa: F401,F403 +import logging +from logging import INFO +FORMAT = '%(asctime)s %(levelname)s [%(module_path)s:%(lineno)s - %(funcName)s()] - %(message)s' + + +class FilePathFilter(logging.Filter): + + def filter(self, record): + record.module_path = record.pathname.split('site-packages/')[1] if 'site-packages' in record.pathname else str(record.pathname) + return True + + +def new_logger(name: str, enabled: bool) -> logging.Logger: + instance = logging.Logger(name, level=INFO) + instance.addFilter(FilePathFilter()) + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(logging.Formatter(FORMAT)) + instance.addHandler(stream_handler) + instance.disabled = not enabled + + return instance diff --git a/bearhub/view/util/translation.py b/bearhub/view/util/translation.py index afd1ef95..807c2cdf 100644 --- a/bearhub/view/util/translation.py +++ b/bearhub/view/util/translation.py @@ -1,2 +1,89 @@ -from bauh.view.util.translation import * # noqa: F401,F403 +import glob +import locale +import os +from typing import Tuple, Set +from bauh.view.util import resource + + +class I18n(dict): + + def __init__(self, current_key: str, current_locale: dict, default_key: str, default_locale: dict): + super(I18n, self).__init__() + self.current_key = current_key + self.current = current_locale + self.default_key = default_key + self.default = default_locale + + def __getitem__(self, item): + try: + return self.current.__getitem__(item) + except KeyError: + if self.default: + try: + return self.default.__getitem__(item) + except KeyError: + return item + else: + return item + + def get(self, *args, **kwargs): + res = self.current.get(args[0]) + + if res is None: + if self.default: + return self.default.get(*args, **kwargs) + else: + return self.current.get(*args, **kwargs) + + return res + + +def get_available_keys() -> Set[str]: + locale_dir = resource.get_path('locale') + return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*') if os.path.isfile(file)} + + +def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]: + + locale_path = None + + if key is None: + try: + current_locale = locale.getlocale() + + if current_locale is None or current_locale[0] is None: + current_locale = ('en', 'UTF-8') + except Exception: + current_locale = ('en', 'UTF-8') + + else: + current_locale = [key.strip().lower()] + + if current_locale: + current_locale = current_locale[0] + + for locale_file in glob.glob(locale_dir + '/*'): + name = locale_file.split('/')[-1] + + if current_locale == name or current_locale.startswith(name + '_'): + locale_path = locale_file + break + + if not locale_path: + return current_locale if current_locale else key, {} + + with open(locale_path, 'r') as f: + locale_keys = f.readlines() + + locale_obj = {} + for line in locale_keys: + line_strip = line.strip() + if line_strip: + try: + keyval = line_strip.split('=') + locale_obj[keyval[0].strip()] = keyval[1].strip() + except Exception: + print("Error decoding i18n line '{}'".format(line)) + + return locale_path.split('/')[-1], locale_obj diff --git a/bearhub/view/util/util.py b/bearhub/view/util/util.py index 202bb503..1611d20e 100644 --- a/bearhub/view/util/util.py +++ b/bearhub/view/util/util.py @@ -1,2 +1,88 @@ -from bauh.view.util.util import * # noqa: F401,F403 +import os +import shutil +import subprocess +import sys +import traceback +from typing import List, Tuple +from PyQt5.QtCore import QCoreApplication +from PyQt5.QtGui import QIcon +from colorama import Fore + +from bauh import __app_name__ +from bauh.api.abstract.controller import SoftwareManager +from bauh.api.paths import CONFIG_DIR, CACHE_DIR, TEMP_DIR +from bauh.commons.system import run_cmd +from bauh.view.util import resource + + +def notify_user(msg: str, icon_path: str = None): + icon_id = icon_path + + if not icon_id: + icon_id = get_default_icon()[0] + + os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_id) if icon_id else '', msg)) + + +def get_default_icon(system: bool = True) -> Tuple[str, QIcon]: + if system: + system_icon = QIcon.fromTheme(__app_name__) + if not system_icon.isNull(): + return system_icon.name(), system_icon + + path = resource.get_path('img/logo.svg') + return path, QIcon(path) + + +def restart_app(): + appimage_path = os.getenv('APPIMAGE') + + restart_cmd = [appimage_path] if appimage_path else [sys.executable, *sys.argv] + + subprocess.Popen(restart_cmd) + QCoreApplication.exit() + + +def get_distro(): + if os.path.exists('/etc/arch-release'): + return 'arch' + + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as os_release_file: + for line in os_release_file: + if 'ID_LIKE=arch' in line: + return 'arch' + + if os.path.exists('/proc/version'): + if 'ubuntu' in run_cmd('cat /proc/version').lower(): + return 'ubuntu' + + return 'unknown' + + +def clean_app_files(managers: List[SoftwareManager], logs: bool = True): + + if logs: + print('[{}] Cleaning configuration and cache files'.format(__app_name__)) + + for path in (CACHE_DIR, CONFIG_DIR, TEMP_DIR): + if logs: + print('[{}] Deleting directory {}'.format(__app_name__, path)) + + if os.path.exists(path): + try: + shutil.rmtree(path) + if logs: + print('{}[{}] Directory {} deleted{}'.format(Fore.YELLOW, __app_name__, path, Fore.RESET)) + except Exception: + if logs: + print('{}[{}] An exception has happened when deleting {}{}'.format(Fore.RED, __app_name__, path, Fore.RESET)) + traceback.print_exc() + + if managers: + for m in managers: + m.clear_data() + + if logs: + print('[{}] Cleaning finished'.format(__app_name__))