mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-06 19:24:16 +02:00
AUR bearhub: fix stale source cache collision (pkgrel=10)
Use tag-specific tarball name bearhub-0.10.7-bearhub.6.tar.gz so makepkg and AUR helpers do not reuse an outdated bearhub-0.10.7.tar.gz from cache. Ignore makepkg artifacts under packaging/aur/.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
121
bauh/api/http.py
121
bauh/api/http.py
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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"<p>{self.i18n['action.reset.body_1'].format(bold(self.context.app_name))}</p>" \
|
||||
f"<p>{self.i18n['action.reset.body_2']}</p>"
|
||||
|
||||
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')
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"<p>{self.i18n['settings.changed.success.warning']}</p>",
|
||||
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"<p>{self.i18n['settings.changed.success.warning']}</p>"
|
||||
f"<p>{self.i18n['settings.changed.success.reboot']}</p>",
|
||||
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"<p>{self.i18n['settings.error']}</p>")
|
||||
|
||||
for w in warnings:
|
||||
msg.write(f'<p style="font-weight: bold">* {w}</p><br/>')
|
||||
|
||||
dialog.show_message(title=self.i18n['warning'].capitalize(), body=msg.getvalue(), type_=MessageType.WARNING)
|
||||
|
||||
self.tab_group.setEnabled(True)
|
||||
self.bt_change.setEnabled(True)
|
||||
self.bt_close.setEnabled(True)
|
||||
|
||||
def _reload_manage_panel(self):
|
||||
if self.window and self.window.isVisible():
|
||||
self.window.reload()
|
||||
|
||||
QApplication.restoreOverrideCursor()
|
||||
self.close()
|
||||
@@ -1,275 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from io import StringIO
|
||||
from subprocess import Popen
|
||||
from threading import Lock, Thread
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, QSize
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
|
||||
|
||||
from bauh import __app_name__, ROOT_DIR
|
||||
from bauh.api.abstract.model import PackageUpdate
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.commons import system
|
||||
from bauh.context import generate_i18n
|
||||
from bauh.view.core.tray_client import TRAY_CHECK_FILE
|
||||
from bauh.view.core.update import check_for_update
|
||||
from bauh.view.qt.about import AboutDialog
|
||||
from bauh.view.qt.qt_utils import load_resource_icon
|
||||
from bauh.view.util import util, resource
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
CLI_NAME = f'{__app_name__}-cli'
|
||||
|
||||
|
||||
def get_cli_path() -> str:
|
||||
if os.getenv('APPIMAGE'):
|
||||
return f"{os.environ['APPRUN_STARTUP_EXEC_PATH']} {os.environ['APPDIR']}usr/bin/{CLI_NAME}"
|
||||
|
||||
venv = os.getenv('VIRTUAL_ENV')
|
||||
|
||||
if venv:
|
||||
cli_path = f'{venv}/bin/{CLI_NAME}'
|
||||
|
||||
if os.path.exists(cli_path):
|
||||
return cli_path
|
||||
elif not sys.executable.startswith('/usr'):
|
||||
cli_path = f'{sys.prefix}/bin/{CLI_NAME}'
|
||||
|
||||
if os.path.exists(cli_path):
|
||||
return cli_path
|
||||
else:
|
||||
return shutil.which(CLI_NAME)
|
||||
|
||||
|
||||
def list_updates(logger: logging.Logger) -> List[PackageUpdate]:
|
||||
cli_path = get_cli_path()
|
||||
if cli_path:
|
||||
exitcode, output = system.execute(f'{cli_path} updates -f json')
|
||||
|
||||
if exitcode != 0:
|
||||
output_log = output.replace('\n', ' ') if output else ' '
|
||||
logger.warning(f'Command "{CLI_NAME} updates" returned an unexpected exitcode ({exitcode}). Output: {output_log}')
|
||||
elif output:
|
||||
return [PackageUpdate(pkg_id=o['id'], name=o['name'], version=o['version'], pkg_type=o['type']) for o in json.loads(output)]
|
||||
else:
|
||||
logger.info("No updates found")
|
||||
|
||||
else:
|
||||
logger.warning(f'"{CLI_NAME}" seems not to be installed')
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class UpdateCheck(QThread):
|
||||
|
||||
signal = pyqtSignal(list)
|
||||
|
||||
def __init__(self, check_interval: int, lock: Lock, check_file: bool, logger: logging.Logger, parent=None):
|
||||
super(UpdateCheck, self).__init__(parent)
|
||||
self.check_interval = check_interval
|
||||
self.lock = lock
|
||||
self.check_file = check_file
|
||||
self.logger = logger
|
||||
|
||||
def _notify_updates(self):
|
||||
with self.lock:
|
||||
updates = list_updates(self.logger)
|
||||
|
||||
if updates is not None:
|
||||
self.signal.emit(updates)
|
||||
|
||||
self.sleep(int(self.check_interval * 60))
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.check_file:
|
||||
if os.path.exists(TRAY_CHECK_FILE):
|
||||
self._notify_updates()
|
||||
try:
|
||||
os.remove(TRAY_CHECK_FILE)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
self.sleep(self.check_interval)
|
||||
else:
|
||||
self._notify_updates()
|
||||
|
||||
|
||||
class AppUpdateCheck(QThread):
|
||||
|
||||
def __init__(self, http_client: HttpClient, logger: logging.Logger, i18n: I18n, interval: int = 300):
|
||||
super(AppUpdateCheck, self).__init__()
|
||||
self.interval = interval
|
||||
self.http_client = http_client
|
||||
self.logger = logger
|
||||
self.i18n = i18n
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
update_msg = check_for_update(http_client=self.http_client, logger=self.logger, i18n=self.i18n, tray=True)
|
||||
|
||||
if update_msg:
|
||||
util.notify_user(msg=update_msg)
|
||||
|
||||
self.sleep(self.interval)
|
||||
|
||||
|
||||
class TrayIcon(QSystemTrayIcon):
|
||||
|
||||
def __init__(self, config: dict, screen_size: QSize, logger: logging.Logger, manage_process: Popen = None, settings_process: Popen = None):
|
||||
super(TrayIcon, self).__init__()
|
||||
self.app_config = config
|
||||
self.i18n = generate_i18n(config, resource.get_path('locale/tray'))
|
||||
self.screen_size = screen_size
|
||||
self.manage_process = manage_process
|
||||
self.settings_process = settings_process
|
||||
self.logger = logger
|
||||
self.http_client = HttpClient(logger=logger)
|
||||
|
||||
if config['ui']['tray']['default_icon']:
|
||||
self.icon_default = QIcon(config['ui']['tray']['default_icon'])
|
||||
else:
|
||||
self.icon_default = QIcon.fromTheme(f'{__app_name__}_tray_default')
|
||||
if self.icon_default.isNull():
|
||||
self.icon_default = QIcon.fromTheme('bauh_tray_default')
|
||||
|
||||
if self.icon_default.isNull():
|
||||
self.icon_default = load_resource_icon('img/logo.svg', 24)
|
||||
|
||||
if config['ui']['tray']['updates_icon']:
|
||||
self.icon_updates = QIcon(config['ui']['tray']['updates_icon'])
|
||||
else:
|
||||
self.icon_updates = QIcon.fromTheme(f'{__app_name__}_tray_updates')
|
||||
if self.icon_updates.isNull():
|
||||
self.icon_updates = QIcon.fromTheme('bauh_tray_updates')
|
||||
|
||||
if self.icon_updates.isNull():
|
||||
self.icon_updates = load_resource_icon('img/logo_update.svg', 24)
|
||||
|
||||
self.setIcon(self.icon_default)
|
||||
|
||||
self.menu = QMenu()
|
||||
|
||||
self.action_manage = self.menu.addAction(self.i18n['tray.action.manage'])
|
||||
self.action_manage.triggered.connect(self.show_manage_window)
|
||||
|
||||
self.action_settings = self.menu.addAction(self.i18n['tray.settings'].capitalize())
|
||||
self.action_settings.triggered.connect(self.show_settings_window)
|
||||
|
||||
self.action_about = self.menu.addAction(self.i18n['tray.action.about'])
|
||||
self.action_about.triggered.connect(self.show_about)
|
||||
|
||||
self.action_exit = self.menu.addAction(self.i18n['tray.action.exit'])
|
||||
self.action_exit.triggered.connect(lambda: QCoreApplication.exit())
|
||||
|
||||
self.setContextMenu(self.menu)
|
||||
|
||||
self.manage_window = None
|
||||
self.dialog_about = None
|
||||
self.settings_window = None
|
||||
|
||||
self.check_lock = Lock()
|
||||
self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), check_file=False, lock=self.check_lock, logger=logger)
|
||||
self.check_thread.signal.connect(self.notify_updates)
|
||||
self.check_thread.start()
|
||||
|
||||
self.recheck_thread = UpdateCheck(check_interval=5, check_file=True, lock=self.check_lock, logger=logger)
|
||||
self.recheck_thread.signal.connect(self.notify_updates)
|
||||
self.recheck_thread.start()
|
||||
|
||||
self.update_thread = AppUpdateCheck(http_client=self.http_client, logger=self.logger, i18n=self.i18n)
|
||||
self.update_thread.start()
|
||||
|
||||
self.last_updates = set()
|
||||
self.update_notification = bool(config['system']['notifications'])
|
||||
self.lock_notify = Lock()
|
||||
|
||||
self.activated.connect(self.handle_click)
|
||||
self.set_default_tooltip()
|
||||
|
||||
def set_default_tooltip(self):
|
||||
self.setToolTip(f"{self.i18n['tray.action.manage']} ({__app_name__})".lower())
|
||||
|
||||
def handle_click(self, reason):
|
||||
if reason == self.Trigger:
|
||||
self.show_manage_window()
|
||||
|
||||
def verify_updates(self, notify_user: bool = True):
|
||||
Thread(target=self._verify_updates, args=(notify_user,), daemon=True).start()
|
||||
|
||||
def _verify_updates(self, notify_user: bool):
|
||||
self.notify_updates(self.manager.list_updates(), notify_user=notify_user)
|
||||
|
||||
def notify_updates(self, updates: List[PackageUpdate], notify_user: bool = True):
|
||||
self.lock_notify.acquire()
|
||||
|
||||
try:
|
||||
if len(updates) > 0:
|
||||
self.logger.info(f"{len(updates)} updates available")
|
||||
update_keys = {f'{up.type}:{up.id}:{up.version}' for up in updates}
|
||||
|
||||
new_icon = self.icon_updates
|
||||
|
||||
if update_keys.difference(self.last_updates):
|
||||
self.last_updates = update_keys
|
||||
n_updates = len(updates)
|
||||
ups_by_type = {}
|
||||
|
||||
for key in update_keys:
|
||||
ptype = key.split(':')[0]
|
||||
count = ups_by_type.get(ptype)
|
||||
count = 1 if count is None else count + 1
|
||||
ups_by_type[ptype] = count
|
||||
|
||||
msg = StringIO()
|
||||
msg.write(self.i18n[f"notification.update{'' if n_updates == 1 else 's'}"].format(n_updates))
|
||||
|
||||
if len(ups_by_type) > 1:
|
||||
for ptype in sorted(ups_by_type):
|
||||
msg.write(f'\n * {ptype} ({ups_by_type[ptype]})')
|
||||
|
||||
msg.seek(0)
|
||||
msg = msg.read()
|
||||
self.setToolTip(msg)
|
||||
|
||||
if self.update_notification and notify_user:
|
||||
util.notify_user(msg=msg)
|
||||
|
||||
else:
|
||||
self.last_updates.clear()
|
||||
new_icon = self.icon_default
|
||||
self.set_default_tooltip()
|
||||
|
||||
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
|
||||
self.setIcon(new_icon)
|
||||
|
||||
finally:
|
||||
self.lock_notify.release()
|
||||
|
||||
def show_manage_window(self):
|
||||
if self.manage_process is None:
|
||||
self.manage_process = Popen([sys.executable, f'{ROOT_DIR}/app.py'])
|
||||
elif self.manage_process.poll() is not None: # it means it has finished
|
||||
self.manage_process = None
|
||||
self.show_manage_window()
|
||||
|
||||
def show_settings_window(self):
|
||||
if self.settings_process is None:
|
||||
self.settings_process = Popen([sys.executable, f'{ROOT_DIR}/app.py', '--settings'])
|
||||
elif self.settings_process.poll() is not None: # it means it has finished
|
||||
self.settings_process = None
|
||||
self.show_settings_window()
|
||||
|
||||
def show_about(self):
|
||||
if self.dialog_about is None:
|
||||
self.dialog_about = AboutDialog(self.app_config)
|
||||
|
||||
if self.dialog_about.isHidden():
|
||||
self.dialog_about.show()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
from bauh import ROOT_DIR
|
||||
|
||||
|
||||
def get_path(resource_path):
|
||||
return ROOT_DIR + '/view/resources/' + resource_path
|
||||
@@ -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
|
||||
@@ -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__))
|
||||
@@ -1,2 +0,0 @@
|
||||
from bauh.api import * # noqa: F401,F403
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from bauh.api.abstract import * # noqa: F401,F403
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from bauh.commons import * # noqa: F401,F403
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
0
bauh/api/__init__.py → bearhub/gems/appimage/resources/__init__.py
Normal file → Executable file
0
bauh/api/__init__.py → bearhub/gems/appimage/resources/__init__.py
Normal file → Executable file
0
bauh/api/abstract/__init__.py → bearhub/gems/appimage/resources/img/__init__.py
Normal file → Executable file
0
bauh/api/abstract/__init__.py → bearhub/gems/appimage/resources/img/__init__.py
Normal file → Executable file
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
0
bauh/commons/__init__.py → bearhub/gems/appimage/resources/locale/__init__.py
Normal file → Executable file
0
bauh/commons/__init__.py → bearhub/gems/appimage/resources/locale/__init__.py
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user